死磕Java源码之ThreadLocal实现分析

死磕Java源码之ThreadLocal实现分析

通俗的讲, ThreadLocal是Java里一种特殊的变量。每个线程都有一个ThreadLocalMap,用来存放ThreadLocal变量表,当然这里不是直接通过Map的方式存储,而是通过一个table和Entry结构存储

因为ThreadLocalMap变量是跟线程绑定的,所以不存在多线程共享变量之间的并发问题,所以ThreadLocal也就是线程安全的变量。

屏幕快照 2018-09-06 上午11.20.55.png

具体的结构,在源码部分说明

ThreadLocal的使用

ThreadLocal主要有以下几个方法:

public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) { } //set()用来设置当前线程中变量的副本
public void remove() { } //remove()用来移除当前线程中变量的副本
protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的

写一个demo,在main线程和新建线程中,对同一个ThreadLocal变量进行修改,看下修改后的结果:

public class ThreadLocalDemo {

    public static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {


        ThreadLocalDemo.threadLocal.set("hello world main");
        System.out.println(ThreadLocalDemo.threadLocal.get());


        try {
            Thread thread = new Thread() {
                public void run() {
                    ThreadLocalDemo.threadLocal.set("hello world thread");
                    System.out.println(ThreadLocalDemo.threadLocal.get());
                };
            };
            thread.start();
            thread.join();
        } catch (Exception ex) {
            System.out.println(ex);
        }

        System.out.println(ThreadLocalDemo.threadLocal.get());

    }

}

执行输出:

hello world main
hello world thread
hello world main

不难看出,我们在new Thread()中对ThreadLocal的变量threadLocal进行修改后,在main线程中再次输出,其值并没有收到影响,他们修改的分别是各自的副本,不会对其他副本有影响。

当然这里完整的逻辑是应该在使用完调用remove方法删除threadLocal副本,以防内存泄露。

具体原理见下文

ThreadLocal的内存泄漏与源码分析

ThreadLocal的结构如图:

image

ThreadLocal为什么会内存泄漏

每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 keyThreadLocal实例本身,value 是真正需要存储的 Object

也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现keynullEntry,就没有办法访问这些keynullEntryvalue,如果当前线程再迟迟不结束的话,这些keynullEntryvalue就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocalget(),set(),remove()的时候都会清除线程ThreadLocalMap里所有keynullvalue,当然这只是一种防护措施,最好的使用方法是在用完了ThreadLocal变量时,调用remove()方法主动将ThreadLocal和value释放。

关于为什么ThreadLocalMap使用ThreadLocal的弱引用,这就跟弱引用的机制有关,若引用的对象在JVM执行GC的时候就会被回收掉。通过gc前后查看table中对应entry对象的referent即可查看是否被回收(示例的前提是ThreadLocal的强引用对象已经释放):

GC之前:

![屏幕快照 2018-09-05 下午5.03.21.png](https://upload-images.jianshu.io/upload_images/1272254-4a2814486c210166.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

GC之后:

此时Entry的referent=null,当再次通过调用get、set、remove方法是,ThreadLocal会有各自的机制,将Map中key(referent)为空的Entry移除,并释放其中的value,一定成都避免了内存泄漏,此机制源码分析阶段说明。

ThreadLocal源码分析

ThreadLocal中的关键属性

//创建ThreadLocal时,复制的HashCode
//HashCode是在全局静态的nextHashCode基础上增加一个HASH_INCREMENT而来
private final int threadLocalHashCode = nextHashCode();

下面分析几个具体的函数:

  • get方法的实现
/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

首先是当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。如果获取成功,则返回value值。如果map为空,则调用setInitialValue方法返回value。

  • setInitialValue方法的实现
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;
}
/**
* 构造ThreadLocalMap
**/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

首先是通过调用initialValueinitialValue是protected方法,初始化ThreadLocal时可以重写此函数,相当于延迟加载,然后通过getMap创建threadLocals,如果threadLocals不存在时,会调用createMap创建一个初始大小为16的Entry数组table,并新建一个Entry存入table中。这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)

这里重点看下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类是集成自WeakReference,然后使用ThreadLocal作为了键,也就是说这里的ThreadLocal是一个弱引用在GC的时候会被回收。

接上文,如果map存在,则会调用map的getEntry方法,getEntry方法实现:

private Entry getEntry(ThreadLocal<?> key) {
    //通过hash算出数组下标
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        //如果取出Entry,并且e.get也就是referent与threadLocal相同,则说明是需要的值,返回Entry对象e ,判断e.get() = key 是解决hash碰撞的情况
        return e;
    else
        //如果下标i的Entry不存在或者 其threadLocal不相同,则执行此
        return getEntryAfterMiss(key, i, e);
}


private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
    
    while (e != null) {
        //说明有此entry,可能是hash碰撞的结果
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            //处理已无引用的ThreadLocal变量等,解决内存泄漏的机制之一
            expungeStaleEntry(i);
        else
            //下标+1 
            i = nextIndex(i, len);
        e = tab[i];
    }
    //如果getEntry中获取的entry=null,则说明无此ThreadLocal变量,返回null
    return null;
}

expungeStaleEntry 方法

//删除可以释放的Entry
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) {
            //如果发现ThreadLocal已经被释放掉,则通过这里来释放value的引用,以及删除数组table中的Entry
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                //重新设置Entry在table中的位置
                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;
}

通过对get方法的大致分析,可以分为几个阶段:

1)判断Map是否存在,如果不存在初始化Map以及table等

2)如果已存在,并且获取到Entry,则返回

3)如果不存在,则调用expungeStaleEntry清除需要释放的ThreadLocal、释放对value的一用,从table中删除相应下标的Entry,以及重新设置元素在table中的位置

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

set方法中的createMap与上文的createMap相同,不在做说明,重点看下map.set(this, value); 这里直接在温中锋

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) {
           //如果通过hash计算的下标取出的entry的key与设置的相同,则更新value
            e.value = value;
            return;
        }

        if (k == null) {
            //和HashMap不一样,由于Entry key继承了软引用,会出现k是null的情况!所以会接着在replaceStaleEntry重新循环寻找相同的key
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    //如果key!= null  并且 k != key 说明存在hash鹏铮
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        //调用cleanSomeSlots()对table进行清理,如果没有任何Entry被清理,并且表的size超过了阈值,就会调用rehash()方
        rehash();
}

hash散列的键值数据在存储过程中可能会发生碰撞,大家知道HashMap存储的是一个Entry链,当hash发生冲突后,将新的Entry存放在链表最前端。但是ThreadLocalMap不一样,采用index+1作为重散列的hash值写入。另外有一点需要注意key出现null的原因是由于Entry的key是继承了软引用,在下一次GC时不管它有没有被引用都会被回收掉而Value没有被回收。当出现null时,会调用replaceStaleEntry()方法接着循环寻找相同的key,如果存在,直接替换旧值。如果不存在,则在当前位置上重新创建新的Entry.

  • remove方法的实现

  • //ThreadLocal
    public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }
    /**
     * ThreadeLocalMap
     * Remove the entry for key.
     */
    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;
            }
        }
    }
    

    remove方法相对简单,通过hashcode计算出下标,然后判断key与要删除的ThreadLocal是否一致,如果一致,释放掉相应的引用,并调用expungeStaleEntry方法清理其他的可以释放的对象。

ThreadLocal的使用场景

  • 每个线程自己独享的数据,比如session数据
  • 实例需要在多个方法中共享,但不希望被多线程共享

比如在Dubbo中的RpcContext实例,在RpcContext.java文件中,通过静态的ThreadLocal变量,为每个线程持有一个RpcContext对象,这个RpcContext对象只有在此线程的不同方法中共享使用,在多线程中不会共享,是一种典型的应用,包括重写了initialValue方法

rivate static final ThreadLocal<RpcContext> LOCAL = new ThreadLocal<RpcContext>() {
   @Override
   /**
   * 重新initialValue方法,当get时为null时,通过回调此方法获取RpcContext实例
   **/
   protected RpcContext initialValue() {
      return new RpcContext();
   }
};

/**
 * get context.
 * 
 * @return context
 */
public static RpcContext getContext() {
    return LOCAL.get();
}

/**
 * remove context.
 * 
 * @see com.alibaba.dubbo.rpc.filter.ContextFilter
 */
public static void removeContext() {
    LOCAL.remove();
}

总结

  • ThreadLocal 并不解决线程间共享数据的问题,通过使用ThreadLocal是使数据在不同线程有不同的副本,不会有多线程共享数据也就不需要解决共享数据的问题
  • 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMapEntryThreadLocal 的引用为弱引用,避免了因ThreadLocalMap强引用 ThreadLocal 对象在线程回收之前无法被回收的问题
  • ThreadLocalMap 的 set 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
  • ThreadLocalMap 的 get 方法通过调用 expungeStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
  • ThreadLocalMap当hash发生冲突后,并不是与HashMap一样采用的Entry链表将新的Entry存放在链表最前端。而是采用index+1作为重散列的hash值来重新存储Entry值

文章部分自己理解,部分借鉴了大牛们的文章,在此表示感谢!如有bug,劳烦指正!

参考:

https://blog.csdn.net/liulongling/article/details/50607802

http://www.importnew.com/21206.html

http://www.jasongj.com/java/threadlocal/

http://www.importnew.com/22039.html

<https://blog.csdn.net/liulongling/article/details/50607802

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容