ThreadLocal声明
什么是线程封闭?在多线程中访问共享可变数据时通常需要同步机制来保证线程安全。线程封闭是一种不使用同步来实现线程安全的方式----如果我们仅在单线程中使用该数据,当然就不用使用同步机制了。也就是说,将数据封闭在单线程中来自动实现线程安全。
ThreadLocal可以实现线程封闭,因为我们都知道ThreadLocal类为变量在每个线程中创建了一个副本,在每个线程中操作的其实是该线程独占的变量副本,不需要同步机制即可实现线程安全。
首先,我们来看一下ThreadLocal上的声明:
该类提供线程局部变量。这些变量和它们相对应的正常变量不同,因为每一个线程访问的都是自己独占的、独立初始化的变量副本。所谓线程局部变量,我们可以对比局部变量的定义,该变量的作用域只在本线程中,线程消失,线程局部变量消失(除非其他地方仍在存在引用)。例如下面的类为每个线程生成本地唯一标识符(线程的ID是在第一次调用 ThreadId.get()时分配的,并且在以后的调用中保持不变):
每个线程都会对其线程局部变量的副本保留隐式引用,前提是线程是活动的并且该ThreadLocal可以访问。线程消失后,其所有ThreadLocal实例副本都将进行垃圾回收(除非存在对这些副本的其他引用)
上面的示例代码可能有些人看不懂,这里简单解释一下(看不懂没关系,后面会详细讲解):
示例中用ThreadLocal类对nextId实现了了线程封闭,每个线程来调用ThreadId类的get方法,其实是在对nextId的副本进行操作,我们写一个main方法来测试一下:
输出如下:
可以看到,thread2并没有对thread1产生影响,thread1在thread2执行过之后,还是会输出原来的1。好了,这里已经证明了ThreadLocal可以实现线程封闭。
下面来熟悉一下ThreadLocal的方法和实现逻辑(然后就能弄清楚为什么上面的示例可以实现一个线程多次调用get方法返回相同的值)。
ThreadLocal剖析
ThreadLocal主要对外提供4个方法:
public T get() { ... } //获取变量副本public void set(T value) { ... } //设置变量副本public void remove() { ... } //移除变量副本protected T initialValue() { ... } //初始化副本。需要用户重写该方法(默认实现直接返回null)
get方法和initialValue方法
get()方法返回此线程局部变量的当前线程副本的值。 如果该变量没有当前线程的副本的值,则首先将其初始化为调用initialValue()方法返回的值。
那么该方法是如何获取当前线程副本的值的呢?看代码不难猜出,每个线程Thread都有一个ThreadLocalMap类型,其中保存有该线程中用到的变量副本。键为当前ThreadLocal变量(代码中查询map传入的是this,即当前的ThreadLocal),value为变量副本(即T类型的变量)。
简单来讲,当前线程里面如果要使用副本变量,就是通过get方法在threadLocals里面查找。反过来看上面的例子,线程thread1的ThreadLocalMap中就保存有这个副本(当然第一次调用get方法时ThreadLocalMap是空的,这时候就会调用我们重写的initialValue()方法初始化并放入ThreadLocalMap中,以后调用get直接去Map中查找即可,所以同一个线程里每次调用返回的值都一样),如果线程还用到了另一个变量副本比如nextTime,可能ThreadLocalMap中会增加一条数据:。
set方法
可以看到,set方法和setInitialValue方法的前半部分一模一样。仔细分析代码可以发现,如果我们没有实现initialValue方法,那么在get之前,必须手动调用set方法先保存线程局部变量到ThreadLocalMap,否则会报空指针异常。
remove方法
remove方法很简单,只是从ThreadLocalMap中删除该线程局部变量而已。
总结
- 每一个线程局部变量ThreadLocal是保存在线程Thread的ThreadLocalMap中的,一个线程有多少个线程局部变量,ThreadLocalMap中就有多少个元素。
- 因为线程局部变量ThreadLocal是保存在线程中的,只有该线程可以访问,所以是线程封闭的,可以保证多线程安全。
上一篇文章
帮你理清楚Java反射应该如何写