ThreadLocal的作用、使用示例
ThreadLocal是线程的本地存储,存储在其内的值只能被当前线程访问到,其他线程获取不到,可以存储任意对象。
经常用来存储当前线程的一些上下文信息,这样不用通过参数一层层的向下传递。比如在计算分库分表时,上层根据业务规则计算出这次要操作的数据库和表编号,存储到ThreadLocal中,下层可以通过ThreadLocal直接获取到。
如果当前线程创建了子线程,某些数据想让子线程也获取到,可以使用InheritableThreadLocal,用法和ThreadLocal类似。
具体使用代码如下:
public class Demo { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); private static InheritableThreadLocal inheritableThreadLocal = new InheritableThreadLocal(); public static void main(String[] args) { threadLocal.set("threadlocal value"); System.out.println(Thread.currentThread().getName() + " thread get threadlocal value : " + threadLocal.get()); new Thread(()->{ System.out.println(Thread.currentThread().getName() + " thread get threadlocal value : " + threadLocal.get()); },"child").start(); inheritableThreadLocal.set("inheritable threadlocal value"); System.out.println(Thread.currentThread().getName() + " thread get inheritable threadlocal value:" + inheritableThreadLocal.get()); new Thread(()->{ System.out.println(Thread.currentThread().getName() + " thread get inheritable threadlocal value : " + inheritableThreadLocal.get()); },"child").start(); } }
执行结果:
main thread get threadlocal value : threadlocal value main thread get inheritable threadlocal value:inheritable threadlocal value child thread get threadlocal value : nullchild thread get inheritable threadlocal value : inheritable threadlocal value
可以看到存储到ThreadLocal中的对象只能被当前线程访问到,存储到InheritableThreadLocal中的对象可以被当前线程以及子线程访问到。
实现原理及源码分析
先不看TheadLocal的源码,思考下如果让我们实现类似功能该怎么做。
首先,数据只能被当前线程访问到,最合适存储的地方肯定是Thread的一个字段,每个线程都是Thread的一个实例,都可以通过Thread.currentThread()获取到,进而可以获取到当前线程的本地存储。其他线程通过Thread.currentThread()获取到的是各自的Thread实例,所以不同线程存储的数据做到了相互隔离。
然后看下应该用什么样的数据结构。要存储数据有可能不只一条,而且有存有取是一个map结构,于是有了一个初步实现,伪代码如下:
Thread{ Map threadLocalMap = nulll; } ThreadLocal{ public set(val){ if(Thread.currentThread().threadLocalMap == null){ Thread.currentThread().threadLocalMap = new HashMap(); } Thread.currentThread().threadLocalMap.put(this,val); } public Object get(){ return Thread.currentThread().threadLocalMap.get(this); } }
使用this中作为key,这样一个threadLocal代表一条数据,多个线程可能持有同一个this,但通过this拿到的值是存储在各种threadLocalMap中的值。
下面看下ThreadLocal的源码,发现思路类似。Thread中有一个map结构的ThreadLocalMap,调用ThreadLocal.set(val)方法时,实际是以this为key,val为value存储到该map中。
具体实现上有很多不同之处,Thread.threadLocalMap访问权限是当前包,也就是对于使用方只能通过ThreadLocal暴露的方法进行操作,可以防止其他线程拿到Thread对象后对threadLcoalMap访问,引起安全性问题。
然后注意到java没有使用HashMap而是自己实现了一个ThreadLocalMap,这是出于什么原因呢?
根据ThreadLocalMap的代码,它依然是一个Hash表,但是在解决hash冲突时,不同于HashMap采用的链地址法,而是采用线行探查法。两种解决Hash冲突方法的示意图如下:
链地址法 线行探查法
链地址法:计算key的hash值并取模,存储到对应的坑位,如果发现该坑位已经被占用,则创建一个链表节点,挂在该坑位的最后,查找时,根据key的hash值计算找到对应坑位,然后遍历坑位下的链表,知道找到key相同的节点。
线行探查法:计算完key的哈希值并取模后,如果发现Hash表(数组)中对应的坑位已经存在了数据,则找该坑位的下一个坑,直到找到没有数据的坑位,进行存储。
除这两种,还有再哈希法、公共溢出区法等,在此不做详细论述。
链表法适用于经常进行插入和删除的情况,插入和删除时只需要把对应坑位的链表节点插入和删除即可,不会影响到其他坑位的数据。缺点是容易浪费空间,如果hash表有0~9十个链表,我们有十条数据但都hash冲突比较多,有可能只有第一个链表有十个数据,其他链表都是空的。而且,散列比较均匀,每个链表有一个数据,除了存储该数据,还需要存储头结点
线行探查法的有点是没有空间的浪费,只需要存储数据。缺点是不适合经常删除,如果删除一个结点,该结点之后的数据都需要重新计算要入到哪个坑位。或者可以假删除,等有下个元素的时候再进行替换。
ThreadLocal使用场景是不会频繁插入,基本不会有删除操作,所以更适合线行探查法。
使用线行探查法,要尽量保证hash值取模后能尽量均匀的三列在每个坑位上,提高插入和查找的效率。
ThreadLocalMap在计算hash值的时候采用了0x61c88647,可以保证散列的非常均匀,基本没有hash冲突。计算this,即ThreadLocal对象的hash值时,返回的是0x61c88647或0x61c88647的倍数。0x61c88647是根据斐波那契散列法得到的一个值,对应着32位有符号整数中的2654435769,用它对2的幂进行取模可以保证散列的非常均匀。
源码中ThreadLocalMap的每个Entry都是弱引用,这是为什么呢?
这个也是重新实现map,而没有使用HashMap的原因之一。ThreadLocalMap是存储在Thread对象里的,线程不结束,这个map就会永远存在不会被GC,如果一个线程之前存储了很多数据到ThrealLocal中,现在已经不用了,但没有删除,如果强引用,这些数据就不会被GC。设置成弱引用,当ThreadLocal对象已经不被引用时,不会因为被ThreadLocalMap而引用,导致无法回收内存。
可被子线程继承的本地存储 InheritableThreadLocal 实现原理解析
InheritableThreadLocal使用模板方法模式,继承自ThreadLocal。
和ThreadLocal相比,InheritableThreadLocal只有一点差别:数据存在在Thread对象的inheritableThreadLocal字段,而不是ThreadLocal字段。所以只覆盖了ThreadLcoal的getMap方法和createMap方法,分别用于获取map和给Thread.inheritableThreadLocal赋值。
在创建一个线程的时候,在线程的init方法中可以看到如下代码:
if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
inheritThreadLocals是init方法的参数,默认为true。只要父线程的inheritableThreadLocals字段不为空,在创建子线程的时候就会把这些数据复制给子线程。
ThreadLocal.createInheritedMap方法是一个工厂方法,这个方法用于创建一个map对象,并把参数传递进来的map复制到新map中,需要注意的是这个复制是浅复制,map中存的是原对象的引用。示例代码如下
public class Demo { private static InheritableThreadLocal<User> inheritableThreadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { /** * 更新对象属性示例 */ User user = new User(); user.name = "lanlingwang"; inheritableThreadLocal.set(user); Thread thread = new Thread(()->{ System.out.println(Thread.currentThread().getName() + " thread get user.name : " + inheritableThreadLocal.get().name); },"child1"); inheritableThreadLocal.get().name = "ake"; thread.start(); System.out.println(Thread.currentThread().getName() + " thread get user.name : " + inheritableThreadLocal.get().name); /** * 更新对象示例 */ Thread thread2 = new Thread(()->{ System.out.println(Thread.currentThread().getName() + " thread get user.name : " + inheritableThreadLocal.get().name); },"child2"); User user2 = new User(); user2.name = "nakelulu"; inheritableThreadLocal.set(user2); thread2.start(); System.out.println(Thread.currentThread().getName() + " thread get user.name : " + inheritableThreadLocal.get().name); } }class User{ public String name ; }
如果ThreadLocal中存储了一个User对象,其中一个线程拿到这个对象后更改了name字段,这个更改对其他线程是可见的。如果某个线程重新往ThreadLocal中set了一个新对象,只是set到当前线程而已,不会修改其他线程的数据,因为各个线程的ThreadLocalMap是独立的,只是在创建线程的时候浅复制了一份而已。
作者:FuzhePan
出处:http://www.cnblogs.com/FuzhePan/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明。