java并发之ThreadLocal

java并发之ThreadLocal

知识导读

  • ThreadLocal主要作用于线程的上下文,而不是线程安全,如果ThreadLocal中放一个共享对象,是无法保证线程安全的,如果是基本类型可以保证线程安全
  • ThreadLocal是对当前线程的threadLocals(Map)变量的一种封装管理。提供了该map的get和set方法,在当前线程执行上下文中可以随时获取该值。
  • ThreadLocal实例就是一个Map的key,每个线程都有一个私有的Map,在使用ThreadLocal时要有这个概念,使用起来就方便了
  • ThreadLocalMap的Entry的key值是ThreadLocal实例,是一个弱引用。value是实际set的值,是一个强引用
  • 线程不消亡时(线程池),value值可能会导致内存泄露,良好的编程习惯是在finnaly代码块中调用ThreadLocal.remove()
  • ThreadLocalMap的set方法,当发生hash冲突的时候,会尝试将值放到当前槽位的下一个槽位。
  • ThreadLocalMap的get方法,先根据key的hash计算槽位,然后比对槽位上的key值,如果key不同会尝试下个槽位,该该过程中清除由于gc导致key为null的entry,直到找到key值相同的返回,否则返回null
  • InheritableThreadLocal用于向子线程中传递父线程的InheritableThreadLocal存储的值,是一个浅拷贝。

原理

用途

  • 保存线程上下文信息,在线程某个地方设置,在随后的任意地方都可以获取

  • 线程私有,以空间换时间

    注意:如果线程ThreadLocal中保存的是一个引用类型的共享对象,当修改共享对象内部值时会出现并发安全问题

内部实现

java的Thread类有两个私有变量threadLocals和inheritableThreadLocals,类型是ThreadLocal.ThreadLocalMap,用于保存线程运行上下文的值,map的key是ThreadLocal对象,value是设置的值。

  • 向ThreadLocal中设置值就是向这个线程私有的map中设置值,key为ThreadLocal实例,value为要设置的值
  • 从ThreadLocal中获取值,就是以ThreadLocal实例为key,从这个线程私有的map中get值
  • map中的key是弱引用,value是强引用

注意:这里的map内部结构是数组,一个槽位只存储一个元素,而不是HashMap中的数组+链表。

内存泄露

在使用不当的情况下,ThreadLocal会导致内存泄露问题。

下图展示了ThreadLocal的引用情况,java堆中有ThreadLocal实例、value实例、Map实例,可能发生内存泄露的就是这三个实例

image
  • ThreadLocal实例由栈中变量指针强引用、线程的私有变量map的key弱引用
  • value实例可能由栈中变量指针强引用、线程的私有变量map的value强引用
  • 线程私有变量map由栈中的线程对象指针强引用

一般为了防止内存泄露,会将栈中变量指针设置null,但是如果线程不消亡,会存在线程到map的引用,从而导致ThreadLocal实例和value实例还会存在引用,无法被回收,从而导致内存泄露。

ThreadLocal针对这种问题作出了部分优化

  1. 在线程私有变量map中key引用是弱引用类型,即当外部没有任何强引用指向ThreadLocal实例时,垃圾回收会回收ThreadLocal实例。
  2. 每次调用ThreadLocal的get和set的时候,如果查询不到值,会将map中所有key为null的值移除,从而释放value。

综上:

  • 如果线程能够消亡,线程到map的强引用断开,map到ThreadLocal实例和value实例的引用都会消失。将栈中各个实例的引用显示设置为null,ThreadLocal和value实例都可以被gc回收,不会发生内存泄露问题

  • 如果线程不消亡,并且每次调用ThreadLocal的get能获取到值或者以后再不调用ThreadLocal的get和set方法,会导致内存泄露,

    • value实例一直被Thread中map的value强引用,无法被垃圾回收,发生内存泄露
    • ThreadLocal实例被Thread中map的key弱引用,可以被gc回收

什么情况下线程不消亡呢?

  • 一个永久存活的线程,一直在循环执行
  • 线程池中的线程,当设置了core线程不回收,线程池中的某些线程有可能一直存活

如何解决

良好的使用习惯是,当确定线程的后续执行流程不需要再用到ThreadLocal中的值、线程执行完毕前,显示调用ThreadLocal的remove方法清理内存中的ThreadLocal实例和value实例。一般都在finnaly代码块中调用 ThreadLocal.remove()

源码解析

Thread类

Thread类中定义了threadLocals和inheritableThreadLocals变量,ThreadLocal.ThreadLocalMap类型。threadLocals变量的管理是在ThreadLocal中实现的。

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocal类

Thread中只是定义threadLocals变量,并没有提供该变量的管理逻辑,ThreadLocal封装了对当前线程threadLocals变量的管理操作。

ThreadLocal中定义了ThreadLocalMap类型,map中的entry使用数组存储,一个槽位只存储一个entry,槽位由hash计算所得,当发生hash冲突时,会判断下一个槽位是否可以放

Entry的key为ThreadLocal类型,是个弱引用。value是调用ThreadLocal的set方法设置的值,是个强引用。

static class ThreadLocalMap {
        
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);//key是弱引用
            value = v;//value是强引用
        }
    }
    //数组 槽位
    private Entry[] table;
      //长度
    private int size = 0;
}

set值

ThreadLocal的set方法,获取当前线程的threadLocals变量

  • 如果不为空,则以当前ThreadLocal实例为key,存储值
  • 如果为空,初始化ThreadLocalMap实例,再向里面添加值
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);//获取当前线程的threadLocals
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
//如果没有map,则创建map,并存储变量
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set值的时候,如果发生hash冲突,会尝试当前槽位的下一个槽位

  • key值为空,清理gc导致的key为null的entry,然后将新值添加到该槽位
  • key值相同,更新操作,直接覆盖
  • key值不同,继续尝试下一个槽位
private void set(ThreadLocal<?> key, Object value) {
    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) {//清除value值,并将新值放到该槽位
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

get值

ThreadLocal的get方法,先获取当前线程的ThreadLocalMap,然后以调用者threadLocal为key获取变量

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);//t.threadLocals
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
//获取当前线程threadLocals变量的值
ThreadLocalMap getMap(Thread t) {
  return t.threadLocals;
}

从map中查询就是通过key的hash值计算出所在槽位,然后获取该槽位的值比对,如果查询不到再遍历所有的key比对

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

根据key查询不到值的时候,会触发遍历map中所有的key,如果遇到gc导致key为空的entry移除,直到找到值或者返回null

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); //清除由于gc导致key为空的entry
        else
            i = nextIndex(i, len);//当前槽位的下一个槽位
        e = tab[i];
    }
    return null;
}

查询不到值的时候,会调用setInitialValue设置初始值,子类可以覆写initialValue提供为空时的默认值

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

remove值

ThreadLocal的remove方法,移除以当前ThreadLocal实例为key的值

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

InheritableThreadLocal

InheritableThreadLocal继承ThreadLocal,覆写了getMap方法,ThreadLocal的getMap方法返回的是当前线程的threadLocals。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
   //为子线程传递变量
    protected T childValue(T parentValue) {
        return parentValue;
    }
    //覆写 getMap 返回 inheritableThreadLocals
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    //覆写 createMap 创建ThreadLocalMap并为inheritableThreadLocals赋值
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

Thread类中定义了inheritableThreadLocals遍历,用于接收父线程给子线程传递的运行上下文信息

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

线程的父线程是当前创建子线程的线程,新创建的子线程的构造方法中会调用init方法,复制当前线程(父线程)的inheritableThreadLocals,将值保存到子线程的inheritableThreadLocals变量中,从而实现继承的效果

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc) {
  //...
  Thread parent = currentThread();
  //...
    if (parent.inheritableThreadLocals != null)
      this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  //...
}

ThreadLocal中对父线程中的inheritableThreadLocals属性进行了浅拷贝,key和value都是原来的引用地址,这样子线程通过InheritableThreadLocal的get方法就能获取到父线程中定义的引用,通过引用访问变量

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
  Entry[] parentTable = parentMap.table;
  int len = parentTable.length;
  setThreshold(len);
  table = new Entry[len];

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

推荐阅读更多精彩内容