IZZY 2018-01-01
阅读目录
什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象的线程创建了一个独立的变量副本。
用一句话总结ThreadLocal真的实在是太苍白无力了!我们通过一个简单的例子入手。比如现在有A和B两台服务器需要通过http请求传递数据,但又希望数据安全性有一定保障,因此发送方A决定用AES算法对传输数据加密后再发送给B。接受方B收到数据后,通过密钥解密数据并进行后续的业务处理。
对数据进行AES解密,接收方B可选择JAVA提供的Cipher类来实现。我们在调用Cipher类进行解密时时,需要获取Cipher对象的实例,如下所示:
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
接着我们调用该实例就可以进行数据解密工作。但很不幸的是,Cipher类存在线程安全问题,它无法工作于多线程场景下。简单来说就是单个Cipher实例无法同时解密多条数据。
那怎么办呢?
有没有什么办法能让每个线程拥有相同的instance实例,且彼此互不干扰呢?这个时候我们可以借助ThreadLocal类来实现特定功能,ThreadLocal能够为每个使用该对象的线程创建独立的变量的副本。这样就满足了我们的需求。
后续篇幅我们会深入到ThreadLocal的源码层来探讨它的实现机制,在看ThreadLocal类的几个基本方法前,让我们先看一下ThreadLocal中静态类ThreadLocalMap的实现,它对于我们理解ThreadLocal有着举足轻重的作用!
ThreadLocalMap是ThreadLocal类中的一个静态类,它拥有一个Entry数组类型的成员变量,名为table。这个Entry是啥?我们来看看。
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
Entry类是ThreadLocalMap中的一个静态类,它继承了WeakReference类,同时拥有一个类型为Object的value成员变量。当我们创建Entry对象后,调用Entry.get()方法,获取到的实际上是ThreadLocal对象的弱引用。而这个设计则保证了Entry对象中保存的ThreadLocal弱引用是易被回收的。网上有很多关于ThreadLocal对象是否会引发内存泄漏的文章,这里的内存泄漏通常指的不是entry的key,也就是ThreadLocal的弱引用。而是这里的value对象。实际上ThreadLocalMap自身提供了一套回收无用Entry节点的机制。在后面我们会聊到它的实现。关于ThreadLocal是否会引发内存泄漏,这里暂时不做探讨。
privatevoidset(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
ThreadLocalMap类持有一个Entry数组,名为table。当我们调用ThreadLocalMap的set()方法时,其实就是更新table中某个Entry,或往table中插入一个Entry。set()方法其实是根据threadLocal对象的threadLocalHashCode来计算当前Entry节点应落入什么位置。当然存在多个entry落入位置发生冲突的情况,在ThreadLocal中使用了线性探测法来解决冲突。知道了这一点,那set方法的实现思路就很清晰了。就是找一个位置让我放节点嘛!如果已经有现成的了,更新一下value就行。要是找不到,那我就按线性探测法来找落入位置就好了嘛。值得注意的是replaceStaleEntry()方法!当entry节点不为空,而key为null时会调用这个方法。这个方法看名字好像是用来替换过时的Entry节点的?我们来看一下它到底是干嘛的?
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
这个方法好像还挺长的,上面我们已经猜测过它的用途了,现在我们就来揭开它的庐山真面目。
首先关注一个这个方法的参数:
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot)
第一个参数就是我们的key,估摸着还是用来计算hash值找位置的;第二个就是要放的value了;第三个staleSlot呢?我们上面好像是找到了一个key为null的entry节点吧?没错,这个staleSlot就是这个节点在tab中的位置了。然后从这个staleSlot节点开始往前找,如果发现某个entry不为空,但key等于null,用slotToExpunge记录下它的位置,直到往前找到一个entry为null的节点停止。这个slotToExpunge是用来干嘛的呢?后面会提到。
我们接着看。往前找完之后,我们又从staleSlot的下一个节点开始往后找,如果发现了某个节点的键值等于我们的key。我们是不是应该用我们的value替换掉这个位置原先的值呢?好像是应该替换。但是别忘记了前面还有个key为null的entry节点呢!由于之前key为null的节点和当前节点计算出来hash值其实是一样的。这里我们将e节点的值更新为最新的value后,互换tab[i]和entry的位置。这一步的目的是什么呢?我猜大概是这样的,因为ThreadLocalMap是根据线性探测法来解决冲突的,因此可能会出现key的哈希值相同但散落位置不连续的情况。为了在一定程度上提高查找哈希值相同entry节点的效率,交换一下位置会是更好的选择。同时接下来会执行cleanSomeSlots()方法。我们上面的for循环会一直往后找,直到发现一个null节点为止。如果找到了null节点,那就说明按照线性探测法找不到这个节点了啊!那咋办呢?staleSlot节点不是空着呢么。直接塞进去不就完事了。。
最后一句又调用了cleanSomeSlots()方法。下面就轮到它了。。
privateintexpungeStaleEntry(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; }
在看cleanSomeSlots()方法前,还得看expungeStaleEntry()。有时候不得不说,好的java命名规范,真的是很重要啊!看到这个方法的名字就知道它大概是用来清理过期节点的。回想一下,有什么节点是需要我们的清理的吗?好像有。。前面是不是有找到过key为null的entry节点啊?这个key为null的节点好像没啥用啊!
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
还记得这个slotToExpunge吗?不记得了的话往前翻一翻。这个slotToExpunge位置指向了一个key为null的entry节点。既然知道这个节点是没用的,那它就应该被回收。这里就很直接粗暴了,直接把它直接null以待后面垃圾回收器清理。清理完之后,又是一个for循环。如果key为null,将该entry置为null。如果不为null,重新计算一下hash值,如果位置与当前位置不同,需要重新找一个位置放该节点。当然也是利用线性探测法了,找到连续位置后面第一个为null的节点放置。
最后返回的节点为从slotToExpunge往后的第一个值为null的entry节点。
再看看cleanSomeSlots方法主体。
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }
这个方法顾名思义是用来清理某些节点的。清理啥节点呢?还是那些不为null但是key为null的节点。参数n决定了for循环要执行的次数。>>>在java中是无符号位移的意思,也就是说如果每次循环tab[i]均不需要清理,最多会执行logn次。如果有需要清理的节点,就会调用expungeStaleEntry()方法去回收这个节点。
上面说了这么一大堆,终于把ThreadLocalMap的set()方法说完了。下面接着来看getEntry()方法。
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); }
getEntry()方法比较简单。先根据key值计算出对应在table中的位置,如果table[i]的key值和我们的参数key相同,直接返回table[i];反之,调用getEntryAfterMiss()方法。
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
进入到这个方法的情况可能如下三种:
getEntry()方法会被ThreadLocal的get()方法调用,我们会在稍后的ThreadLocal源码的讲解中再谈。
介绍ThreadLocalMap用了不少的篇幅啊!下面就来看看我们ThreadLocal啦!关于ThreadLocal的方法网上已经有太多太多的文章介绍了。不过这里我们还是简单的结合我们上面所说的ThreadLocalMap来总结一下!
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
当我们调用ThreadLocal对象的set方法时,程序会获取当前线程,并将其作为作为参数传递给getMap()方法。
Thread.java ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocal.java ThreadLocal.ThreadLocalMap threadLocals = null; ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
查看Thread类源码可以发现,Thread类中包含了一个ThreadLocalMap对象,这个家伙我们上面已经花了很大的篇幅来说了,简单的说它的key为ThreadLocal的弱引用,而value为待保存的对象。至于为什么是弱引用,大家自己去google下。
继续说getMap()方法。getMap()方法很简单:返回当前线程中的ThreadLocalMap对象。
如果ThreadLocalMap对象为null,则调用createMap()方法。
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
createMap()方法在这里不细说。它会初始化当前线程的ThreadLocalMap对象,并将当前需要保存的对象放入ThreadLocalMap中,key值为当前线程的弱引用对象。
小结一下:ThreadLocal的set方法会获取当前线程的ThreadLocalMap对象,如果TreadLocalMap对象不为空,则将当前线程的弱引用作用key,待保存对象作为value保存起来;若ThreadLocalMap对象为null,则会先初始化,再放入键值对。
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); }
知道了怎么放,接下来聊一聊怎么取。眼尖的朋友们肯定已经发现了,这里又出现了ThreadLocalMap对象。那是自然,因为我们上面不就是往ThreadLocalMap里面放的吗!还记得ThreadLocalMap里面存了啥不?不记得的往上翻一翻。
如果ThreadLocalMap对象不为空,当前线程作为key值,从ThreadLocalMap中取出来了一个ThreadLocalMap.Entry对象。这个getEntry()方法我们在上面已经已经介绍过了,可能再返回去看看。当然了,肯定有人要问!我们刚才放的时候放的明明不是ThreadLocalMap.Entry对象!这咋回事呢?
ThreadLocalMap.java static class Entry extends WeakReference<ThreadLocal> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal k, Object v) { super(k); value = v; } }
实际上,在ThreadLocalMap中有一个静态类,它名叫Entry,继承了WeakReference类。再看看Entry的构造方法。如果调用Entry的get方法,实际上拿���的是ThreadLocal对象的弱引用对象。是不是很熟悉?上面的set方法有聊到过。
继续说上面的get()方法。当我们拿到了Entry对象后,如果Entry对象不为空,直接返回Entry对象的value值,即我们想要的值。
那么如果ThreadLocalMap为空呢?则会执行setInitialValue()方法。光看名字,你肯定觉得它无非执行了两步操作:1.初始化对象;2. 将初始化后的对象塞入ThreadLocalMap对象中;3. 返回初始化后的对象。那我们来看看我们的猜想对不对呢?
private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; }
除了第一句代码,后面的是不是好像都在哪里见过啊?可不是!不就是上面的set()方法吗?这个方法我们只需要关注initialValue()方法...而这个initialValue()方法是需要我们自己重写的。
小结一下:如果ThreadLocalMap中有ThreadLocal对应的值,取Entry对象的value值;如果ThreadLocalMap为null,三步走!初始化,将初始化后的对象放入ThreadLocalMap中,返回初始化后的对象。
很感谢您能耐心的看到这里,我们最后再总结一下ThreadLoacl的实现机制。
在Thread类中存在一个ThreaLocalMap变量,ThreadLocalMap中又有一个Entry类型的数组,而这个Entry对象则以ThreadLocal的弱引用为key。当我们调用ThreadLocal的get()方法时,会先获取当前线程的ThreadLocalMap对象,并将当前ThreadLocal对象作为key(实际上key为ThreadLocal的弱引用),去它的Entry数组中寻找我们需要的value。就这是我们说ThreadLocal为每个线程创建了一个变量副本的意思,线程对自己ThreadLocalMap中的值进行操作时,并不会对其它线程造成影响。