线程封闭与ThreadLocal

线程封闭与ThreadLocal

多线程访问共享可变数据时,涉及到线程间数据同步问题。然而,并不是所有时候都需要共享数据,所以,线程封闭的概念就提出来了。

通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。

线程封闭的具体体现有:

  • 局部变量
  • ThreadLocal

局部变量

局部变量位于执行线程的栈中,其他线程无法访问这个栈。线程封闭是局部变量的固有属性。

ThreadLocal

java.lang.ThreadLocal,顾名思义,它可以存放线程本地变量。ThreadLocal让每个线程维护变量的一个副本,各线程通过ThreadLocal去访问该变量时会拿到各自的副本,副本之间相互独立,互不影响,这样竞争条件被彻底消除了。

使用示例

下面通过一个例子来验证ThreadLocal的特性。

public class ThreadLocalTest {

    private static final ThreadLocal<String> value = new ThreadLocal<>();

    public static void main(String[] args) throws InterruptedException {
        value.set("主线程设置的123");
        System.out.println("线程1执行之前,主线程取到的值: " + value.get());
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println("线程1取到的值: " + value.get());
                    value.set("线程1设置的值456");
                    System.out.println("重新设置后线程1取到的值: " + value.get());
                    System.out.println("线程1执行结束");
                } finally {
                    value.remove();
                }
            }
        }, "线程1");
        thread.start();
        // 等待线程1执行结束
        thread.join();
        System.out.println("线程1执行之后,主线程取到的值: " + value.get());
        value.remove();
    }
}

这段程序的输出是:

线程1执行之前,主线程取到的值: 主线程设置的123
线程1取到的值: null
重新设置后线程1取到的值: 线程1设置的值456
线程1执行结束
线程1执行之后,主线程取到的值: 主线程设置的123

可以看出,不同的线程通过ThreadLocal进行变量的读写时,是互不干扰的。

原理分析

ThreadLocal这么神奇,它到底是怎么实现的呢?

ThreadLocal有3个核心方法:

  • get()
  • set()
  • remove()

这里主要看get()方法 。

public T get() {
    // 拿到当前线程对应的ThreadLocalMap对象
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 从map中查询对应的变量副本
    if (map != null) {
        // 以ThreadLocal对象为key,从map中获取ThreadLocalMap.Entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 如果entry不为空,entry的value就是目标变量副本
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 否则,初始化变量副本
    return setInitialValue();
}

get()方法中可以看出,我们希望得到的变量副本存放在ThreadLocalMap中。而ThreadLocalMap是和线程绑定的:

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

Thread类里,有这样一个属性:

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

ThreadLocalMap的结构如下:

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;
        }
    }
    
    private Entry[] table;

ThreadLocalMap是一个哈希表,它里面存放若干个指向ThreadLocal对象的弱引用,而我们需要的value值就挂靠在这个弱引用上。因此,根据ThreadLocal找到对应的Entry就能拿到目标变量的副本。

这里使用弱引用的目的是希望在ThreadLocal对象被回收后可以自动回收value对象。

接下来看get()方法里的第二个分支,setInitialValue()。进入这个分支说明当前线程对应的ThreadLocalMap还未初始化,或者ThreadLocalMap里面还没有初始化ThreadLocal对象对应的Entry

private T setInitialValue() {
    // 获取初始值(变量副本)
    T value = initialValue();
    // 获取当前线程对应的ThreadLocalMap
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 如果ThreadLocalMap已经初始化,则将ThreadLocal对象和变量副本的映射关系保存在map中
    if (map != null)
        map.set(this, value);
    // 否则,初始化ThreadLocalMap,并保存ThreadLocal对象和变量副本的映射关系
    else
        createMap(t, value);
    // 返回变量副本的值
    return value;
}

其中,initialValue()的实现是:

protected T initialValue() {
    return null;
}

这是一个protected方法,默认返回null值。这意味着,对于一个ThreadLocal对象,线程访问它拿到的默认变量副本是null(这也解释了在前面的示例中线程1一开始拿到的是null值)。我们可以覆盖这个方法,指定一个默认的变量副本,这样可以省去调用get()方法时的一次非空判断。ThreadLocal类里有一个静态内部类SuppliedThreadLocal,它已经帮我们覆盖了默认的initialValue()方法,只需要使用ThreadLocal的静态方法ThreadLocal#withInitial就可以在创建ThreadLocal对象时轻松指定默认值。

public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

到这里,我们对get()方法的有了大致的了解:获取当前线程的ThreadLocalMap对象,在ThreadLocalMap里以ThreadLocal对象为Key查询EntryEntry对应的value就是我们希望得到的变量副本。如果查找失败,就初始化变量副本(还可能初始化ThreadLocalMap),并存入ThreadLocalMap里,再将变量副本返回给调用者。

ThreadLocal与使用它的Thread紧密相连:

  • 一个Thread有且仅有一个ThreadLocalMap对象。
  • 一个ThreadLocalMap对象存储多个Entry对象。
  • 一个Entry对象的key的弱引用指向一个ThreadLocal对象。
  • 一个ThreadLocal对象被多个线程所共享。
  • ThreadLocal对象不持有value,value由线程的Entry对象持有。

了解了get()的实现逻辑,set()remove()方法就不难理解了,这里不再展开。

注意事项

ThreadLocal的主要问题是会产生脏数据和内存泄漏。这两个问题通常是在线程池中使用ThreadLocal引发的,因为线程池有线程复用和内存常驻两个特点。

  1. 脏数据

线程复用会产生脏数据。由于线程池会重用Thread对象,那么与Thread绑定的ThreadLocalMap变量也会被重用。如果在实现的线程的run()方法中不显式的调用remove()清理与线程相关的ThreadLocal信息,那么倘若下一个任务不调用set()设置初始值,就有可能get()到重用的线程信息,包括ThreadLocal所关联的线程对象的value值。

  1. 内存泄漏

通常使用static关键字来修饰ThreadLocal,在此场景下,寄希望于ThreadLocal对象失去引用后,触发弱引用机制来回收Entry的value就不现实了。如果不进行remove()操作,那么ThreadLocal对象持有的value是不会被释放的。

以上两个问题解决办法很简单,就是在每次用完ThreadLocal时,必须及时调用remove()方法清理

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

推荐阅读更多精彩内容

  • 他们相识于一个雨夜。 彼时,女孩正被一个大汉钳住了双手,按在墙上,整个巷子里回荡着她的哭喊。 男孩本就瘦弱,可是不...
    姝小涵阅读 474评论 0 0
  • 【积极养育故事】妈妈,我又打翻了碗?——如何体验错误是最好的学习机会 ++2岁的时候,开始习惯性的说“不”...
    遇见真实的自己阅读 470评论 1 1
  • 我和你的爱 深情厚重 即使隔着时空 也逃不出思念的魔咒 有一丝微弱的光亮 若隐若现 冲破世俗枷锁 指引着彼此相互怜...
    心中的小火苗阅读 635评论 6 13
  • 几年前曾经在一次培训班上,一位男讲师发挥子下口才,本意是讲营销团队要优化结构,要从文化,年龄上来优化。结果他表...
    余香斋主人阅读 244评论 0 1