Java 多线程(五):ThreadLocal 详解

ThreadLocal

  • 多线程在并发执行时,需要数据共享,因此才有了 volatile 变量解决多线程数据之间可见性的问题,也有了锁的同步机制,使变量或代码再某一时刻只能被一个线程访问,确保共享数据的正确性。但多线程并发执行时,并不是所有数据都是要共享的,这些不需要共享的数据让每个线程各自去维护就可以了,因此才有了 ThreadLocal
  • ThreadLocal(线程局部变量),其功能是为每一个使用该变量的线程提供一个变量值的副本,使它不会和其它线程的副本冲突,在线程结束后其副本会被垃圾回收。它为多线程并发访问提供了一种隔离机制
  • ThreadLocal 实例通常都是 private static 类型的,用于关联线程和线程上下文
  • ThreadLocal 类中有一个 Map,用于存储每一个线程的变量副本,从而做到为每一个线程维护变量副本,使得多个线程线程同时访问一份变量而不互相影响

ThreaLocal 接口方法

  • set(T value):设置当前线程的线程局部变量的值
  • get():返回当前线程所对应的线程局部变量的值
  • remove():将当前线程局部变量的值删除(当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度)
  • initialValue():返回该线程局部变量的初始值,该方法是一个 protected 的方法,目的是为了让子类重写而设计的(这个方法是一个延迟调用方法,在线程第 1 次调用 get 或 set 时才执行,并且仅执行 1 次,缺省实现直接返回一个 null)

ThreadLocal 用法

public class TreadLocalTest {

    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>();

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                public void run() {
                    int data = new Random().nextInt();
                    System.out.println(Thread.currentThread().getName() + " put data :" + data);
                    threadLocal.set(data);

                    new A().get();
                }
            }).start();
        }
    }

    static class A {
        public void get() {
            int data = threadLocal.get();
            System.out.println(Thread.currentThread().getName() + " get data :" + data);
        }
    }
}
  • 输出结果如下,可以看到 ThreadLocal 为线程之间提供了隔离机制,多线程虽然访问的是相同的 ThreadLocal 对象,但它们之间的线程局部变量都是独立的
Thread-0 put data :-405727869
Thread-1 put data :-2129414360
Thread-0 get data :-405727869
Thread-1 get data :-2129414360

ThreadLocal 解决的问题

  • 解决并发问题:使用 ThreadLocal 代替 synchronized 来保证线程安全,同步机制采用了“以时间换空间”的方式,而 ThreadLocal 采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响
  • 解决数据存储问题:ThreadLocal 为变量在每个线程中都创建了一个副本,所以每个线程可以访问自己内部的副本变量,不同线程之间不会互相干扰

Spring 与 ThreadLocal 解决线程安全问题

  • 在一般情况下,只有无状态的 Bean 才可以再多线程环境下共享
  • 但在 Spring 中绝大部分 Bean 都可以声明为 singleton(单例) 作用域,这是因为 Spring 对一些有"状态性对象"的 Bean 采用 ThreadLocal 进行处理,让它们成为线程安全的 "状态性对象",因此有状态的 Bean 才能以单例模式在多线程中正常工作
  • 它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题

ThreadLocal 内存泄露问题

  • 当使用线程池来复用线程时,一个线程使用完后并不会销毁线程,那么分发的那个实例会一直绑定在这个线程上
  • 由于 Entry 继承了 ThreadLocal,而 ThreadLocal 使用 WeakReference 弱引用,并作为了 ThreadLocalMap 的 Entry 的 Key
  • 如果在某些时候 ThreadLocal 对象被赋 Null 的话,弱引用会被 GC 收集,这样就会导致 Entry 的 Value 对象无法获取到到
static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
  • 线程被复用后如果有调用 ThreadLocal.get/set 方法的话,方法里面会去做遍历清除那些以 [ThreadLocal=Null ] 为 Key 的 Entry
  • 但如果一直没调用 ThreadLocal.get/set 方法的话就会导致内存泄漏了
  • 所以一般使用完 ThreadLocal 后要调用 threadLocal.remove() 方法线程局部变量

ThreadLocal 源码分析

  • 每个 Thread 维护一个 ThreadLocalMap 哈希表,这个哈希表的 key 是 ThreaLocal 实例本身(因为一个 Thread 可能有多个 ThreadLocal),value 才是真正要存储的值 Object

get 方法

  • 获取当前线程的 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap 对象
  • 判断当前的 ThreadLocalMap 是否存在
    • 如果存在,则以当前的 ThreadLocal 为 key,调用 ThreadLocalMap 中的 getEntry 方法获取对应存储的键值对 e,再获取 e 中的 value 值并返回
    • 如果不存在,则证明此线程没有维护的 ThreadLocalMap 对象,调用 setInitialValue 方法进行初始化,返回 setInitialValue 初始化的值
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
    
    ThreadLocalMap getMap(Thread t) {
        // 获取当前线程Thread对应维护的ThreadLocalMap 
        return t.threadLocals;
    }

setInitialValue 方法

  • 调用 initialValue 获取初始化的值
  • 获取当前线程 Thread 对象,进而获取此线程对象中维护的 ThreadLocalMap 对象
  • 判断当前的 ThreadLocalMap 是否存在
    • 如果存在,则调用 map.set 设置此实体 entry
    • 如果不存在,则调用 createMap 进行 ThreadLocalMap 对象的初始化,并将此实体 entry 作为第一个值存放至 ThreadLocalMap 中
    // 返回该线程局部变量的初始值,该方法是一个 protected 的方法,目的是为了让子类重写而设计的
    protected T initialValue() {
        return null;
    }
    
    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;
    }

set 方法

  • 获取当前线程 Thread 对象,进而获取此线程对象中维护的ThreadLocalMap对象
  • 判断当前的 ThreadLocalMap是否存在:
    • 如果存在,则调用 map.set 设置此实体 entry
    • 如果不存在,则调用 createMap 进行 ThreadLocalMap 对象的初始化,并将此实体 entry 作为第一个值存放至 ThreadLocalMap 中
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

    // 为当前线程 Thread 创建对应维护的 ThreadLocalMap
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

remove 方法

  • 获取当前线程 Thread 对象,进而获取此线程对象中维护的ThreadLocalMap对象
  • 判断当前的 ThreadLocalMap是否存在:
    • 如果存在,则调用 map.remove 以当前 ThreadLocal 为 key 删除对应的实体 entry
    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,919评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,567评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,316评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,294评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,318评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,245评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,120评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,964评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,376评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,592评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,764评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,460评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,070评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,697评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,846评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,819评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,665评论 2 354

推荐阅读更多精彩内容

  • 前言 ThreadLocal很多同学都搞不懂是什么东西,可以用来干嘛。但面试时却又经常问到,所以这次我和大家一起学...
    liangzzz阅读 12,446评论 14 228
  • Android Handler机制系列文章整体内容如下: Android Handler机制1之ThreadAnd...
    隔壁老李头阅读 7,635评论 4 30
  • 1. 概念 ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变...
    zly394阅读 1,740评论 0 1
  • 房盾科技第一次问好 人类最伟大的成就就是她缔造的城市,城市代表了人作为一个物种具有的恢弘想象力,以及深远持久重塑自...
    房盾科技阅读 285评论 0 0
  • 感恩天地滋养,宇宙永恒;感恩大自然无私的爱;感恩祖先传承,历代宗亲护佑;感恩国泰民安,繁荣昌盛;感恩父母生养大恩,...
    天门金珠瑜伽阅读 183评论 0 3