zoukankan      html  css  js  c++  java
  • ThreadLocal源码解读

    一.ThreadLoacl的理解:

    官方的讲:

    ThreadLocal是一个本地线程副本变量工具类,主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰

    通俗的讲:

    ThreadLocal也叫做线程本地变量,ThreadLoacl为变量在每个线程中的都创建了副本,每个线程可以访问自己内部的副本变量,线程之间互不影响

     

    二.TreadLocal的原理:

    从上图我们可以初步窥见ThreadLocal的核心机制:

    1)每个Thread线程内部都有一个Map

    2)Map里面储存线程本地对象key和线程的变量副本value

    3)Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向Map获取和设置线程的变量值

    这样对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,这样就形成了副本隔离,互不干扰

     

    三.ThreadLocal的底层源码

    ThreadLocal类提供了以下几个核心方法:

    1.get方法:获取当前线程的副本变量值

    2.set方法:设置当前线程的副本变量值

    3.remove方法:移除当前线程的副本变量值

    4.initilaValue方法:初始化当前线程的副本变量值,初始化null

    1)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 getMap(Thread t) {
    return t.threadLocals;
    }

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

    protected T initialValue() {
    return null;
    }

    复制代码

    源码解析:

    1.获取当前线程的ThreadLocalMap对象threadLocals(实际储存副本值的Map)

    2.Map不为空的话,从Map中获取线程储存的K-V Entry结点,然后从Entry结点中获取Value副本值返回

    3.Map为空的话,返回初始值null,之后还需向Map中添加value为null的键值对,避免空指针异常

    2.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);
    }
    

    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
    }

    void createMap(Thread t, T firstValue) {
    t.threadLocals
    = new ThreadLocalMap(this, firstValue);
    }

    复制代码

    源码解析:

    1.获取当前线程的成员变量Map

    2.Map不为空:重新将ThreadLocal对象和Value副本放入Map中

    3.Map为空:对线程成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal对象和Value副本放入Map中

    3.ThreadLocal.remove():

    复制代码
    public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
    }
    

    ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
    }

    复制代码

    源码分析:

    直接调用了ThreadLocalMap的remove方法(后面我们还会探究ThreadLocalMap类的底层源码!,这里先放着)

    4.ThreadLocal.initialValue() :

    protected T initialValue() {
        return null;
    }

    就是直接返回null

    小结一下:我们发现ThreadLocal的底层源码都有一个ThreadLocalMap类,那么ThreadLocalMap类的底层源码又是什么样子的呢?我们一起来看看吧!

    四.ThreadLocalMap的底层源码分析

    ThreadLocalMap是ThreadLocal内部的一个Map实现,然而它没有实现任何集合的接口规范,因为它仅供ThreadLocal内部使用,数据结构采用数组+开方地址法,Entry继承WeakRefrence,是基于ThreadLocal这种特殊场景实现的Map,它的实现方式很值得我们取研究!!

    1.ThreadLocalMap中Entry的源码

    复制代码
    static class Entry extends WeakReference<ThreadLocal> {
        /** The value associated with this ThreadLocal. */
        Object value;
    
    Entry(ThreadLocal k, Object v) {
        </span><span style="color: #0000ff;">super</span><span style="color: #000000;">(k);
        value </span>=<span style="color: #000000;"> v;
    }
    

    }

    复制代码

    源码分析:

    1.Entry中key只能是ThreadLocal对象,被规定死了的

    2.Entry继承了WeakRefrence(弱引用,生存周期只能活到下次GC前),但是只有Key是弱引用,Value并不是弱引用

    ps:value既然不是弱引用,那么key在被回收之后(key=null)Value并没有被回收,如果当前线程被回收了那还好,这样value也和线程一起被回收了,要是当前线程是线程池这样的环境,线程结束没有销毁回收,那么Value永远不会被回收,当存在大量这样的value的时候,就会产生内存泄漏,那么Java 8中如何解决这个问题的呢?

    解决办法:

    这里写图片描述

    以上是ThreadLocalMap的set方法,for循环遍历整个Entry数组,遇到key=null的就会替换,这样就不存在value内存泄漏的问题了!!!

    2.ThreaLocalMap中key的HashCode计算

    ThreaLocalMap的key是ThreaLocal,它不会传统的调用ThreadLocal的hashcode方法(继承自object的hashcode),而是调用nexthashcode,源码如下:

    复制代码
    private final int threadLocalHashCode = nextHashCode();
    

    private static AtomicInteger nextHashCode = new AtomicInteger();

    //1640531527 这是一个神奇的数字,能够让hash槽位分布相当均匀
    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    复制代码

    源码分析:

    我们发现ThreaLocalMap的hashcode计算没有采用模长度的方法,没有采用拉链法,采用的是开放地址法,其槽位采用静态的AtomicInteger每次增加1640531527实现,冲突了则加1或者减1继续进行增加1640531527

    我们把这个数叫做魔数,通过这个魔数我们可以位key产生完美的槽位分配,hahs冲突的次数很少

    (据说魔数和黄金比例,斐波那契数列存在某种关系)

    3.ThreaLocalMap中set方法:

    复制代码
    private void set(ThreadLocal<?> key, Object value) {
    
    Entry[] tab </span>=<span style="color: #000000;"> table;
    </span><span style="color: #0000ff;">int</span> len =<span style="color: #000000;"> tab.length;
    </span><span style="color: #0000ff;">int</span> i = key.threadLocalHashCode &amp; (len-1); <span style="color: #008000;">//</span><span style="color: #008000;"> 用key的hashCode计算槽位
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> hash冲突时,使用开放地址法
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 因为独特和hash算法,导致hash冲突很少,一般不会走进这个for循环</span>
    <span style="color: #0000ff;">for</span> (Entry e =<span style="color: #000000;"> tab[i];
         e </span>!= <span style="color: #0000ff;">null</span><span style="color: #000000;">;
         e </span>= tab[i =<span style="color: #000000;"> nextIndex(i, len)]) {
        ThreadLocal</span>&lt;?&gt; k =<span style="color: #000000;"> e.get();
    
        </span><span style="color: #0000ff;">if</span> (k == key) { <span style="color: #008000;">//</span><span style="color: #008000;"> key 相同,则覆盖value</span>
            e.value =<span style="color: #000000;"> value; 
            </span><span style="color: #0000ff;">return</span><span style="color: #000000;">;
        }
    
        </span><span style="color: #0000ff;">if</span> (k == <span style="color: #0000ff;">null</span>) { <span style="color: #008000;">//</span><span style="color: #008000;"> key = null,说明 key 已经被回收了,进入替换方法</span>
    

    replaceStaleEntry(key, value, i);
    return;
    }
    }
    // 新增 Entry
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold) // 清除一些过期的值,并判断是否需要扩容
    rehash(); // 扩容
    }

    复制代码

    源码分析:

    1.先是计算槽位

    2.Entry数组中存在需要插入的key,直接替换即可,存在key=null,也是替换(可以避免value内存泄漏)

    3.Entry数组中不存在需要插入的key,也没有key=null,新增一个Entry,然后判断一下需不需要扩容和清除过期的值(关于扩容和清除过期值先不细讲)

    4.ThreadLocalMap中getEntry方法:

    复制代码
    private Entry getEntry(ThreadLocal<?> key) {
        int i = key.threadLocalHashCode & (table.length - 1);
        Entry e = table[i];
        if (e != null && e.get() == key) // 无hash冲突情况
            return e;
        else
            return getEntryAfterMiss(key, i, e); // 有hash冲突情况
    }
    复制代码

    源码分析:

    1.计算槽位i,判断table[i]是否有目标key,没有(hahs冲突了)则进入getEntryAfterMiss方法

    getEntryAfterMiss方法分析:

    复制代码
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
        Entry[] tab = table;
        int len = tab.length;
    
    </span><span style="color: #0000ff;">while</span> (e != <span style="color: #0000ff;">null</span><span style="color: #000000;">) {
        ThreadLocal</span>&lt;?&gt; k =<span style="color: #000000;"> e.get();
        </span><span style="color: #0000ff;">if</span> (k ==<span style="color: #000000;"> key)
            </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> e;
        </span><span style="color: #0000ff;">if</span> (k == <span style="color: #0000ff;">null</span><span style="color: #000000;">)
            expungeStaleEntry(i); </span><span style="color: #008000;">//</span><span style="color: #008000;"> 清除过期的slot</span>
        <span style="color: #0000ff;">else</span><span style="color: #000000;">
            i </span>=<span style="color: #000000;"> nextIndex(i, len);
        e </span>=<span style="color: #000000;"> tab[i];
    }
    </span><span style="color: #0000ff;">return</span> <span style="color: #0000ff;">null</span><span style="color: #000000;">;
    

    }

    复制代码

    源码分析:

    遇到hash冲突之后继续向后查找,并且会在查找路上清除过期的slot

    5.ThreadLocalMap中rehash方法:

    复制代码
    private void rehash() {
        expungeStaleEntries();
    

    // 清除过程中,size会减小,在此处重新计算是否需要扩容
    // 并没有直接使用threshold,而是用较低的threshold (约 threshold 的 3/4)提前触发resize
    if (size >= threshold - threshold / 4)
    resize();
    }

    private void expungeStaleEntries() {
    Entry[] tab
    = table;
    int len = tab.length;
    for (int j = 0; j < len; j++) {
    Entry e
    = tab[j];
    if (e != null && e.get() == null)
    expungeStaleEntry(j);
    }
    }

    复制代码

    源码分析:

    先调用expungeStaleEntries()清除所有过期的slot,然后提前触发resize(约 threshold 的 3/4的时候)

    下面看看resize():

    复制代码
    private void resize() {
        Entry[] oldTab = table;
        int oldLen = oldTab.length;
        int newLen = oldLen * 2;
        Entry[] newTab = new Entry[newLen];
        int count = 0;
    
    </span><span style="color: #0000ff;">for</span> (<span style="color: #0000ff;">int</span> j = 0; j &lt; oldLen; ++<span style="color: #000000;">j) {
        Entry e </span>=<span style="color: #000000;"> oldTab[j];
        </span><span style="color: #0000ff;">if</span> (e != <span style="color: #0000ff;">null</span><span style="color: #000000;">) {
            ThreadLocal</span>&lt;?&gt; k =<span style="color: #000000;"> e.get();
            </span><span style="color: #0000ff;">if</span> (k == <span style="color: #0000ff;">null</span><span style="color: #000000;">) {
                e.value </span>= <span style="color: #0000ff;">null</span>; <span style="color: #008000;">//</span><span style="color: #008000;"> Help the GC</span>
            } <span style="color: #0000ff;">else</span><span style="color: #000000;"> {
                </span><span style="color: #0000ff;">int</span> h = k.threadLocalHashCode &amp; (newLen - 1<span style="color: #000000;">);
                </span><span style="color: #0000ff;">while</span> (newTab[h] != <span style="color: #0000ff;">null</span><span style="color: #000000;">)
                    h </span>=<span style="color: #000000;"> nextIndex(h, newLen);
                newTab[h] </span>=<span style="color: #000000;"> e;
                count</span>++<span style="color: #000000;">;
            }
        }
    }
    
    setThreshold(newLen);
    size </span>=<span style="color: #000000;"> count;
    table </span>=<span style="color: #000000;"> newTab;
    

    }

    复制代码

    扩容2倍,同时在Entry移动过程中会清除一些过期的entry

    6.ThreadLocal中的remove方法:

    复制代码
    private void remove(ThreadLocal<?> key) {
        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)]) {
            if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }
    复制代码

    源码分析:

    遍历Entry数组寻找需要删除的ThreadLocal,建议在ThreadLocal使用完成之后再调用此方法

    现在再详细分析一下ThreadLocalMap的set方法中的几个方法:

    1.replaceStaleEntry方法:替换

    复制代码
    private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                   int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
        Entry e;
    
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 往前寻找过期的slot</span>
    <span style="color: #0000ff;">int</span> slotToExpunge =<span style="color: #000000;"> staleSlot;
    </span><span style="color: #0000ff;">for</span> (<span style="color: #0000ff;">int</span> i =<span style="color: #000000;"> prevIndex(staleSlot, len);
         (e </span>= tab[i]) != <span style="color: #0000ff;">null</span><span style="color: #000000;">;
         i </span>=<span style="color: #000000;"> prevIndex(i, len))
        </span><span style="color: #0000ff;">if</span> (e.get() == <span style="color: #0000ff;">null</span><span style="color: #000000;">)
            slotToExpunge </span>=<span style="color: #000000;"> i;
    
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 找到 key 或者 直到 遇到null 的slot 才终止循环</span>
    <span style="color: #0000ff;">for</span> (<span style="color: #0000ff;">int</span> i =<span style="color: #000000;"> nextIndex(staleSlot, len);
         (e </span>= tab[i]) != <span style="color: #0000ff;">null</span><span style="color: #000000;">;
         i </span>=<span style="color: #000000;"> nextIndex(i, len)) {
        ThreadLocal</span>&lt;?&gt; k =<span style="color: #000000;"> e.get();
    
        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 如果找到了key,那么需要将它与过期的 slot 交换来维护哈希表的顺序。
        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 然后可以将新过期的 slot 或其上面遇到的任何其他过期的 slot 
        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 给 expungeStaleEntry 以清除或 rehash 这个 run 中的所有其他entries。</span>
    
        <span style="color: #0000ff;">if</span> (k ==<span style="color: #000000;"> key) {
            e.value </span>=<span style="color: #000000;"> value;
    
            tab[i] </span>=<span style="color: #000000;"> tab[staleSlot];
            tab[staleSlot] </span>=<span style="color: #000000;"> e;
    
            </span><span style="color: #008000;">//</span><span style="color: #008000;"> 如果存在,则开始清除前面过期的entry</span>
            <span style="color: #0000ff;">if</span> (slotToExpunge ==<span style="color: #000000;"> staleSlot)
                slotToExpunge </span>=<span style="color: #000000;"> i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            </span><span style="color: #0000ff;">return</span><span style="color: #000000;">;
        }
    
        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 如果我们没有在向前扫描中找到过期的条目,
        </span><span style="color: #008000;">//</span><span style="color: #008000;"> 那么在扫描 key 时看到的第一个过期 entry 是仍然存在于 run 中的条目。</span>
        <span style="color: #0000ff;">if</span> (k == <span style="color: #0000ff;">null</span> &amp;&amp; slotToExpunge ==<span style="color: #000000;"> staleSlot)
            slotToExpunge </span>=<span style="color: #000000;"> i;
    }
    
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 如果没有找到 key,那么在 slot 中创建新entry</span>
    tab[staleSlot].value = <span style="color: #0000ff;">null</span><span style="color: #000000;">;
    tab[staleSlot] </span>= <span style="color: #0000ff;">new</span><span style="color: #000000;"> Entry(key, value);
    
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 如果还有其他过期的entries存在 run 中,则清除他们</span>
    <span style="color: #0000ff;">if</span> (slotToExpunge !=<span style="color: #000000;"> staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    

    }

    复制代码

    上文中run的意思不好翻译,理解为开放地址中一个slot中前后不为null的连续entry

    2.cleanSomeSlots方法:清除一些slot(按照规则清除“一些”slot,而不是全部)

    复制代码
    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);  // n = n / 2, 对数控制循环 
        return removed;
    }
    复制代码

    当新元素被添加时,或者另外一个过期元素已经被删除的时候,会调用该方法,该方法会试探性的扫描一些Entry寻找过期的条目,它执行对数数量的扫描,是一种基于不扫描(快速但保留垃圾)和所有元素扫描之间的平衡!!

    对数数量的扫描!!!

    这是一种折中的方案

    3.expungeStaleEntry:清除

    复制代码
    private int expungeStaleEntry(int staleSlot) {
        Entry[] tab = table;
        int len = tab.length;
    
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> 清除当前过期的slot</span>
    tab[staleSlot].value = <span style="color: #0000ff;">null</span><span style="color: #000000;">;
    tab[staleSlot] </span>= <span style="color: #0000ff;">null</span><span style="color: #000000;">;
    size</span>--<span style="color: #000000;">;
    
    </span><span style="color: #008000;">//</span><span style="color: #008000;"> Rehash 直到 null 的 slot</span>
    

    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;

                </span><span style="color: #0000ff;">while</span> (tab[h] != <span style="color: #0000ff;">null</span><span style="color: #000000;">)
                    h </span>=<span style="color: #000000;"> nextIndex(h, len);
                tab[h] </span>=<span style="color: #000000;"> e;
            }
        }
    }
    </span><span style="color: #0000ff;">return</span><span style="color: #000000;"> i;
    

    }

    复制代码

    真正的清除,不仅会清除当前过期的slot,还会继续往后查询直到遇到null的slot为止,对于查询遍历中没有被回收的情况,做了一次rehash

    推荐大佬的博客:https://www.cnblogs.com/micrari/p/6790229.html

    写的太详细了,太强了,源码注释贼多

    ---------------------------------------------------------------------------------------------

    没有下文了,这是底线…

     

  • 相关阅读:
    Mysql日志管理
    Mysql 安全和DCL语句
    Mysql DDL语句之视图
    Mysql增删改查(DML、DQL)
    Mysql操作之部分DDL语句
    如何做事情
    temp
    asp.net入门
    希望尽快回忆起来
    需求?
  • 原文地址:https://www.cnblogs.com/edda/p/12673577.html
Copyright © 2011-2022 走看看