【JDK1.8源码学习】ThreadLcoal

前言

线程封闭:当访问共享的可变数据时,通常需要同步。一种避免同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步,这种技术称为线程封闭(thread confinement)【摘自《Java并发编程实战》】

实现线程封闭的三种手段:
(1)Ad-hoc线程封闭: 开发者自己实现线程封闭,这种方式严重依赖开发者的水平,最后效果可能会相当的脆弱,因此基本不被推荐;
(2)栈封闭: JVM为我们实现的线程封闭,对应的是JVM区域中的栈区,当一个方法被多个线程调用时,这个方法内部的局部变量在每个线程内都是独立不被共享的,达到了线程封闭的效果,这是JVM的固有特性;
(3)ThreadLocal线程封闭: Java针对线程封闭规范封装的一个关键字,本文就针对这一关键字的实现原理做简单的阅读和分析。

介绍

ThreadLocal,线程私有变量,JDK提供的该工具类可以使得一种类型的对象单独存放在各自的线程里,不与其他线程共享,从而达到线程封闭的效果,因此存放在其中的变量也是线程安全的。
通常情况下,ThreadLocal变量设置为一个全局静态变量,本次学习基于JDK1.8

设计原理

  • ThreadLocal内部定义了一个ThreadLocalMap静态内部类,ThreadLocalMap内部又维护了一个Entry<T> extends WeakReference<ThreadLocal<?>>类型的table数组,该Entry类并非HashMap中的Entry键值对,但是具有类似的功能,即键-值存储功能;该Entry类内部有一个Object value字段,该value存放的就是实际每个线程需要存放的ThreadLocal对象;Entry继承自弱引用类WeakReference,此设定存在内存泄漏风险,关于这点后面后有说明;
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;
}
  • 同时,Thread线程类也维护了一个属性threadLocals:ThreadLocal.ThreadLocalMap threadLocals = null;该属性的作用就是维护一个Entry数组table,每个元素的referent对象为一个ThreadLocal对象,value为实际需要线程封闭的对象;Thread对象在初始化时,threadlocals属性赋值为null,所以第一次调用时获取到的threadLocals对象必然为null,这个时候就会去初始化这个threadLocals对象,并且赋予初值,如果重写了initialValue方法就返回重写方法体内构造的对象,否则就默认返回 null
public class Thread implements Runnable {
    // 当前线程占有的线程局部变量值,map由ThreadLocal类维护
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

源码解读

  1. get()方法:获得当前线程的局部变量值。
// 返回当前当前线程局部变量值,如果当前线程的局部变量map还未被初始化,那么执行setInitialValue方法进行初始化
public T get() {
    // 首先获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程中维护的ThreadLocal.ThreadLocalMap threadLocals变量
    ThreadLocalMap map = getMap(t);
    if (map != null) { // threadLocals变量未被初始化
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    
    // threadLocals变量未被初始化
    return setInitialValue();
}

get方法流程图

get方法流程图

在执行get方法时,有一个重要的方法也必须被提及,这个方法由ThreadLocal.ThreadLocalMap来维护,即getEntry()方法,

/**
 * 获取当前ThreadLocal对象对象的entry,如果存在对应的entry,对应的entry会被命中,且该过程将是快速的;
 * 如果不存在对应的entry,那么将出发getEntryAfterMiss方法;
 * 
 * @param  key ThreadLocal对象
 * @return 与传入key关联的Entry对象,不存在则返回null
 */
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);
}

getEntry方法流程图:


getEntry方法流程图
  1. set()方法:为当前线程指定一个局部变量值。
/**
 * 为当前线程指定一个局部变量值.
 */
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程维护的ThreadLocal.ThreadLocalMap threadLocals变量
    ThreadLocalMap map = getMap(t);
    if (map != null) // threadLocals变量已被初始化过则存放值
        map.set(this, value);
    else // threadLocals变量未被初始化,出发创建流程:createMap
        createMap(t, value);
}

set方法流程图:


set方法流程图

关于内存泄漏

学习借鉴于(侵删):深入分析 ThreadLocal 内存泄漏问题

  • 为什么会内存泄漏?
    ThreadLocalMap内维护的Entry数组映射表,key为ThreadLocal对象本身,value为实际需要被隔离的线程私有对象;

    由于Entry继承自弱引用类WeakReference<T>,使得key的引用为弱引用,弱引用的特性(引用强度比强引用和软引用都更弱,被其关联的对象只能生存到下一次GC之前,JDK1.2之后提供WeakReference来实现软引用),当线程存活时间足够长,直到下一次GC发生,key就会被回收,如果GC后线程依然存活,那么就会存在key为null的Entry.value,这个value就永远不会被访问到,导致内存泄漏。

  • 如何防止内存泄漏?
  1. ThreadLocal在设计时,加上了一些防护措施,即ThreadLocal的get()set()remove()方法都会清除key为null的entry;
  2. 在使用完后及时调用remove()方法;
  • Entry.key为何不使用强引用?
    从内存泄漏的原因分析来看,似乎是因为key使用了弱引用WeakReference,那么key为什么不使用引用强度更强的强引用来避免内存泄漏呢?
         我们可以确定的是,ThreadLocalMap的生命周期和Thread一样长,假设key为强引用对象,如果ThreadLocal对象不需要了被回收,但是由于ThreadLocalMap还持有ThreadLocal对象的强引用,是不会被GC回收掉的,而且ThreadLocal自身的机制也不能清除掉这个Entry,除非线程结束或者手动置null,否则ThreadLocal对象和对应的Entry会一直占用内存也造成内存泄漏;
         反之,如果是弱引用,那么至少在下次GC时ThreadLocal对象就会被回收,再辅以ThreadLocal自身的清除机制,整个Entry也会被清除。

总结

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

推荐阅读更多精彩内容