1 ThreadLocal的作用
当多个线程访问同一个共享变量
的时候,开发人员必须采取措施避免并发操作所产生的各种冲突情况,有两种措施,锁同步
及ThreadLocal
。
1.1 锁同步
锁同步
是指线程在访问共享变量前必须先获取锁资源,若获取锁资源失败就会被挂起,直至其他线程释放锁资源后,才被唤醒并再次尝试获取锁资源。通过锁同步
机制,可以保证同一时间只有一个线程
可以访问共享变量
。
我们可以通过synchronized
关键字或Lock
实现锁同步
,以后再通过其他文章进行详细的介绍。
1.2 ThreadLocal
锁同步
保证了同一时间只能有一个线程访问共享变量
,而ThreadLocal
则通过另一种思路解决并发导致的问题,那就是为每个线程提供了一个各自独享的本地变量
,即各线程通过ThreadLocal
变量访问各自的本地变量
,因此无需锁同步保证线程安全。
1.3 锁同步与ThreadLocal对比
-
锁同步:以牺牲
时间
和效率
为代价解决并发访问共享变量的冲突,因为,竞争锁资源会导致线程的上下文切换。 -
ThreadLocal:每个线程拥有自己的
本地变量
,不再需要考虑冲突的情况,但需要消耗更多的内存资源
用于保存本地变量
。
2 ThreadLocal的使用例子
下面的例子,定义一个名为localStr
的ThreadLocal
变量;启动两个线程,分别使用localStr
设置各自的变量值,并调用print()
方法,print()
方法会调用localStr.get()
获取各线程的值,并输出。
public class ThreadLocalTest {
private final static ThreadLocal<String> localStr = new ThreadLocal<>();
/**
* 输出localStr的值
*/
private static void print() {
// 打印变量
System.out.println(Thread.currentThread().getName() + " - " + localStr.get());
// 后面不再需要该本地变量了,把它remove掉
localStr.remove();
}
public static void main(String[] args) {
//
// 创建线程1
//
new Thread(() -> {
// 设置线程1的本地变量
localStr.set("local value from thread-1");
// 打印本地变量
print();
// print方法中,打印后会把本地变量移除,因此此处localStr.get()将返回null
System.out.println("local value after thread-1 print() : " + localStr.get());
}, "thread-1").start();
//
// 创建线程2
//
new Thread(() -> {
// 设置线程2的本地变量
localStr.set("local value from thread-2");
// 打印本地变量
print();
// print方法中,打印后会把本地变量移除,因此此处localStr.get()将返回null
System.out.println("local value after thread-2 print() : " + localStr.get());
}, "thread-2").start();
}
}
运行上面的代码,控制台将输出:
thread-1 - local value from thread-1
local value after thread-1 print() : null
thread-2 - local value from thread-2
local value after thread-2 print() : null
从上面的代码,可以看到ThreadLocal的两个常用方法,get()
、set(T value)
及remove()
。上面的例子中:
(1) 定义了一个名为localStr
的ThreadLocal
类型变量。
(2) thread-1和thread-2都调用了localStr.set(Strint)
方法设置各自的本地变量,最后的输出结果表明,不同的线程调用同一个ThreadLocal
变量设置各自的本地变量
并不会出现并发冲突
的情况。
(3) 为了避免OOM异常
,在本地变量
使用完毕后,应该调用ThreadLocal 变量的remove()
方法移除本地变量
。
3 TheadLocal的实现原理
要明白ThreadLocal
的原理,需要先搞清楚 Thread
、ThreadLocal
及ThreadLocalMap
三者的作用和关系。下面的类图展示了三者的主要成员变量、方法及各自的关系:
三者关系可以概括为:
ThreadLocal
并不存储实际的内容,它是一个工具类,各线程通过它,以ThreadLocal
对象为key
,要保存的值为value
,把本地变量
保存到各自的,类型为ThreadLocalMap
的threadLocals
成员变量中。下图展示了上面的例子是如何通过ThreadLocal
保存各自的变量
:
3.1 Thread与ThreadLocalMap
从上面的类图,可知线程类Thread
都包含了类型为ThreadLocalMap
的两个变量,threadLocals
及inheritableThreadLocals
,上面说的线程的本地变量
就是保存在它们里面,每个Thread都有属于自己的threadLocals
和inheritableThreadLocals
,互不相影响。下面为这两个ThreadLocalMap
的定义代码段:
public
class Thread implements Runnable {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
...
}
ThreadLocalMap
是ThreadLocal
里面的内部类,它的结构类似于HashMap
。ThreadLocalMap
是一个以ThreadLocal
为Key
的Map
,下面截取ThreadLocalMap
的代码段:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
...
}
从上面的代码段可知ThreadLocalMap
的大概结构:
(1) 与HashMap
类似,内部都是使用一个Entry
数组保存数据。
(2) Entry
是以ThreadLocal变量为Key。
(3) Entry
继承了WeakReference
,在Entry
构造器中,通过调用super(k)
把ThreadLocal<?>
变量保存到WeakReference
的referent
变量中。WeakReference
被称为弱引用
,若一个对象,只被WeakReference
引用了,那么,当发生gc
的时候,该对象就会被回收。例如下面的代码:
public static void main(String[] args) {
ThreadLocal<String> testTl = new ThreadLocal<>();
WeakReferenceTest test = new WeakReferenceTest(testTl);
System.out.println(test.get());
System.out.println(testTl);
System.out.println("=====================================================");
System.gc();
// testTl = null; (1)
Thread.sleep(10_000L);
System.out.println(test.get());
System.out.println(testTl);
}
static class WeakReferenceTest extends WeakReference<ThreadLocal<String>> {
public WeakReferenceTest(ThreadLocal<String> referent) {
super(referent);
}
}
上面的代码,定义了WeakReference
的子类WeakReferenceTest
,并定义了一个名为testTl
的ThreadLocal
变量,并把它引用到WeakReferenceTest
中。然后通过System.gc()
强行触发gc,最后通过WeakReferenceTest.get()
方法查看该ThreadLocal
变量是否已被回收。
运行上面代码可看到下面输出,会发现该ThreadLocal变量并没有在gc时候被回收,这是因为它还被testTl引用着:
java.lang.ThreadLocal@25f38edc
java.lang.ThreadLocal@25f38edc
=====================================================
java.lang.ThreadLocal@25f38edc
java.lang.ThreadLocal@25f38edc
若我们把(1)的注释去掉,该ThreadLocal
变量不再被testTl
引用,而只被WeakReference
引用了,因此,在gc时候就被回收:
java.lang.ThreadLocal@25f38edc
java.lang.ThreadLocal@25f38edc
=====================================================
null
null
因此,ThreadLocalMap
中的Entry
继承WeakReference
的目的也非常明确了,就是若当作为Key
的ThreadLocal
变量仅被Entry
引用的时候,它就会在gc的时候被回收,当ThreadLocal
被回收后,ThreadLocalMap
中会存在key
为null
的Entry
,因此在执行get
、set
、remove
方法中,都会直接或间接调用expungeStaleEntry(int staleSlot)
方法删除ThreadLocal
已被回收的Entry
。
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null; // (1)
tab[staleSlot] = null;
size--; // (2)
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len); // (3)
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) { // (4)
e.value = null;
tab[i] = null;
size--;
} else { // (5)
int h = k.threadLocalHashCode & (len - 1); // (6)
if (h != i) { // (7)
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null) // (8)
h = nextIndex(h, len);
tab[h] = e; // (9)
}
}
}
return i;
}
- (1) 把
Entry[]数组
中,staleSlot
为下标
的元素
的value
设置为null
,再把staleSlot
为下标的元素
设置为null
。 - (2) 由清除了一个元素,因此size减1。
- (3) 从
staleSlot+1
开始遍历Entry[]数组
,目的是为了重新整理数组中元素的位置,使数组中的非空元素是连续保存的
。 - (4)
ThreadLocal
已经被回收了,把对应的Entry
的value
设置为null
,并把Entry[]数组
对应的下标元素设置为null。 - (5)
ThreadLocal
未被回收。 - (6) 通过
ThreadLocal
的hashCode
与Entry[]数组长度-1
作按位与
计算对应的Entry
所在的下标。注意:只有在数组长度为2的n次方
才能使用这种算法,而ThreadLocalMap
的长度永远都为2的n次方。 - (7) 若
第(6)步
计算出的下标
与当前遍历到的下标
不一致,则证明该Entry
元素在保存到ThreadLocalMap
的时候,发生了Hash冲突
,即Entry[]数组
中,对应计算出的下标
的槽位,已被其他Entry
占用了,因此要继续往后
寻找第一个为null
的位置保存进去。 - (8) 从
第(6)步
计算出的下标
开始,寻找第一个
为null
的元素
的下标
,其目的是为了使Entry[]数组
中的非空元素连续保存,避免了由于清除ThreadLocal
被回收的Entry
元素而导致数组的非空元素
不连续。 - (9) 把
Entry
保存到Entry[]数组
的第(8)步
计算到的下标中。
3.2 ThreadLocal的作用
正如上节所说,线程的本地变量保存在Thread
的变量threadLocals
及inheritableThreadLocals
中,下面先对threadLocals
进行说明。ThreadLocal
并没有保存任何内容,它作为一个工具类
,通过get
、set
、remove
方法对Thread
的threadLocals
进行获取、存放及删除操作。下面对ThreadLocal中的代码进行简要的分析。
3.2.1 void set(T value)
public void set(T value) {
Thread t = Thread.currentThread(); // (1)
ThreadLocalMap map = getMap(t); // (2)
if (map != null) // (3)
map.set(this, value);
else
createMap(t, value); // (4)
}
- (1) 获取当前的线程。
- (2) 获取当前线程的
threadLocals
指向的ThreadLocalMap
对象。 - (3) 若
threadLocals
不为空,则以本ThreadLocal
变量为key
,value
为值
保存到threadLocals
中。 - (4) 若
threadLocals
为空,则创建,线程的threadLocals
变量初始值
为null
。
下面再看getMap(Thread t)
方法,返回的就是线程的threadLocals
。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
下面再看createMap(Thread t, T firstValue)
方法,为线程的threadLocals
创建一个ThreadLocalMap
对象。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
3.2.2 T get()
public T get() {
Thread t = Thread.currentThread(); // (1)
ThreadLocalMap map = getMap(t); // (2)
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // (3)
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue(); // (4)
}
- (1) 获取当前线程。
- (2) 获取当前线程的
threadLocals
指向的ThreadLocalMap
对象。 - (3) 若
threadLocals
不为空,则以本ThreadLocal
变量为key
,获取value
。 - (4) 若
threadLocals
为空,则初始化当前线程的threadLocals
变量。
下面再看T setInitialValue()
方法:
protected T initialValue() {
return null;
}
private T setInitialValue() {
T value = initialValue(); // (1)
Thread t = Thread.currentThread(); // (2)
ThreadLocalMap map = getMap(t); // (3)
if (map != null) // (4)
map.set(this, value);
else // (5)
createMap(t, value);
return value; // (6)
}
- (1) 执行
initialValue()
方法获取初始值,initialValue()
方法只返回null
。 - (2) 获取当前线程。
- (3) 获取当前线程的
threadLocals
。 - (4) 如果
threadLocals
不为空,则以本ThreadLocal
为key
,null
为值保存到threadLocals
中。 - (5) 如果
threadLocals
为空,则创建。 - (6) 返回
null
。
3.2.3 void remove()
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread()); // (1)
if (m != null) // (2)
m.remove(this);
}
- (1) 获取当前线程的
threadLocals
指向的ThreadLocalMap
对象。 - (2) 以为当前的
ThreadLocal
对象为key
,删除对应的Entry
。
注意:
由于每个线程的本地变量
被保存在各自的threadLocals
变量中,若线程一直不结束,则threadLocals
的本地变量会一直存在,有可能会造成内存溢出
(OOM
),因此当一个本地变量使用完后,我们应该使用remove()
把它删除,这是一个编程的良好习惯。
4 InheritableThreadLocal
上面介绍了,Thread
里面包含了两个ThreadLocalMap
的对象,threadLocals
和inheritableThreadLocals
,其中ThreadLocal
操作的是threadLocals
,那inheritableThreadLocals
有上面作用呢?
4.1 ThreadLocal无法获取父线程的本地变量
先看下面代码:
public class ThreadLocalTest {
private static ThreadLocal<String> localStr = new ThreadLocal<>(); // (1)
public static void main( String[] args ) {
localStr.set("local variable"); // (2)
// (3)
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " = " + localStr.get());
}, "child-thread").start();
System.out.println(Thread.currentThread().getName() + " = " + localStr.get());
}
}
运行上面代码,控制台将输出:
child-thread = null
main = local variable
- (1) 定义一个
ThreadLocal
变量localStr
。 - (2) 在
main线程
设置localStr
的值。 - (3) 新建
子线程child-thread
,尝试输出在main线程
设置的localStr
的值,但无法获取,输出null
。这也正常,因为,我们上面已经介绍了,本地变量只保存在各线程自己的threadLocals
变量中。
若我们需要在子线程获取父线程的本地变量,可以使用ThreadLocal
的子类,InteritableThreadLocal
。
4.2 InheritableThreadLocal分析
InteritableThreadLocal
继承自ThreadLocal
,重写了T childValue(T parentValue)
,ThreadLocalMap getMap(Thread t)
及void createMap(Thread t, T firstValue)
三个方法:
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
可以看到,InheritableThreadLocal
操作的对象是Thread
的inheritableThreadLocals
,其他跟ThreadLocal
的行为是一致的。
那子线程
又是如何获取父线程
的inheritableThreadLocals
的本地变量的?
从Thread
的构造器
说起:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
Thread
的构造器
会调用init
方法,再追踪至最终的init
方法:
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc) {
...
Thread parent = currentThread(); // (1)
...
if (parent.inheritableThreadLocals != null) // (2)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); // (3)
...
}
-
(1) 获取当前正在创建
子线程
的线程(父线程)
。 -
(2) 如果
父线程
的inheritableThreadLocals
不为空,以父线程
的inheritableThreadLocals
为参数,调用ThreadLocal
的createInheritedMap
方法。
再看ThreadLocal
的createInheritedMap
方法:
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap); // (1)
}
- (1) 以父线程的
inheritableThreadLocals
为参数
,实例化一个ThreadLocalMap
。
最后,我们再看ThreadLocalMap
的构造器代码:
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
构造器
的逻辑非常简单,就是把父线程
的inheritableThreadLocals
拷贝到当前正在实例化的ThreadLocalMap
中,从而,子线程
再实例化的时候,它的inheritableThreadLocals
获取到了父线程的inheritableThreadLocals
中的内容。
因此,若把上面的例子中的localStr
改为InheritableThreadLocal
类型,则子线程
就可以获取父线程
的本地变量了:
public class InheritableThreadLocalTest {
private static InheritableThreadLocal<String> localStr = new InheritableThreadLocal<>();
public static void main( String[] args ) {
localStr.set("local variable");
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " = " + localStr.get());
}, "child-thread").start();
System.out.println(Thread.currentThread().getName() + " = " + localStr.get());
}
}
运行上面的程序,从控制台的输出可以发现,子线程获取到了父线程的本地变量:
child-thread = local variable
main = local variable
5 总结
(1) ThreadLocal为每个线程提供了一个各自独享的本地变量
,即各线程通过ThreadLocal
变量访问各自的本地变量
,因此无需锁同步保证线程安全,但需要消耗更多的内存资源
。
(2) 为了避免内存溢出异常
,我们应该养成好的习惯,那就是再ThreadLocal
变量使用完后,要马上调用remove()
方法