ThreadLocal实现原理和应用实例

java多线程编程中,线程安全是个很重要的概念。在保证线程安全的手段中,除了同步、锁、不变对象外,还有一种很重要的方法便是线程封闭技术,就是将可变对象封闭在一个线程中,同时只能由一个线程访问该对象。实现线程封闭的一种常用方法便是使用ThreadLocal类,这个类能使线程中的某个值与保存的对象关联起来。ThreadLocal为每个使用变量的线程都存有一份副本,因此get总是返回由当前线程set的最新值。那么ThreadLocal如何实现变量的线程封闭的呢?下面我们通过对ThreadLocal的源码分析来解读其实现原理。

实现原理

类的关系和职责

ThreadLocal中定义了一个内部静态类ThreadLocalMap,该类是线程本地变量的容器——一个类似HashMap的容器。这两个类再加上Thread类共同配合实现了线程封闭技术。首先,我们来看一下这几类的关系图

ThreadLocal类关系图

从图中我们可看出这几个类有如下重要关系:

  1. Thread持有一个包级别的ThreadLocalMap成员变量threadLocals,两者属于聚合关系

  2. ThreadLocal通过Thread.currentThread()静态方法获得对当前线程的引用依赖

  3. 由于ThreadLocal类与Thread类在同一个包下,因此ThreadLocal可以自由访问Thread.threadLocals变量,因此ThreadLocal类则负责创建和初始化threadLocals变量

  4. 我们自定义的应用类ApplicationClass类只依赖ThreadLocal类,并访问该类的公有方法可以写入和读取线程的本地变量

  5. ThreadLocalMap的key是ThreadLocal对象,value是ThreadLocal中存放的变量

图中类之间的关系还是比较绕的,但对于使用者来说相对简单,我们只关心ThreadLocal类便可以了。最常用的ThreadLocal类方法public T get()public void set(T value),还有一个protected作用域的方法protected T initialValue()用于初始化变量值。这里我们会发现,ThreadLocal类似于Facade设计模式的门面类,对外提供简单的接口,对内协调Thread和ThreadLocalMap之间的关系。而真正实现需求核心逻辑的是类Thread和ThreadLocalMap。

那么,我们可以定义这么一个需求:我们需要将一个或多个可变对象的封闭在一个线程中,使得一个可变对象在不进行同步的情况下,同时只能有一个线程可以访问该变量,从而实现线程安全。对于这个需求,我们明确一下三个类的职责关系,可能更容易理解:

  1. Thread类是使线程封闭的主体,对象的访问权限就是封闭在当前运行的线程中,只能由当前线程访问(这也是为什么会由Thread类以聚合的关系持有ThreadLocalMap对象)

  2. 我们的需求是可以实现多个可变对象的线程封闭,因此,我们定义了一个map容器用于存储多个可变对象,这个类便是ThreadLocalMap类,这个map的key是ThreadLocal对象,value是可变对象(也就是那个被封闭起来的宝宝)。

  3. ThreadLocal对外提供简单的方法,同时由于ThreadLocal和可变对象是一对一的关系,正好可以作为ThreadLocalMap的key与可变对象做关联。

经过以上的分析,我们会发现,这个设计非常巧妙,既遵循了类职责单一的面向对象原则,又实现了我们的需求,同时对使用者非常友好。

ThreadLocalMap实现关键点

通过以上类关系和职责的分析,我们知道ThreadLocal的核心业务逻辑其实是在ThreadLocalMap中的,那么,接下来我们就重点关注一下ThreadLocalMap实现中的几个关键点。

从代码中我们发现,ThreadLocalMap其实就是一个简版的HashMap,在这个特定的map中的存储的是一个Entry数组,而ThreadLocalMap中的Entry实现非常简单却也很特别:

static class ThreadLocalMap {
    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
    */
    private Entry[] table;

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

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

这个Entry是一个弱引用,而ThreadLocalMap中的table数组变量只是持有ThreadLocal对象的虚引用。这里为什么要这么设计呢?我们不妨回忆一下几种引用的特性。

  1. 强引用(Strong Reference)就是我们最常见的普通对象引用,只要还有强引用指向这个对象,就表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。
  2. 软引用(SoftReference)是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
  3. 弱引用(WeakReference)并不能使对象被豁免垃圾回收,仅仅提供一种访问在弱引用状态下对象的途径。被弱引用关联的对象,在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重新实例化。它同样是很多缓存实现的选择。
  4. 虚引用(PhantomReference)不能通过它访问对象。幻象引用仅仅是提供了一种确保对象被 finalize 以后,做某些事情的机制,比如,通常用来做所谓的 Post-Mortem 清理机制,也有人利用幻象引用监控对象的创建和销毁。

这里我们重点关注一下弱引用的特性:只有弱引用指向的对象是不影响垃圾收集器对其回收的。那么,ThreadLocalMap中的Entry为什么定义为ThreadLocal的弱引用类呢?这里我们不妨反向去思考,假设这里不用虚引用的实现,而是类似HashMap中的强引用,会有什么问题吗?我们知道除了我们自定义的应用类持有ThreadLocal的强引用外,还存在一条线程到ThreadLocal的引用链条:Thread.threadLocals --> ThreadLocalMap.table --> ThreadLocal对象。在很多应用尤其是后端服务器应用中,会创建很多线程,并且线程的生命的周期往往比很多对象生命周期要长很多。在使用ThreadLocal的过程中,不在引用作用域范围内的ThreadLocal对象或手动置空(threadLocal=null)的ThreadLocal对象,是可以被垃圾回收掉的。而如果在线程到ThreadLocal的引用链中是强引用,那么这个ThreadLocal对象的生命周期将伴随着Thread对象的存活而一直存在。这其实是不利于垃圾回收和内存利用的,相当于白白浪费了ThreadLocal对象占用的内存空间。因此,为了不影响垃圾收集器对ThreadLocal变量的回收,这里Entry的实现用了WeakReference。

从上面代码分析中,我们也会了解到为了尽快实现垃圾收集器对ThreadLocal以及相关联的对象的回收,对于我们不再使用的ThreadLocal变量,最好将相关的强引用置空,以避免内存溢出。代码如下示例所示:

// threadLocal是事先定义好的ThreadLocal变量
threadLocal.remove();
threadLocal = null;

应用实例

在一些框架或基础类库中,ThreadLocal的使用还是比较广泛的。下面就举几个常见的例子:

concurent包中的应用

在读写锁java.util.concurrent.locks.ReentrantReadWriteLock实现中,ThreadLocal用于记录当前线程持有读锁的次数:

/**
 * ThreadLocal subclass. Easiest to explicitly define for sake
 * of deserialization mechanics.
*/
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
/**
 * The number of reentrant read locks held by current thread.
 * Initialized only in constructor and readObject.
 * Removed whenever a thread's read hold count drops to 0.
*/
private transient ThreadLocalHoldCounter readHolds;

spring中的事务管理

spring中的事务管理,也大量使用了ThreadLocal,代码如下:

// spring-core中定义的NamedThreadLocal
public class NamedThreadLocal<T> extends ThreadLocal<T> {
    private final String name;

    public NamedThreadLocal(String name) {
        Assert.hasText(name, "Name must not be empty");
        this.name = name;
    }

    public String toString() {
        return this.name;
    }
}
// spring-tx包中定义的事务管理器
public abstract class TransactionSynchronizationManager {

    private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

    private static final ThreadLocal<Map<Object, Object>> resources =
            new NamedThreadLocal<>("Transactional resources");

    private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
            new NamedThreadLocal<>("Transaction synchronizations");

    private static final ThreadLocal<String> currentTransactionName =
            new NamedThreadLocal<>("Current transaction name");

    private static final ThreadLocal<Boolean> currentTransactionReadOnly =
            new NamedThreadLocal<>("Current transaction read-only status");

    private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
            new NamedThreadLocal<>("Current transaction isolation level");

    private static final ThreadLocal<Boolean> actualTransactionActive =
            new NamedThreadLocal<>("Actual transaction active");
    // ...
}

在spring的事务管理框架通过将事务上下文信息保存在ThreadLocal对象中,实现了事务上线文与执行线程的关联关系,从而使事务管理与数据访问服务解耦,同时也保证了多线程环境下connection的线程安全问题。

避免滥用

不过,开发人员也经常会滥用ThreadLocal,例如将所有全局变量都作为ThreadLocal对象,或者作为一种“隐藏”的传参手段。但是ThreadLocal也有全局变量的缺点,它降低了代码的可重用性,并在类之间引入隐含的耦合性,因此,在使用时需要谨慎,必要的情况下再使用。

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

推荐阅读更多精彩内容