我们再介绍一个在多线程环境中经常使用的类ThreadLocal,它是java为解决多线程程序的并发问题提供了一种新的方向,使用这个ThreadLocal类可以帮助开发者很简单地编写出简洁的程序,并且是线程安全的。ThreadLocal很容易让人误解,认为是一个“本地线程”,其实ThreadLocal并不是一个Thread,而是Thread的一个局部变量。
当使用ThreadLocal变量时,ThreadLocal为每个使用这个变量的线程提供不同的变量副本,所以每一个线程改变的只是自己的副本,而不会影响其它线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量。我们先来了解一下ThreadLocal的接口方法
ThreadLocal类提供的接口方法比较简单,很容易使用:
public void set(Object value) 该方法设置当前线程局部变量的值。
public Object get() 该方法返回当前线程所对应的线程局部变量的值。
public void remove() 该方法删除当前线程局部变量的值,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。这这里需要说明的是,当线程结束后,该线程的所有局部变量将自动被垃圾回收器(GC)回收,所以显式调用该方法清除线程的某些局部变量并不是必须的操作,但它可以加快内存回收的速度。
protected Object initialValue() 返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的,ThreadLocal中的缺省实现直接返回一个null。
下面举一个列子来说明一下他的用法。
public class TestThreadLocal {
static class myRunnable implements Runnable {
int index;
public myRunnable(int i){
index =i;
}
public void run() {
myThreadLocal.set("Thread:"+ index);
System.out.println(myThreadLocal.get());
}
}
public static ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();
public static void main(String[] args){
Thread thread1 = new Thread(new myRunnable(1));
Thread thread2 = new Thread(new myRunnable(2));
thread1.start();
thread2.start();
System.out.println(myThreadLocal.get());
}
}
执行的结果如下:
Thread:1
Thread:2
null
thread 1 返回Thread:1的结果,thread2返回Thread:2,最后一个null是main线程的执行结果,因为 main线程没有设置自己的局部变量。
ThreadLocal的实现原理
在介绍ThreadLocal变量之前,我们先来看一个Thread的内部变量:
ThreadLocal.ThreadLocalMap threadLocals = null;
这个变量就是线程用来存储自己的ThreadLocal变量的空间,他的类型 ThreadLocal.ThreadLocalMap,作用相当于一个hashmap,对这种数据类型的操作是不需要加锁的,因为只有一个线程(也就是线程自己)才可能操作到自己的变量。
我们先看ThreadLocal源代码是来解析两个比较重要的函数:set和get
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//拿到本地线程map
if (map != null)
map.set(this, value);//map存在时,添加value
else
createMap(t, value);//map不存在时,创建map并且添加value
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //拿到本地线程map
if (map != null) {//map存在时,或者key
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
从代码上看,ThreadLocal函数都是先获得当前线程的引用,然后得到线程局部变量,最后以ThreadLocal作为key,设置或者获取value,线程本地变量是类型为ThreadLocalMap的变量,ThreadLocalMap相当于一种哈希表,有兴趣的可以看一下。整个过程没有任何同步操作,因此性能很高。
ThreadLocal与其它同步机制的比较
ThreadLocal和线程同步机制都是为了解决多线程访问变量冲突的问题,同步机制是通过对象的锁机制保证同一时间只有一个线程访问变量,此时该变量是多个线程共享的,使用同步机制要求用户分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等复杂的问题,程序设计和编写难度相对比较大。而ThreadLocal则从另一个角度来解决多线程的并发访问,它会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突,因为每一个线程都拥有自己的变量副本,它只操作自己的变量副本,从而也就没有必要对该变量进行同步了。
当然ThreadLocal并不能替代同步机制,两者面向的问题不同。同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源,这样当然不需要对多个线程进行同步了。所以,如果你需要进行多个线程之间进行通信,则使用同步机制;如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化你的程序,使程序更加易读、简洁。
InheritableThreadLocal
通常线程还需要维护另外一个变量 inheritableThreadLocals
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null
当创建一个新的线程时,线程会从父线程哪里复制一份数据。也就是是说,子线程会享有父线程的数据,下面的代码摘自Thread.init函数,子线程创建时,将会调用。
if (parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
请看它的getMap函数,显然操作的是inheritableThreadLocals变量
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
这时所有的操作都是针对Thread的inheritableThreadLocals,这种操作也是线程安全的。
我们来举一个例子来说明InheritableThreadLocal的说法
public class TestInheritableThreadLocal {
static class myRunnable implements Runnable {
int index;
public myRunnable(int i){
index =i;
}
public void run() {
//
System.out.println("thread " + index + "-" + myThreadLocal.get());
myThreadLocal.set("Thread:"+ index);
System.out.println("thread " + index + "-" + myThreadLocal.get());
}
}
public static InheritableThreadLocal<String> myThreadLocal = new InheritableThreadLocal<String>();
public static void main(String[] args){
myThreadLocal.set("parent main thread");
Thread thread1 = new Thread(new myRunnable(1));
Thread thread2 = new Thread(new myRunnable(2));
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("main thead -" + myThreadLocal.get());
}
}
结果如下:
thread 1-parent main thread
thread 2-parent main thread
thread 2-Thread:2
thread 1-Thread:1
main thead -parent main thread
线程1和线程2会共享main线程的数据,子线程也可以修改自己的副本,修改后,线程1得到的结果Thread:1,线程2得到结果Thread:2
而父线程(main)的变量时不会噶边的,因为子线程的修改只作用于自己的变量空间,不会作用到父线程。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而不受影响。