Java并发编程(二十):为啥ThreadLocal设置为static的还能保证线程安全
为啥ThreadLocal设置为static的还能保证线程安全1 概述2 static角色的本质一把“储物柜的钥匙”3 Tread内部构造真正的“私有储物柜”4 JVM内存回收的“陷阱”5 总结大家好我是欧阳方超公众号同名。1 概述使用ThreadLocal类型的成员变量时一般会将其设置为static的呢既然是static的那它就是全局共享的为什么却能实现数据的线程隔离呢私有化。为了探讨这个这个问题需要把视角从代码层级下移到JVM运行时内存布局和Thread对象的内部结构。2 static角色的本质一把“储物柜的钥匙”在JVM中static变量属于类存储在metaspace中JDK8。一般下面所声明的成员变量在整个JVM进程只有一份privatestaticThreadLocalStringuserContextnewThreadLocal();其实ThreadLocal示例本身并不存储任何用户信息如用户A或用户B。它更像是一个索引或钥匙。数据存在每个Thread对象的内部static修饰ThreadLocal是为了让所有线程都能通过这把“同样的钥匙”去各自的私人口袋里找东西。3 Tread内部构造真正的“私有储物柜”在JVM层面每一个线程Thread实例都在堆Heap内存中对应一个对象。在这个对象内部有一个隐藏的成员变量threadLocals它的类型是ThreadLocalMap。当执行userContext.set(“用户A”)时JVM底层发生了以下动作获取当前线程通过Thread.currentThread()找到正在运行的线程对象。定位Map找到该线程对象内部的threadLocals属性这是一个特殊的Map。存入数据Key是那个static的userContext对象的弱引用Value是“用户A”因此如果有两个线程Thread-A、Thread-B他们是如何通过ThreadLocal类型的静态成员变量将各自的数据存入各自的线程的呢下表展示了这一过程步骤Thread-A的动作Thread-B的动作JVM视角1.启动创建Thread-A对象创建Thread-B对象堆内存中出现了两个独立的Thread实例2.set()Thread.currentThread().threadLocals.put(userContext, “用户A”)Thread.currentThread().threadLocals.put(userContext, “用户B”)静态变量userContext指向同一个地址但它在两个线程不同的Map里充当Key3.get()从自己的Map里找userContext对应的Value从自己的Map里找userContext对应的Value虽然Key是同一个但存放值的Map容器是线程私有的所以互不干扰注意Thread.currentThread().threadLocals在IDE中是无法直接调出threadLocals的这里只是一种伪代码的写法在Thread的源码中获取当前线程并往其threadLocalMap中set值的逻辑如下片段publicvoidset(Tvalue){ThreadtThread.currentThread();ThreadLocalMapmapgetMap(t);if(map!null)map.set(this,value);elsecreateMap(t,value);}ThreadLocalMapgetMap(Threadt){returnt.threadLocals;}4 JVM内存回收的“陷阱”由于userContext是static的它的生命周期和类一样长。因此如果线程运行完就销毁了那没问题Thread对象被回收内部的Map也就消失了但是在现在IT架构中我们使用线程池线程是不销毁的会重复利用如果Thread-A处理完“用户A”后回到池子里没调用remove()下次这个线程去处理“用户C”时如果不小心调用了get()可能还会拿到“用户A”的数据。这就是内存泄漏和数据污染的根源。5 总结ThreadLocal一般都设置为static的看似每个线程都会共享这个变量所以会带来它是如何保证线程安全的这一令人费解的问题因为被多个线程共享的东西肯定潜伏着安全问题但由于这个变量并不保存实际的数据数据都在堆中每个线程对象里面保存着而且各自保存各自的因此它是从各自线程保存各自数据的角度来解决线程安全的而不是从用锁保护共享资源的角度。我是欧阳方超把事情做好了自然就有兴趣了如果你喜欢我的文章欢迎点赞、转发、评论加关注。我们下次见。