当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了。
数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。
本文主要介绍线程封闭中的其中一种体现:ThreadLocal,将会介绍什么是 ThreadLocal;从 ThreadLocal 源码角度分析,最后介绍 ThreadLocal 的应用场景。
什么是 ThreadLocal?
ThreadLocal 是 Java 里一种特殊变量,它是一个线程级别变量,每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞态条件被彻底消除了,在并发模式下是绝对安全的变量。
可以通过 ThreadLocal<T> value = new ThreadLocal<T>();
来使用。
会自动在每一个线程上创建一个 T 的副本,副本之间彼此独立,互不影响,可以用 ThreadLocal 存储一些参数,以便在线程中多个方法中使用,用以代替方法传参的做法。
下面通过例子来了解下 ThreadLocal:
public class ThreadLocalDemo { /** * ThreadLocal变量,每个线程都有一个副本,互不干扰 */ public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>(); public static void main(String[] args) throws Exception { new ThreadLocalDemo().threadLocalTest(); } public void threadLocalTest() throws Exception { // 主线程设置值 THREAD_LOCAL.set("wupx"); String v = THREAD_LOCAL.get(); System.out.println("Thread-0线程执行之前," + Thread.currentThread().getName() + "线程取到的值:" + v); new Thread(new Runnable() { @Override public void run() { String v = THREAD_LOCAL.get(); System.out.println(Thread.currentThread().getName() + "线程取到的值:" + v); // 设置 threadLocal THREAD_LOCAL.set("huxy"); v = THREAD_LOCAL.get(); System.out.println("重新设置之后," + Thread.currentThread().getName() + "线程取到的值为:" + v); System.out.println(Thread.currentThread().getName() + "线程执行结束"); } }).start(); // 等待所有线程执行结束 Thread.sleep(3000L); v = THREAD_LOCAL.get(); System.out.println("Thread-0线程执行之后," + Thread.currentThread().getName() + "线程取到的值:" + v); } }
首先通过 static final
定义了一个 THREAD_LOCAL
变量,其中 static
是为了确保全局只有一个保存 String 对象的 ThreadLocal 实例;final
确保 ThreadLocal 的实例不可更改,防止被意外改变,导致放入的值和取出来的不一致,另外还能防止 ThreadLocal 的内存泄漏。上面的例子是演示在不同的线程中获取它会得到不同的结果,运行结果如下:
Thread-0线程执行之前,main线程取到的值:wupx Thread-0线程取到的值:null 重新设置之后Thread-0线程取到的值为:huxy Thread-0线程执行结束 Thread-0线程执行之后,main线程取到的值:wupx
首先在 Thread-0
线程执行之前,先给 THREAD_LOCAL
设置为 wupx
,然后可以取到这个值,然后通过创建一个新的线程以后去取这个值,发现新线程取到的为 null,意外着这个变量在不同线程中取到的值是不同的,不同线程之间对于 ThreadLocal 会有对应的副本,接着在线程 Thread-0
中执行对 THREAD_LOCAL
的修改,将值改为 huxy
,可以发现线程 Thread-0
获取的值变为了 huxy
,主线程依然会读取到属于它的副本数据 wupx
,这就是线程的封闭。
看到这里,我相信大家一定会好奇 ThreadLocal 是如何做到多个线程对同一对象 set 操作,但是 get 获取的值还都是每个线程 set 的值呢,接下来就让我们进入源码解析环节:
ThreadLocal 源码解析
首先看下 ThreadLocal 都有哪些重要属性:
// 当前 ThreadLocal 的 hashCode,由 nextHashCode() 计算而来,用于计算当前 ThreadLocal 在 ThreadLocalMap 中的索引位置 private final int threadLocalHashCode = nextHashCode(); // 哈希魔数,主要与斐波那契散列法以及黄金分割有关 private static final int HASH_INCREMENT = 0x61c88647; // 返回计算出的下一个哈希值,其值为 i * HASH_INCREMENT,其中 i 代表调用次数 private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } // 保证了在一台机器中每个 ThreadLocal 的 threadLocalHashCode 是唯一的 private static AtomicInteger nextHashCode = new AtomicInteger();
其中的 HASH_INCREMENT
也不是随便取的,它转化为十进制是 1640531527
,2654435769
转换成 int 类型就是 -1640531527
,2654435769
等于 (√5-1)/2
乘以 2 的 32 次方。(√5-1)/2
就是黄金分割数,近似为 0.618
,也就是说 0x61c88647
理解为一个黄金分割数乘以 2 的 32 次方,它可以保证 nextHashCode 生成的哈希值,均匀的分布在 2 的幂次方上,且小于 2 的 32 次方。
下面是 javaspecialists 中一篇文章对它的介绍:
This number represents the golden ratio (sqrt(5)-1) times two to the power of 31 ((sqrt(5)-1) * (2^31)). The result is then a golden number, either 2654435769 or -1640531527.
下面用例子来证明下:
private static final int HASH_INCREMENT = 0x61c88647; public static void main(String[] args) throws Exception { int n = 5; int max = 2 << (n - 1); for (int i = 0; i < max; i++) { System.out.print(i * HASH_INCREMENT & (max - 1)); System.out.print(" "); } }
运行结果为:0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25
可以发现元素索引值完美的散列在数组当中,并没有出现冲突。
ThreadLocalMap
除了上述属性外,还有一个重要的属性 ThreadLocalMap,ThreadLocalMap 是 ThreadLocal 的静态内部类,当一个线程有多个 ThreadLocal 时,需要一个容器来管理多个 ThreadLocal,ThreadLocalMap 的作用就是管理线程中多个 ThreadLocal,源码如下:
static class ThreadLocalMap { /** * 键值对实体的存储结构 */ static class Entry extends WeakReference<ThreadLocal<?>> { // 当前线程关联的 value,这个 value 并没有用弱引用追踪 Object value; /** * 构造键值对 * * @param k k 作 key,作为 key 的 ThreadLocal 会被包装为一个弱引用 * @param v v 作 value */ Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } // 初始容量,必须为 2 的幂 private static final int INITIAL_CAPACITY = 16; // 存储 ThreadLocal 的键值对实体数组,长度必须为 2 的幂