Java ThreadLocal
关于ThreadLocal
ThreadLocal相当于一个装饰器,装饰一个变量,通常将ThreadLocal实例定义为静态的。通常用来存一些多个线程都会用到的全局上下文信息,如请求信息、用户身份信心、数据库连接、当前事务等。
其作用是既使得各线程都可访问到该实例、又使各线程访问到的实际上是本线程私有的变量副本从而不用进行同步。这样就不用在各线程方法间来回传递该变量从而简化代码。
若不使用ThreadLocal,要实现上述两个效果,前者可以通过声明一个静态变量实现,但此时存在线程安全问题;后者可以通过在每个线程中声明一个变量实现,但变量无法在不同线程中共享。
对于多线程资源共享的问题,传统的同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。
前者仅提供一份变量,让不同的线程排队访问;
而后者每一个线程都有自己的一份变量副本(逻辑上的,实际上并没有复制什么副本,因为每个线程有自己的ThreadLocalMap成员,key为ThreadLocal实例的弱引用、value为变量的值,操作ThreadLocal变量实际上是操作线程自己的ThreadLocalMap,如下图所示),所以线程间对该变量的访问也就不存在竞争了,因此可以同时访问而互不影响(感觉实际上只是个语法糖,因为可以为每个线程创建一个变量实现等价效果)。但ThreadLocal变量的这种隔离策略也不是任何情况下都能使用的。如果多个线程并发访问的对象实例只允许或只能创建一个,那只能使用同步机制来访问。
ThreadLocal的典型用途是存储上下文信息,避免在不同代码间来回传递,简化代码。比如在一个Web服务器中,一个线程执行用户的请求,在执行过程中,很多代码都会访问一些共同的信息,比如请求信息、用户身份信息、数据库连接、当前事务等,它们是线程执行过程中的全局信息,如果作为参数在不同代码间传递,代码会很啰嗦,这时,使用ThreadLocal就很方便(设置个static ThreadLocal变量),所以它被用于各种框架如Spring中。
JDK建议将ThreadLocal变量定义成private static
的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal对象的强引用,所以ThreadLocal对象也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。
几个问题:
为什么把ThreadLocaMap放Thread对象里而非ThreadLocal对象里?不太好:一是每个线程可能有多个ThrealLocal变量,放TrheadLocal对象里则key须加特殊标识变量是哪个线程的;二是多线程并发操作同一个Map性能较低且需要同步;三是不知何时要删除Map的entry。
为什么通常定义为static的?因为实际上是借助各线程内部的ThreadLocalMap起数据隔离作用的,而非借助ThrealLocal对象自身,为了节省空间那创建一个就行而没必要多个。
更多详情可参阅:https://mp.weixin.qq.com/s/qSUi_bQ3trRUKEC84UvQVA
ThreadLocalMap的memory leak问题
什么是memory leak?申请内存使用完后不用了,既没释放OS又没法回收。
相关概念:
ThreadLocal Ref:ThreadLocal引用,或称ThreadLocal变量
TThreadLocal对象:ThreadLocal Ref所指向的堆上的对象
Thread Ref:线程变量
Thread对象
ThreadLocalMap、ThreadLocalMap.entry
Memory Leadk问题:(详情参阅:ThreadLocal Memry Leak问题)
指对象无法被回收的问题,这里的对象包括三个:ThreadLocal对象,该ThrealLocal对象在各线程的ThreadLocaMap中对应的Entry对象、Value对象。ThreadLocal对象与对应的Enry的Key对象是同一个对象。
导致Memory Leak(ThreadlLocal对象未被回收)的原因有两个:用到ThreadlLocal对象的线程还在执行、该对象对应的entry未删除,从而存在对该对象的强引用。
以下是分析。
ThreadLocal Ref(即ThreadLocal变量)是强引用类型,要使得ThreadLocal对象被回收,直接将该引用变量置null(ThreadLocal变量通常是静态变量,故置1次即可)即可,垃圾回收器会进行回收。在此前提下:
1 若ThreadLocalMap的key是强引用(Strong Reference)类型,则存在ThreadLocal对象无法被回收的问题。
即使将ThreadLocal Ref 变量赋为了null,只要存在 用过该ThreadLocal变量 的线程尚未结束 且 此线程未删除该变量对应的Entry(即未调用该变量的remove方法),则就存在 CurrentThread Ref → CurrentThread对象 →Map(ThreadLocalMap)对象-> entry对象 -> ThreadLocal对象 的强引用链,从而ThreadLocal对象无法被回收。示意如下:
实际上,此时 该ThreadLocal对象在各线程的ThrealLocalMap中对应的Entry对象、Value对象也无法被回收,除非手动删除(即调用ThreadLocal的remove方法)。
2 若ThreadLocalMap的key是弱引用(Weak Reference)类型 ,则不存在ThreadLocal对象无法被回收的问题,但仍存在对应的Entry对象、Value对象无法被回收的问题。
将ThreadLocal Ref 变量赋为了null后,由于key是弱引用类型时,其引用的对象虽然有被引用但仍会在存活过一次GC后被回收,从而ThreadLocal对象可被正常回收,此时key变为null。
在未手动删除对应的Entry及用过该ThreadLocal变量的线程尚未结束的情况下,同样存在到该Entry对象、Value对象的强引用链,故这两者仍无法被回收。示意图如下:
可见 导致Memory Leak(ThreadlLocal对象未被回收)的原因有两个:用到ThreadlLocal对象的线程还在执行、该对象对应的entry未删除,从而存在对该对象的强引用。
3 解决方案:在方案2的基础上增加额外处理逻辑——在 ThreadLocalMap 的put/get/remove/getEntry等方法中会判断key是否为null(即key指向的ThreadLocal是否被回收了),若是则会删除该key对应的entry、value,从而解决方案2存在的问题。相关源码:
private int expungeStaleEntry(int staleSlot) { Entry[] tab = table; int len = tab.length; // expunge entry at staleSlot tab[staleSlot].value = null; tab[staleSlot] = null; size--; // Rehash until we encounter null Entry e; int i; for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == null) { e.value = null; tab[i] = null; size--; } else { int h = k.threadLocalHashCode & (len - 1); if (h != i) { tab[i] = null; // Unlike Knuth 6.4 Algorithm R, we must scan until // null because multiple entries could have been stale. while (tab[h] != null) h = nextIndex(h, len); tab[h] = e; } } } return i; }
ThreadLocalMap在实现时,实际上是将整个Entry作为WeakReference,相关源码:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
综上,要避免memory leak,则应该在不用该ThrealLocal时在线程内部remove掉相关key,就像使用Lock加锁后记得解锁一样。
Java 的Reference在实现上提供了一些机制使得在Soft Reference、Weak Reference、Phantom Reference 等所对应的实际对象在回收时用户代码可以知道发生回收了,如通过引用队列、通过null判断(ThreadLocal即用此)、通过Cleaner,详情参阅 Java Reference原理。
应用
session等
实际项目中使用举例1
涉及的平台:客户管理平台、超管平台(大后台)。前者管理客户、子客户、学校、教师、学生、班级等,后者负责创建顶级客户、不同角色的超管等。前者的顶级客户是由后者调用前者的Feign API创建的。
现状:客户管理平台内各API的实现逻辑中有个前提假设————若设置了id则认为是修改否则认为是新增。该前提会导致新增的数据在大后台和本平台内的id不一致。
解决:为在尽可能少改变本平台原有实现的前提下解决该问题做如下特殊处理: 让大后台先在其内部新增并将新增后的数据的id也传来,本平台也使用这些id作为新增数据的id。其风险之一是由于id依赖调用者设置,可能重,须由调用者保证不重。
实现:借助ThreadLocal存可要复用的id。
/** * 用于一个线程内id复用的工具类。<br> * 本平台内的关于新增或修改数据(如客户、管理员等)的实现中的一个假设是“若请求数据中设置了id则认为是修改否则是新增”;而在大后台和本平台同步客户等数据时,不管是新增还是修改数据,传来的数据中id都是有值的。为在不改变本平台内原有实现的前提下解决该问题,引入此工具。 */ public class IdUtil { private static ThreadLocal<Queue<String>> reuseableIdsThreadLocal = new ThreadLocal<>(); /** 设置将要被复用的id,使用时将从会前往后取 */ public static void setReuseableIds(Queue<String> reuseableIdQueue) { reuseableIdsThreadLocal.set(reuseableIdQueue); } /** 从可复用的id列表中取id,若不存在可复用者,则直接生成UUID */ public static String getId() { Queue<String> reuseableIds = reuseableIdsThreadLocal.get(); return CollectionUtils.isEmpty(reuseableIds) ? UUID.randomUUID().toString() : reuseableIds.poll(); } }
Jedis 连接公用导致响应数据串在一起
项目中使用到Jedis客户端,然而不想直接使用Jedis,因为使用到的地方就需要处理连接过程,因此打算对之封装一层——将连接处理封装起来,封装成JedisUtil,JedisUtil暴露少量接口供外调用。然而使用过程中遇到一个问题:在JedisUtil中维护一个公共连接时,llen、lpush等可以功能正常使用,然而对于brpop等就有问题了:后者阻塞等待连接返回数据,若与llen等共用一个连接,llen、brpop的返回会交杂在一起,从而出错。因此需要为brpop维护单独的连接来执行该命令,该连接与调用者线程挂钩,这时用ThreadLocal就很方便了。