并发编程之ThreadLocal 前言
当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了。
数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。
本文主要介绍线程封闭中的其中一种体现:ThreadLocal,将会介绍什么是 ThreadLocal;从 ThreadLocal 源码角度分析,最后介绍 ThreadLocal 的应用场景。
什么是ThreadLocalThreadLocal 是 Java 里一种特殊变量,它是一个线程级别变量,每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞态条件被彻底消除了,在并发模式下是绝对安全的变量。
可以通过 ThreadLocal value = new ThreadLocal(); 来使用。
会自动在每一个线程上创建一个 T 的副本,副本之间彼此独立,互不影响,可以用 ThreadLocal 存储一些参数,以便在线程中多个方法中使用,用以代替方法传参的做法。
下面通过例子来了解下 ThreadLocal:
@Slf4j public class ThreadLocalUtil { /** * static 确保全局只有一个保存 String 对象的 ThreadLocal 实例 * final 确保 ThreadLocal 实例不可更改 防止被意外改变 导致存入的值和取出的值不一致,并且还能防止 ThreadLocal 实例内存泄漏 */ private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>(); public static void main(String[] args) throws InterruptedException { // 主线程设置值 THREAD_LOCAL.set("主线程值"); String v = THREAD_LOCAL.get(); log.info("线程pool-1-thread-1执行之前," + Thread.currentThread().getName() + " 线程获取到的值为:{}", v); ExecutorService executorService = Executors.newFixedThreadPool(1); executorService.submit(() -> { String s = THREAD_LOCAL.get(); log.info(Thread.currentThread().getName() + " 线程获取到的值为:{}", s); // 子线程设置值 THREAD_LOCAL.set("子线程值"); s = THREAD_LOCAL.get(); log.info("重新设置值之后," + Thread.currentThread().getName() + " 线程获取到的值为:{}", s); log.info(Thread.currentThread().getName() + "线 程执行结束"); THREAD_LOCAL.remove(); }); // 等待子线程执行结束 Thread.sleep(1000L); v = THREAD_LOCAL.get(); log.info("线程pool-1-thread-1执行之后," + Thread.currentThread().getName() + " 线程获取到的值为:{}", v); THREAD_LOCAL.remove(); } }首先通过 static final 定义了一个 THREAD_LOCAL 变量,其中 static 是为了确保全局只有一个保存 String 对象的 ThreadLocal 实例;
final 确保 ThreadLocal 的实例不可更改,防止被意外改变,导致放入的值和取出来的不一致,另外还能防止 ThreadLocal 的内存泄漏。上面的例子是演示在不同的线程中获取它会得到不同的结果,运行结果如下:
14:33:42.176 [main] INFO com.linkcld.redis.util.ThreadLocalUtil - 线程pool-1-thread-1执行之前,main 线程获取到的值为:主线程值 14:33:42.307 [pool-1-thread-1] INFO com.linkcld.redis.util.ThreadLocalUtil - pool-1-thread-1 线程获取到的值为:null 14:33:42.307 [pool-1-thread-1] INFO com.linkcld.redis.util.ThreadLocalUtil - 重新设置值之后,pool-1-thread-1 线程获取到的值为:子线程值 14:33:42.307 [pool-1-thread-1] INFO com.linkcld.redis.util.ThreadLocalUtil - pool-1-thread-1线 程执行结束 14:33:43.307 [main] INFO com.linkcld.redis.util.ThreadLocalUtil - 线程pool-1-thread-1执行之后,main 线程获取到的值为:主线程值首先在 pool-1-thread-1 线程执行之前,先给 THREAD_LOCAL 设置为 主线程值,然后可以取到这个值,然后通过创建一个新的线程以后去取这个值,发现新线程取到的为 null,意外着这个变量在不同线程中取到的值是不同的,不同线程之间对于 ThreadLocal 会有对应的副本,接着在线程 pool-1-thread-1 中执行对 THREAD_LOCAL 的修改,将值改为 子线程值,可以发现线程 pool-1-thread-1 获取的值变为了 子线程值,主线程依然会读取到属于它的副本数据 主线程值,这就是线程的封闭。
看到这里,我相信大家一定会好奇 ThreadLocal 是如何做到多个线程对同一对象 set 操作,但是 get 获取的值还都是每个线程 set 的值呢,接下来就让我们进入源码解析环节:
ThreadLocal 源码解析首先看下 ThreadLocal 都有哪些重要属性:
// 当前 ThreadLocal 的 hashCode 由 nextHashCode 计算得来的,用于计算当前 ThreadLocal 再 ThreadLocalMap 中的索引位置 private final int threadLocalHashCode = nextHashCode(); // 哈希魔数,主要与斐波那契散列法和黄金分割相关 private static final int HASH_INCREMENT = 0x61c88647; // 返回计算出的hash值,其值为 i * HASH_INCREMENT,其中 i 代表调用次数 private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } // 保证了一台机器上,每个 ThreadLocal 的 threadLocalHashCode 值是唯一的 rivate static AtomicInteger nextHashCode = new AtomicInteger();