多线程知识梳理(9) - ThreadLocal

一、基本概念

1.1 ThreadLocal 的用途

首先,我们来看一下JDK源码中对于ThreadLocal的解释:

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

翻译过来就是:

ThreadLocal用来提供线程内的局部变量。这些变量在多线程环境下访问时能够保证各个线程里的变量相对独立于其它线程内的变量,ThreadLocal实例通常来说都是private static类型的。

因此,ThreadLocal适用于满足下面条件的场景:

  • 每个线程 有且仅有 该对象的一个实例
  • 在该线程的整个生命周期内 有多处用到 该实例
  • 存在 多线程访问 的情况

1.2 ThreadLocal 的使用

ThreadLocalAPI很简单,它包含以下四个签名:

  • get:获取ThreadLocal中当前线程共享变量的值。
  • set:设置ThreadLocal中当前线程共享变量的值。
  • remove:移除ThreadLocal中当前线程共享变量的值。
  • initialValueThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法,返回此方法值。

我们用下面的一小段例子,来熟悉一下ThreadLocal的使用。

class ThreadLocalSamples {

    private static ThreadLocal<Integer> sThreadLocal = new ThreadLocal<Integer>() {

        @Override
        protected Integer initialValue() {
            return 5;
        }

    };

    static void startSample() {
        for (int i = 0; i < 3; i++) {
            new SampleThread("thread_" + i).start();
        }
    }

    private static class SampleThread extends Thread {

        private String mThreadName;

        SampleThread(String threadName) {
            mThreadName = threadName;
        }

        @Override
        public void run() {
            for (int j = 0; j < 5; j++) {
                try {
                    long sleep = (long) (Math.random() * 50);
                    Thread.sleep(sleep);
                    int result = sThreadLocal.get();
                    sThreadLocal.set(++result);
                    Log.d("ThreadLocalSamples", "ThreadName=" + mThreadName + ",result=" + result);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

运行结果:

运行结果

从打印的结果可以看到,虽然这3个线程访问是同一个ThreadLocal实例,但是它们通过ThreadLocalget/set方法读写的并不是同一个实例,所以保证了在多线程环境下的独立性。

二、源码

2.1 源码实现

为了加深对于ThreadLocal的理解,我们来分析一下它的内部实现。ThreadLocal设计的核心思想就是:每一个Thread维护一个ThreadLocalMapThreadLocalMapkeyThreadLocal,而value就是真正要存储的Object。这种方案设计的优点是:

  • 每个MapEntry数量变小了,之前是Thread的数量,现在是ThreadLocal的数量,能提高性能。
  • Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。

我们先来看一下setget的主要流程:

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

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

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

写入的流程为:

  • 通过静态方法currentThread获取当前执行指令的线程。
  • 得到该线程的私有成员变量threadLocals,其类型为ThreadLocalMap,如果没有创建那么就先创建。
  • 通过ThreadLocalMapset方法存入实际的Object,其key值为ThreadLocal实例。

读取的流程为:

  • 通过静态方法currentThread获取当前执行指令的线程,然后获取和该线程关联的ThreadLocalMap
  • ThreadLocal实例为key值,通过ThreadLocalMapgetEntry方法找到Object,如果找到就直接返回;如果没有找到就调用setInitialValue方法,该方法会调用到我们重写的initialValue来尝试获取一个初始值。

总结下来就是:ThreadLocal将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦

它之所以可保证 多线程环境下的相互独立,原因在于:每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,线程可以正确的访问到自己的对象。

当然,这种 独立性必须要基于一个前提通过set方法存储的对象并不是多个线程共享的。如果是共享的,那么多个线程get出来的是同一个是实例,仍然会存在多线程问题。

2.2 ThreadLocalMap

ThreadLocalMapThreadLocal中的一个内部类,与HashMap类似,它也会遇到Hash冲突的问题,HashMap采用了 链地址法 解决冲突,而ThreadLocalMap则采用 开放寻址法 解决冲突。

关于ThreadLocalMap还有一个疑问,就是它有可能会出现内存泄漏,原因是:ThreadLocalMapkey值保存的是ThreadLocal的弱引用,假如ThreadLocal被回收,那么就会无法通过Key找到Object,假如线程一直没有结束,那么这些Object就永远不会被回收。

ThreadLocalMap内部对于这种情况做了优化,就是在getEntryset方法查找存储位置的时候,如果发现了keynull的槽,那么会将这些槽中对应的Object引用置为null。这并不能解决所有问题,对于使用者来说,可以做额外的两项优化操作:

  • 手动调用ThreadLocalremove函数,删除不再需要的ThreadLocal
  • ThreadLocal声明为private static的,使得ThreadLocal的生命周期更长。

参考文献

(1) 正确理解 ThreadLocal
(2) 深入剖析 ThreadLocal 实现原理以及内存泄漏问题
(3) ThreadLocal 和 synchronized 的区别

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

推荐阅读更多精彩内容