共计 4008 个字符,预计需要花费 11 分钟才能阅读完成。
一、ThreadLocal 类定义
ThreadLocal,意指线程局部变量,它可以为每一个线程提供一个实例变量的副本,每个线程独立访问和更改自己的副本,从而保证线程之间不会发生变量冲突,是一种通过将共享变量进行线程隔离而实现线程安全的方式。主要方法有以下三个:
T get():返回此线程局部变量中当前线程副本中的值。
void remove():删除此线程局部变量中当前线程的值。
void set(T value):设置此线程局部变量中当前线程副本中的值。
我们可以通过一个简单的实验,来验证 ThreadLocal 如何保证线程安全:
public class ThreadLocalTest implements Runnable {
// 重写 ThreadLocal 类的初始化方法
private ThreadLocal<Integer> i = new ThreadLocal<Integer>(){public Integer initialValue(){return 0;}
};
public static void main(String[] args){ThreadLocalTest threadLocalTest = new ThreadLocalTest();
new Thread(threadLocalTest,"st-0").start();
new Thread(threadLocalTest,"st-1").start();
new Thread(threadLocalTest,"st-2").start();}
@Override
public void run() {for (;i.get()<100;){i.set(i.get() + 1);
System.out.println(Thread.currentThread().getName() + ":" + i.get());
}
}
}
运行以上代码,截取部分实验结果,可以看到每个线程都是从 1 开始计数,每个线程更改后的变量副本并不会影响到其他线程。
st-0: 1
st-1: 1
st-0: 2
st-1: 2
st-2: 1
st-0: 3
st-2: 2
st-1: 3
二、ThreadLocal 类的实现原理
我们可以从 java.lang.ThreadLocal 类的源码出发,来了解 ThreadLocal 类的实现原理。
1、ThreadLocal 提供了一个 ThreadLocalMap 类,类的定义如下,不同的 ThreadLocal 变量在 ThreadLocalMap 中拥有自己独立的一个键值对,key 值为 ThreadLocal 实例。
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
2、ThreadLocal 定义了 ThreadLocalMap,但是 ThreadLocalMap 的引用变量保存在 Thread 实例中,与每个线程共存亡,一个 ThreadLocalMap 可以存放多个 ThreadLocal 变量。
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
3、再来看 get 和 set 方法,可以看到,每个线程拥有自己的 ThreadLocalMap,Map 中保存自己的 ThreadLocal 变量,并只对自己的 ThreadLocal 变量做读和写操作。
public T get() {Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();}
public void set(T value) {Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
三、线程池模式下 ThreadLocal 的表现
每个线程占用的资源在运行结束后均会被 JVM 回收,但是在线程池模式下,线程结束后并不会立即死亡,而是归还到线程池成为空闲线程,等待下一次调用。如果在线程池模式下使用 ThreadLocal,由于线程结束后,ThreadLocalMap 变量并没有被回收,所以线程池在下次处理同个 ThreadLocal 变量时,有可能会有冲突。
1、不使用线程池时,ThreadLocal 的表现:
public class ThreadLocalTest2 implements Runnable {
// 重写 ThreadLocal 类的初始化方法
private ThreadLocal<Integer> i = new ThreadLocal<Integer>(){public Integer initialValue(){return 0;}
};
public static void main(String[] args){ThreadLocalTest2 threadLocalTest = new ThreadLocalTest2();
Thread t1 = new Thread(threadLocalTest,"st-1");
Thread t2 = new Thread(threadLocalTest,"st-2");
t1.start();
while (t1.isAlive());
if (!t1.isAlive()) {System.out.println("t1 is dead");
t2.start();}
}
@Override
public void run() {for (;i.get()<100;){i.set(i.get() + 1);
System.out.println(Thread.currentThread().getName() + ":" + i.get());
}
i.set(50);
}
}
运行结果:
st-1: 96
st-1: 97
st-1: 98
st-1: 99
st-1: 100
t1 is dead
st-2: 1
st-2: 2
st-2: 3
st-2: 4
t2 线程在 t1 线程结束后开始启动,可以发现,i 变量仍从 0 开始,t1 线程的运行不会对 t2 线程造成影响。
2、使用线程池时,ThreadLocal 的表现:
public class ThreadPoolTest implements Runnable{
// 重写 ThreadLocal 类的初始化方法
private ThreadLocal<Integer> i = new ThreadLocal<Integer>(){public Integer initialValue(){return 0;}
};
public static void main(String[] args) throws Exception{ExecutorService pool = Executors.newFixedThreadPool(1);
ThreadPoolTest threadPoolTest = new ThreadPoolTest();
Thread t1 = new Thread(threadPoolTest,"st-1");
Thread t2 = new Thread(threadPoolTest,"st-2");
Future<Integer> result = pool.submit(t1,1);
if (result.get() == 1){System.out.println("t1 runs off");
pool.submit(t2);
}
}
@Override
public void run() {for (;i.get()<100;){i.set(i.get() + 1);
System.out.println(Thread.currentThread().getName() + ":" + i.get());
}
i.set(50);
}
}
运行结果:
pool-1-thread-1: 96
pool-1-thread-1: 97
pool-1-thread-1: 98
pool-1-thread-1: 99
pool-1-thread-1: 100
t1 runs off
pool-1-thread-1: 51
pool-1-thread-1: 52
pool-1-thread-1: 53
pool-1-thread-1: 54
我们发现,t1 线程运行结束之后,t2 线程的 i 变量从 50 开始,也就是说 t1 线程对 ThreadLocal 变量的更改,影响到了 t2 线程的读取。此时 ThreadLocal 并不能保证数据的隔离性和安全性,所以在线程池模式下,需慎重考虑用 ThreadLocal 实现线程安全的方式,可以在每次线程结束后,调用 remove 方法将 key 释放。
四、总结
ThreadLocal 可以有效隔离多个线程访问共享变量的冲突,但不适用于多个线程通过共享数据进行通信的场景。在线程池模式下,ThreadLocal 不仅会造成数据的冲突,而且有可能在线程池长时间运行时,ThreadLocal 变量长期存活在内存中,导致大量的内存消耗,故需慎重考虑两者并存的场景。