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
- 1
- 2
- 3
- 4
从上面的代码,可以看到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;
...
}
ThreadLocaMap是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;
...
}
27
从上面的代码段可知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。
评论(0)