当多个线程访问同一个共享变量
的时候,开发人员必须采取措施避免并发操作所产生的各种冲突情况,有两种措施,锁同步
及ThreadLocal
。
锁同步
是指线程在访问共享变量前必须先获取锁资源,若获取锁资源失败就会被挂起,直至其他线程释放锁资源后,才被唤醒并再次尝试获取锁资源。通过锁同步
机制,可以保证同一时间只有一个线程
可以访问共享变量
。
我们可以通过synchronized
关键字或Lock
实现锁同步
,以后再通过其他文章进行详细的介绍。
锁同步
保证了同一时间只能有一个线程访问共享变量
,而ThreadLocal
则通过另一种思路解决并发导致的问题,那就是为每个线程提供了一个各自独享的本地变量
,即各线程通过ThreadLocal
变量访问各自的本地变量
,因此无需锁同步保证线程安全。
锁同步:以牺牲时间
和效率
为代价解决并发访问共享变量的冲突,因为,竞争锁资源会导致线程的上下文切换。
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()
方法移除本地变量
。
要明白ThreadLocal
的原理,需要先搞清楚 Thread
、ThreadLocal
及ThreadLocalMap
三者的作用和关系。下面的类图展示了三者的主要成员变量、方法及各自的关系:
三者关系可以概括为:
ThreadLocal
并不存储实际的内容,它是一个工具类,各线程通过它,以ThreadLocal
对象为key
,要保存的值为value
,把本地变量
保存到各自的,类型为ThreadLocalMap
的threadLocals
成员变量中。下图展示了上面的例子是如何通过ThreadLocal
保存各自的变量
:
从上面的类图,可知线程类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;
}
Entry[]数组
中,staleSlot
为下标
的元素
的value
设置为null
,再把staleSlot
为下标的元素
设置为null
。staleSlot+1
开始遍历Entry[]数组
,目的是为了重新整理数组中元素的位置,使数组中的非空元素是连续保存的
。ThreadLocal
已经被回收了,把对应的Entry
的value
设置为null
,并把Entry[]数组
对应的下标元素设置为null。ThreadLocal
未被回收。ThreadLocal
的hashCode
与Entry[]数组长度-1
作按位与
计算对应的Entry
所在的下标。注意:只有在数组长度为2的n次方
才能使用这种算法,而ThreadLocalMap
的长度永远都为2的n次方。第(6)步
计算出的下标
与当前遍历到的下标
不一致,则证明该Entry
元素在保存到ThreadLocalMap
的时候,发生了Hash冲突
,即Entry[]数组
中,对应计算出的下标
的槽位,已被其他Entry
占用了,因此要继续往后
寻找第一个为null
的位置保存进去。第(6)步
计算出的下标
开始,寻找第一个
为null
的元素
的下标
,其目的是为了使Entry[]数组
中的非空元素连续保存,避免了由于清除ThreadLocal
被回收的Entry
元素而导致数组的非空元素
不连续。Entry
保存到Entry[]数组
的第(8)步
计算到的下标中。正如上节所说,线程的本地变量保存在Thread
的变量threadLocals
及inheritableThreadLocals
中,下面先对threadLocals
进行说明。ThreadLocal
并没有保存任何内容,它作为一个工具类
,通过get
、set
、remove
方法对Thread
的threadLocals
进行获取、存放及删除操作。下面对ThreadLocal中的代码进行简要的分析。
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)
}
threadLocals
指向的ThreadLocalMap
对象。threadLocals
不为空,则以本ThreadLocal
变量为key
,value
为值
保存到threadLocals
中。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);
}
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)
}
threadLocals
指向的ThreadLocalMap
对象。threadLocals
不为空,则以本ThreadLocal
变量为key
,获取value
。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)
}
initialValue()
方法获取初始值,initialValue()
方法只返回null
。threadLocals
。threadLocals
不为空,则以本ThreadLocal
为key
,null
为值保存到threadLocals
中。threadLocals
为空,则创建。null
。