java多线程编程中,线程安全是个很重要的概念。在保证线程安全的手段中,除了同步、锁、不变对象外,还有一种很重要的方法便是线程封闭技术,就是将可变对象封闭在一个线程中,同时只能由一个线程访问该对象。实现线程封闭的一种常用方法便是使用ThreadLocal类,这个类能使线程中的某个值与保存的对象关联起来。ThreadLocal为每个使用变量的线程都存有一份副本,因此get总是返回由当前线程set的最新值。那么ThreadLocal如何实现变量的线程封闭的呢?下面我们通过对ThreadLocal的源码分析来解读其实现原理。
实现原理
类的关系和职责
ThreadLocal中定义了一个内部静态类ThreadLocalMap,该类是线程本地变量的容器——一个类似HashMap的容器。这两个类再加上Thread类共同配合实现了线程封闭技术。首先,我们来看一下这几类的关系图
从图中我们可看出这几个类有如下重要关系:
Thread持有一个包级别的ThreadLocalMap成员变量threadLocals,两者属于聚合关系
ThreadLocal通过Thread.currentThread()静态方法获得对当前线程的引用依赖
由于ThreadLocal类与Thread类在同一个包下,因此ThreadLocal可以自由访问Thread.threadLocals变量,因此ThreadLocal类则负责创建和初始化threadLocals变量
我们自定义的应用类ApplicationClass类只依赖ThreadLocal类,并访问该类的公有方法可以写入和读取线程的本地变量
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。
那么,我们可以定义这么一个需求:我们需要将一个或多个可变对象的封闭在一个线程中,使得一个可变对象在不进行同步的情况下,同时只能有一个线程可以访问该变量,从而实现线程安全。对于这个需求,我们明确一下三个类的职责关系,可能更容易理解:
Thread类是使线程封闭的主体,对象的访问权限就是封闭在当前运行的线程中,只能由当前线程访问(这也是为什么会由Thread类以聚合的关系持有ThreadLocalMap对象)
我们的需求是可以实现多个可变对象的线程封闭,因此,我们定义了一个map容器用于存储多个可变对象,这个类便是ThreadLocalMap类,这个map的key是ThreadLocal对象,value是可变对象(也就是那个被封闭起来的宝宝)。
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对象的虚引用。这里为什么要这么设计呢?我们不妨回忆一下几种引用的特性。
- 强引用(Strong Reference)就是我们最常见的普通对象引用,只要还有强引用指向这个对象,就表明对象还“活着”,垃圾收集器不会碰这种对象。对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,就是可以被垃圾收集的了,当然具体回收时机还是要看垃圾收集策略。
- 软引用(SoftReference)是一种相对强引用弱化一些的引用,可以让对象豁免一些垃圾收集,只有当 JVM 认为内存不足时,才会去试图回收软引用指向的对象。JVM 会确保在抛出 OutOfMemoryError 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。
- 弱引用(WeakReference)并不能使对象被豁免垃圾回收,仅仅提供一种访问在弱引用状态下对象的途径。被弱引用关联的对象,在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。这就可以用来构建一种没有特定约束的关系,比如,维护一种非强制性的映射关系,如果试图获取时对象还在,就使用它,否则重新实例化。它同样是很多缓存实现的选择。
- 虚引用(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也有全局变量的缺点,它降低了代码的可重用性,并在类之间引入隐含的耦合性,因此,在使用时需要谨慎,必要的情况下再使用。