Java线程-ThreadLocal学习(六)

一、简介

对于ThreadLocal的简介,我们先来看一下API文档:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

  ThreadLocal,被称为线程本地变量或线程私有变量,这种变量在多线程访问下的时候能够保证各个线程里的变量相对独立于其他线程内的变量,这样使得线程只有自己能访问和修改对应的变量,从而可以实现多个线程间变量的隔离,从而实现变量的线程安全。通常情况下,ThreadLocal变量类型都是private static类型的,用于关联线程和线程的上下文。

参考别人总结的一句话:

ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

二、ThreadLocal
1. 实例

我们先看一个简单的例子:

public class ThreadTest {
    public static class MyRunnable extends Thread {
        private static Integer random;

        @Override
        public void run() {
            random = (int) (Math.random() * 1000);
            System.out.println("线程" + Thread.currentThread().getName() + "获取random:" + random);
        }
    }

    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}

由于我们没有进行相应的线程同步的操作,所以多线程直接操作的话,很大程序上会出现第二个线程的值覆盖掉第一个线程的值,最终两个值是相同的。

2. ThreadLocal源码

我们会来简单看下ThreadLocal的源码,ThreadLocal提供的public的方法不多:

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier)
public T get()
public void set(T value)
public void remove()
  • get方法是用来获取当前线程在ThreadLocal中保存的变量的副本;
  • set方法是将当前线程对应的变量保存一份到ThreadLocal中;
  • remove方法用于移除当前线程在ThreadLocal中的副本;
  • withInitial方法用于初始化一个默认值的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) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

这里我们可以看到get方法所操作的其实是一个ThreadLocalMap,我们来简单看下这个Map的结构:

static class ThreadLocalMap {

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
}

  可以看到,我们最终保存,获取线程本地变量所操作的对象其实是WeakReference,也就是弱引用,弱引用我们前面已经了解过,这里使用弱引用,是为了更好的垃圾回收,因为弱引用的生命周期是两次GC之间。其实ThreadLocalMap的定义和WeakHashMap有点相似,感兴趣的可以查看下以前学习WeakHashMap时的内容:Java1.8-WeakHashMap源码解析

我们先顺着源码来看下get方法的流程,首先获取当前线程,然后把当前线程对象作为key获取ThreadLocalMap:

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

这里获取到的ThreadLocalMap其实是Thread的内部变量,每个线程内部维护了一个ThreadLocalMap,用于存储与此线程相关的ThreadLocal值:

/* ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;

然后获取该线程对应的ThreadLocalMap中的Entry,如果不为空,获取Entry的value属性的值;

  • 如果map为空,或者map中对应的Entry为空,则调用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;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

到这里我们来简单总结下ThreadLocal是如何为每个线程创建变量副本的:

  1. 首先,每个线程内部都有一个变量 threadLocals,该变量就是用来保存相应的变量副本的,key为ThreadLocal对象,value是相应的变量值;
  2. 初始化该变量的的时候,无论是通过get或者set方法都会进行相应的判断,如果为空的话,进行相应的初始化;
  3. get方法中的通过ThreadLocalMap获取对应的Entry,是根据ThreadLocal作为参数,这点也可以看出Thread中的变量threadLocals的存储方式;

到这里,get方法就介绍完成了,至于set,remove方法的实现其实和get方法类似,就不多说了,贴上源码简单了解下:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

另外,针对最开始的随机数问题,我们可以修改为如下代码:

public static class MyRunnable extends Thread {
    private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    @Override
    public void run() {
        threadLocal.set( (int) (Math.random() * 100D) );
        System.out.println("线程" + Thread.currentThread().getName() + "获取random:" + threadLocal.get());
    }
}
3. InheritableThreadLocal

  InheritableThreadLocal 是ThreadLocal的子类,由于ThreadLocal内部每个线程都只能访问到自己的私有变量值,而该继承类的作用是允许一个线程创建的所有子线程可以访问其父线程的值。我们来简单看下两个例子即可,代码来自:java并发编程学习: ThreadLocal使用及原理

public class ThreadTest {
    public static class MyRunnable implements Runnable {
        private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

        public MyRunnable(){
            threadLocal.set((int) (Math.random() * 100D));
            System.out.println("构造方法:" + Thread.currentThread().getName() + ":" + threadLocal.get());
        }

        @Override
        public void run() {
            System.out.println("run方法:" + Thread.currentThread().getName() + ":" + threadLocal.get());
        }
    }

    public static void main(String[] args) {
        new Thread(new MyRunnable(), "A").start();
        new Thread(new MyRunnable(), "B").start();
    }
}

先看下打印结果:

构造方法:main:92
构造方法:main:43
run方法:A:null
run方法:B:null

  这里,我们把ThreadLocal赋值的地方放到了构造方法中,然后在run方法中获取该值,但却没有获取到。

这是因为new MyRunnable构造方法是由main线程调用的,所以ThreadLocal的set方法,实际上是对main线程进行操作的,因此也只能在main线程中进行获取,而run方法的上下文环境是子线程,所以自然获取不到。接下来我们将ThreadLocal修改为InheritableThreadLocal,然后再来看一下结果:

构造方法:main:53
构造方法:main:29
run方法:A:53
run方法:B:29

可以看到,在run方法中调用获取到了父线程的值。InheritableThreadLocal的实现和ThreadLocal类似,在Thread类中也保存了一个该类型的变量:

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
三、ThreadLocal的内存泄漏问题
1. 为什么会发生内存泄漏?

  由于ThreadLocalMap的key使用的是弱引用类型,由于弱引用的生命周期比较短,存在于两次GC之间的这段时间,那么如果key在GC的时候被回收了,那么就会导致value永远不会被调用到,但如果对应的线程一直不结束,那value就一直存在,这样的话,就会有可能出现内存泄漏的情况。

2. 如何避免内存泄漏

JDK的开发者为了解决这些潜在的问题,其实已经做了一些改进,在ThreadLocal的get,set,remove方法都有相应的处理。首先,在ThreadLocalMap的定义上,有如下注释:

/**
 * The entries in this hash map extend WeakReference, using
 * its main ref field as the key (which is always a
 * ThreadLocal object).  Note that null keys (i.e. entry.get()
 * == null) mean that the key is no longer referenced, so the
 * entry can be expunged from table.  Such entries are referred to
 * as "stale entries" in the code that follows.
 */

  意思是说,当该map中的key==null的时候,就可以认为这个值无效了,这个无效的key被称为stale entry,就可以调用expunged进行删除,而expunged所对应的方法expungeStaleEntry方法在set,get及remove方法都有直接或间接的调用,所以有的文章中说,线程调用完成之后,调用remove方法来避免内存泄露,因为remove方法直接调用了该方法来删除相应的value。

有关清除value源码方面的解析可以参考这篇文章:从源码角度深入详解ThreadLocal内存泄漏问题

3. 总结
  1. 首先我们要明白,即使是key被回收了value还存在,是不一定会发生内存泄漏的。首先,一般情况下,程序都会调用expungeStaleEntry方法来进行清理,然后,即使不调用该方法清理,线程一旦消亡了,那对应的ThreadLocal,ThreadLocalMap及对应的entry 自然也会被GC给回收掉了。
  2. 当然如果线程一直不消亡,还是会有内存泄漏的可能的,而常用的线程一直不结束的场景,就是线程池了,因为这种情况下,线程是一种重复利用的,有可能会造成value一直累加的情况,具体的模拟可以参考: 深入理解ThreadLocal的"内存溢出"
  3. 综上,线程会发生内存泄漏的情况一般是使用线程之后,我们没有进行及时清理;另一种情况就是线程池的使用;所以,无论哪种情况,在使用完成之后一定要记得及时清理;
  4. JDK建议将ThreadLocal变量定义为private static的,这样的话ThreadLocal的生命周期就会变长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,这样的话能保证任何时候都可以根据ThreadLocal的弱引用访问到Entry的value值,然后remove掉,防止内存泄漏。
四、应用场景

在常用的框架中,有许多使用到ThreadLocal的地方,比如Spring的事务:

public abstract class TransactionSynchronizationManager {

    private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<Map<Object, Object>>("Transactional resources");
    //事务注册的事务同步器
    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
            new NamedThreadLocal<Set<TransactionSynchronization>>("Transaction synchronizations");
    //事务名称
    private static final ThreadLocal<String> currentTransactionName =
            new NamedThreadLocal<String>("Current transaction name");
    //事务只读属性
    private static final ThreadLocal<Boolean> currentTransactionReadOnly =
            new NamedThreadLocal<Boolean>("Current transaction read-only status");
    //事务隔离级别
    private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
            new NamedThreadLocal<Integer>("Current transaction isolation level");
    //事务同步开启
    private static final ThreadLocal<Boolean> actualTransactionActive =
            new NamedThreadLocal<Boolean>("Actual transaction active");
}
  1. 我们可以使用ThreadLocal来记录日志,比如我们想记录下多线程下程序的日志记录,以方便错误情况下更容易的查到与某个特定线程相关的记录。
  2. 我们还可以通过ThreadLocal,然后借助拦截器来记录接口的调用时间;
五、总结
  1. ThreadLocal的实现其实并不是特别复杂,首先,ThreadLocal的内部定义了一个ThreadLocalMap内部类,用于变量的存储。而每个线程内部则有一个ThreadLocalMap的变量,该变量的key是ThreadLocal,value是对应的变量,这样就实现了两个线程同时访问一个ThreadLocal变量,但两个线程所读到的变量都是各自线程独有的。也就是说ThreadLocal本身并不存储,它只是作为一个key来让线程从ThreadLocalMap来获取值。
  2. ThreadLocalMap使用弱引用来作为key,如果ThreadLocal没有外部引用来引用它,那么GC的时候就会被回收掉;不过使用ThreadLocal存在内存泄漏的风险,所以每次使用完成之后,尽量都调用它的remove方法,来清除数据;

本文参考自:
海子-Java并发编程:深入剖析ThreadLocal
关于ThreadLocal内存泄露的备忘
详细介绍并发容器之ThreadLocal
知乎-深入剖析ThreadLocal
《并发编程实战》

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

推荐阅读更多精彩内容