• Java并发编程之ThreadLocal源码分析


    1 一句话概括ThreadLocal

      什么是ThreadLocal?顾名思义:线程本地变量,它为每个使用该对象的线程创建了一个独立的变量副本。

    2 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有着举足轻重的作用!

    3 ThreadLocalMap源码分析

      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是否会引发内存泄漏,这里暂时不做探讨。

    3.1 ThreadLocalMap.set()方法

    private void set(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()方法。下面就轮到它了。。

    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;
    }
    

      在看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()方法。

    3.2 ThreadLocalMap.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;
    }
    

      进入到这个方法的情况可能如下三种:

    1. entry节点为null
      如果entry节点为null,直接返回null。
    2. entry节点不为null,但是key为null
      执行expungeStaleEntry()方法回收该节点,回收完之后,tab[i]节点也就变成了null,直接返回null。
    3. entry节点不为null,entry节点的key和参数key不同
      从i节点开始往后找,如果有key值相同的节点,也就是我们找到了我们需要的节点,返回entry即可。如果找不到,从i节点往后找,遇到key为null的回收一下该节点后返回null,遇到entry为null的直接返回null;

      getEntry()方法会被ThreadLocal的get()方法调用,我们会在稍后的ThreadLocal源码的讲解中再谈。
      介绍ThreadLocalMap用了不少的篇幅啊!下面就来看看我们ThreadLocal啦!关于ThreadLocal的方法网上已经有太多太多的文章介绍了。不过这里我们还是简单的结合我们上面所说的ThreadLocalMap来总结一下!

    4 ThreadLocal源码分析

    4.1 ThreadLocal.Set()方法

    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对象后,如果它不为空。则往该对象里面塞入一个键值对,key为ThreadLocal对象的弱引用,vaule为需要保存的对象。
    • 如果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,则会先初始化,再放入键值对。

    4.2 ThreadLocal.Get()方法

    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中,返回初始化后的对象。

    5 ThreadLocal总结

      很感谢您能耐心的看到这里,我们最后再总结一下ThreadLoacl的实现机制。
      在Thread类中存在一个ThreaLocalMap变量,ThreadLocalMap中又有一个Entry类型的数组,而这个Entry对象则以ThreadLocal的弱引用为key。当我们调用ThreadLocal的get()方法时,会先获取当前线程的ThreadLocalMap对象,并将当前ThreadLocal对象作为key(实际上key为ThreadLocal的弱引用),去它的Entry数组中寻找我们需要的value。就这是我们说ThreadLocal为每个线程创建了一个变量副本的意思,线程对自己ThreadLocalMap中的值进行操作时,并不会对其它线程造成影响。

  • 相关阅读:
    windows安装nacos
    anki处理
    minikube安装net5
    在.net core中使用属性注入
    C# 使用MD5算法对密码进行加密
    c# 获取本机系统已经安装的打印机信息
    C# 比较两个datatable并找出修改差异的值
    打印机点击打印后无反应
    GUID转换成16位字符串或19位数据(确保唯一)
    RESTful
  • 原文地址:https://www.cnblogs.com/cfyrwang/p/8166369.html
Copyright © 2020-2023  润新知