概述
ThreadLocal
是一种线程封闭技术,用于隔离线程间的数据,从而避免使用同步控制。
一种避免使用同步的方式就是不共享数据。如果仅在单线程内访问数据,就不需要同步。这种技术称为线程封闭。
ThreadLocal
为每条使用它的线程提供专属的内部变量。在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相互独立,互不影响。
基本用法
ThreadLocal
对象通常被设计为类的私有静态类型(private static
)字段,用来关联线程的某种状态。
举个例子:
public class ThreadLocalTest {
private static ThreadLocal<Integer> counter = new ThreadLocal<Integer>() {
protected Integer initialValue() {
return new Integer(0);
}
};
public static class MyRunnable implements Runnable {
private void save() {
System.out.printf("线程[%s]保存数据, 当前计数是: %s\n", Thread.currentThread().getId(), counter.get());
}
public void run() {
while(true) {
counter.set( (counter.get() + 1) % 10 );
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.printf("线程[%s]处理业务, 当前计数是: %s\n", Thread.currentThread().getId(), counter.get());
if(counter.get() == 0) {
save();
}
}
}
}
public static void main(String[] args) throws InterruptedException {
MyRunnable runnable = new MyRunnable();
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
这个例子很简单,业务线程内有一个循环在不断的处理业务,假设每次处理业务会产生一些数据,出于性能考虑,希望每处理完10次业务才批量保存数据。
ThreadLocal
主要有四个方法:
initialValue
get
set
-
remove
(例子中未使用)
下面逐一简介。
initialValue
方法
initialValue
是设计给子类重写的方法,用以返回初始化的线程内部变量。在线程第一次调用get
时它会被调用,但如果在调用get
之前已经调用了set
为线程内部变量设过值,则该方法不会被调用。所以,如果你希望手动调用set
来初始化线程内部变量,则不必重写initialValue
。
通常initialValue
只会被调用一次,除非手动调用remove
清除了内部变量,之后又调用get
方法,这时initialValue
会再被调用初始化一个新的内部变量返回。
get
方法
get
用以获取ThreadLocal
对象关联的线程内部变量。
public T get()
set
方法
set
用以设置ThreadLocal
对象关联的线程内部变量的值。
public void set(T value)
remove
方法
remove
用以移除ThreadLocal
对象关联的线程内部变量,某些情况需要用它来显式地移除,以防止内存泄漏。
public void remove()
你可能会问,为什么要这么复杂,在run
里面使用一个方法局部变量来做计数器岂不是更简单。
对于这个例子来说,确实如此,ThreadLocal
的功能性和方法局部变量没有本质的区别。
不过,ThreadLocal
相较于方法局部变量,可以帮你管理线程内部变量,降低了同一线程内多个方法和组件间传递参数的复杂度。
内部实现
如上图所示,ThreadLocal
机制主要由Entry
、ThreadLocalMap
、Thread
、ThreadLocal
这四个类相互协作实现的。
下面分析这四个类各自的职责和协作
Entry
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry
的定义很简单,它扩展自ThreadLocal
类型的WeakReference
类,是一个key-value对类。key是ThreadLocal
对象的弱引用,value
是线程的内部变量。
Entry
使用弱引用作为key目的是,希望在外部不再需要访问ThreadLocal
对象时可以让GC尽快地回收对象,而不必等到线程结束后。
当GC回收ThreadLocal
对象后,再通过Entry.get()
获取ThreadLocal
对象时返回null
,这使得内部能够感知什么时候不需要再持有对value
的引用,从而释放Entry
对象的引用,进而释放value的引用,这时如果value在外部没有任何引用的话(通常你不应该在外部持有对value的引用),随后被GC回收。这种感知和释放的行为发生在ThreadLocal
的get
、set
、remove
操作时。
Thread
Thread
内部持有一个ThreadLocalMap
类型引用的成员变量。
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
threadLocals
的初始值为null
,它会延迟到初次访问时才实例化,即线程首个ThreadLocal
对象调用get
方法时才为threadLocals
创建对象。
ThreadLocalMap
ThreadLocalMap
是为ThreadLocal
而设计的hash map
,内部维护着一个哈希table
数组,table
内保存Entry
的对象,通过ThreadLocal
的哈希码可索引到(哈希码需转成数组下标)。
ThreadLocal
ThreadLocal
是整个机制的总导演,对外,它提供使用的接口;对内,它协调类之间的相互协作。
ThreadLocal
内部不会持有对线程内部变量的引用,线程内部变量的引用由Entry
对象持有,而Entry
对象寄存在ThreadLocalMap
内的table
中。
每一个ThreadLocal
对象对应一个唯一的哈希码(threadLocalHashCode
),通过这个哈希码可以从ThreadLocalMap
中索引出对应的Entry
,从而获得线程内部变量。
这里很巧妙,ThreadLocal
对象与线程内部变量之间通过Entry
对象间接关联,在内部只有Entry
对象持有对ThreadLocal
对象的弱引用,这样当外部不再使用ThreadLocal
对象后,GC能够回收ThreadLocal
对象,当内部探测到ThreadLocal
对象被回收后就接着释放Entry
对象。
最后
Java与C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人却想出来。
通常在Java的世界里,我们不需要关系对象的释放,大部分情况下GC会自动帮我们回收。
但是如果使用ThreadLocal
不当,是有可能导致内存泄漏的。
ThreadLocal
释放内部变量通常在以下时机:
- 线程结束后
- 显式调用
remove
- 在调用
get
、set
时,如果探测到ThreadLocal
对象的弱引用对象get
返回null
顺便释放。
所以,如果线程存活的生命周期很长,特别是和进程一样长的话,就要特别注意防止ThreadLocal
引入内存泄漏的风险,在不需要再使用某个线程内部变量时记得显式调用remove
清理掉。