如果写过多线程的代码,你肯定考虑过线程安全问题,更进一步你可能还考虑在在线程安全的前提下性能的问题。大多数情况下大家用来解决线程安全问题都会使用同步,比如用synchron或者concurrent包提供的各种锁,当然这些都能解决问题。但有多线程做同步一定会涉及到资源争抢和等待的问题。java中各种同步方法都是提供一种准入机制,JVM会调用系统同步原语来保证临界区任意时刻只能有一个线程进入,那必然其他线程都得等待了,性能的瓶颈就在这同步上了。
解决问题最好的方式是啥?当然是避免问题的发生了。ThreadLocal就是用这样一种方式提升性能的。ThreadLocal也不是万金油,它也只能在多线程之间数据相互独立的情况下使用,如果是多线程间的数据同步,还得使用某个同步的方式。
要理解ThreadLocal的工作原理,还需要了解Thread,ThreadLocalMap
每一个ThreadLocal只能存储一个值,一个ThreadLocal可以被多个线程记录,一个线程Thread也可以记录多个ThreadLocal,他们通过ThreadLocalMap来关联。
ThreadLocal
1. 先从ThreadLocal提供的几个公共方法入口来分析它的使用原理
get方法
public T get() {
Thread t = Thread.currentThread();
// 获取当前线程对象保存的ThreadLocalMap,存储在线程对象的成员变量threadLocals中
ThreadLocalMap map = getMap(t);
if (map != null) {
// 关于ThreadLocalMap的相关操作,另外写在其他地方
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果当前线程对象的threadLocals尚未创建,则设置初始值
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private T setInitialValue() {
// 获取默认的初始值,如未重写该方法,默认为null
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
/**
* initialValue方法,默认返回null,如果希望在未进行set操作之前,调用get方法时能返回一个非
* null初始值,那么必须创建一个ThreadLocal的子类对象并重写该方法。
*/
protected T initialValue() {
return null;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
set方法
public void set(T value) {
Thread t = Thread.currentThread();
// 可以发现,实际的赋值是对当前线程绑定的ThreadLocalMap进行赋值
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
可以发现,get,set ,remove方法中的逻辑都十分简单,但都涉及到了许多对ThreadLocalMap的操作,实际上,我认为ThreadLoacl的核心就是ThreadLocalMap,ThreadLoacl为每个线程存储的值实际上就是通过 对每个线程对象中的ThreadLocalMap 进行赋值,所以即使在多线程环境下,我们对同一个ThreadLoacl对象进行set,get操作,实际上操作的是每个线程下不同的ThreadLocalMap对象,所以不存在线程安全问题。所以我们重点关注一下ThreadLocalMap 。
Thread
每一个线程对象中都要一个ThreadLocalMap成员变量,初始值为null,其初始化由ThreadLocal完成
// 这个map通过ThreadLocal类,保留了与此线程相关的ThreadLocal值。
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocalMap 简单介绍
ThreadLocalMap 是 ThreadLocal 的一个静态成员内部类,且访问权限为默认,并且没有提供任何公有方法,这就意味着除了在ThreadLocal中,其他地方是无法对其进行操作的。另外,它没有实现Map接口,emmm 这就意味它不是一个标准的map了
核心成员 private Entry[] table;
这个Entry是ThreadLocalMap的一个静态内部类,可以简单理解为一个键值对,键始终为ThreadLocal实例对象,值为该ThreadLocal对象存储的值。该类继承了WeakReference(不了解),当key为null时,意味着该key已经无效,意味着这个entry已经可以从table这个数组中移除。
通过继承至父类的get()方法,可以获取key
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
构造方法
需要提出来的是,每个Entry在table数组中的位置的计算方式比较特殊(具体方式看代码),用这种方式,不同的key可能会得出相同的下标i,如何解决这种冲突,参考博文https://blog.csdn.net/xindoo/article/details/88777833#commentBox
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 构造一个初始容量的Entry数组
table = new Entry[INITIAL_CAPACITY];
// 计算出每个元素在数组中的位置
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
// 数组扩容相关,比较常见,不关心
setThreshold(INITIAL_CAPACITY);
}
set方法
private void set(ThreadLocal<?> key, Object value) {
// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
从set方法中我们就可以看出ThreadLocalMap和HashMap,TreeMap的设计不同之处。首先也是对Key求hash值做定位,但当遇到hash冲突的时候,它的选择不是开链,而是调用nextIndex往后移动,直到遇见某个entry为null或者其key和要插入的key一样。同时,插入的过程也会调用replaceStaleEntry对Map做清理,清理过程比较复杂,稍后介绍。插入后,如果size大于阀值,也会对整个map做扩容操作。
remove方法
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
get方法
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
// 理想情况,直接命中
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
// 说明对应entry不存在
if (k == null)
// 如果键为null,说明这个entry已经无效了,该方法用于删除那些无用的entry,不关心具体实现
expungeStaleEntry(i);
else
// 这里说明发生了下标冲突,k != key两个不同的键产生了相同的下标
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
因为刚刚说到ThreadLocalMap处理key冲突的方式是往后移,直到有空闲的位置。这样虽然实现简单,但查的时候问题就来了,首先根据参数key的hash值算出目标entry的位置
1. 如果目标位置entry为null,意味着不存在该键key对应的entry
2. 如果目标位置entry存在,但其键k与作为参数的key不等,这就意味着发生了下标冲突,插入过程中,位置被后移,或者确实从未插入,不存在该键key对应的entry
所以得往后遍历,直到找到或者遍历到某个空Entry。如果你仔细想想可能就会发现问题,如果只是遍历到遇到null,而不是遍历整个tab,可能会漏掉。比如下面这个例子。
| 0 | 1 | 2 | 3 | 5 | 6 | 7 |
| | a | b | c | d | e | |
开始的时候,tab状态是这样的,现在我要插入一个h,其hashcode恰好是1,然而a已经在那了,按插入逻辑,h只能插到7的位置了,插入后如下。
| 0 | 1 | 2 | 3 | 5 | 6 | 7 |
| | a | b | c | d | e | h |
后来,我把c删掉,变成了下面这样。如果我现在想查h,按照上面getEntry的逻辑,是不是遍历到3就停了,所以找不到h了? getEntry的逻辑表面确实是这样,但实际上getEntryAfterMiss、remove、get时都会直接或者间接调用expungeStaleEntry会对表里的数据做整理。expungeStaleEntry()除了利用弱引用的特性对tab中Entry做清理外,还会对之前Hash冲突导致后移的Entry重新安放位置。所以不可能出现下面这种tab排放的。
| 0 | 1 | 2 | 3 | 5 | 6 | 7 |
| | a | b | | d | e | h |
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
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)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
还有set中调用的replaceStaleEntry(),代码很长,其实也是保证key失效的Entry被清理,Hash冲突的key能放回正确的位置。
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
看这么多复杂的代码,最后看个简单的resize(),ThreadLocalMap的resize相较于HashMap的简单多了,就是新建一个长度为当前2倍的tab,然后把当前tab中的每个entry重新计算index再插入新tab。
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (Entry e : oldTab) {
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
使用场景
- 数据库连接
- Cache
- 线程池
参考资料
- ThreadLocal源码
- 简单理解ThreadLocal原理和适用场景,多数据源下ThreadLocal的应用
测试代码
package threadLocal;
/**
* @author LiFeng
* @create 2019-11-05 上午 11:08
* @describe
*/
public class TestLocal {
public static void main(String[] args) {
ThreadLocal<String> t = new ThreadLocal();
String s = t.get();
System.out.println(s);
// 重写initialValue的方法,未set直接调用get也能获得一个初始值
ThreadLocal<String> t2 = new ThreadLocal<String>() {
@Override
protected String initialValue() {
return "lifeng";
}
};
s = t2.get();
System.out.println(s);
t2.remove();
s = t2.get();
System.out.println(s);
t2.set("hello world");
s = t2.get();
System.out.println(s);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread1:" + t.get());
System.out.println("thread1:" + t2.get());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
System.out.println("wait...");
}
}
测试结果如图
因为无法在外部获取thread对象中的ThreadLocalMap,所以我通过debug的方式观察了一下。符合预期
InheritableThreadLocal
InheritableThreadLocal 继承自 ThreadLocal,但是仅仅是重写了3个方法,可以看到并没有什么复杂的操作。它的特性是,子线程可以继承父线程的初始值,那么它是如何实现的呢? 对于后两个方法非常简单,用处也在上文提到过。稍后介绍childValue方法的用途。
其实,这个要联系到Thread类的核心构造方法,这个我在另一篇博文中详细介绍过,这里简单说一下。
对于这个参数,默认为true,仅有一个构造方法可以设定其值为false,
我们平常创建线程时,绝大多数时候是不会调用此方法的,所以inheritThreadLocals这个参数通常为true,它的作用是
简单说,就是利用父线程中的ThreadLocal.ThreadLocalMap inheritableThreadLocals属性来构建自己的inheritableThreadLocals
可以认为这是一次copy操作。这种构建是一次性的,之后父线程修改其inheritableThreadLocals中的值,子线程并不受其影响。
这里我们也看到了childValue方法的调用,在ThreadLocal方法中未被重写时,它是这样的,这是一种典型的模板方法模式,定义
了构造ThreadLocalMap的步骤,但将其中某个步骤交由其子类去具体实现。
对于childValue方法,我们也可以进一步重写,以达到值继承并进行修改的效果。写了一个简单的demo如下。
测试代码:
package threadLocal;
/**
* @author LiFeng
* @create 2019-11-06 上午 10:35
* @describe
*/
public class TestInheritable {
public static void main(String[] args) {
InheritableThreadLocal<String> iht = new InheritableThreadLocal<>();
iht.set("first value");
System.out.println("main get: " + iht.get());
InheritableThreadLocal<String> iht2 = new InheritableThreadLocal<String>() {
@Override
protected String childValue(String parentValue) {
return parentValue + "取得父线程中的值并加以补充修改";
}
};
iht2.set("iht2");
System.out.println("main get: " + iht2.get());
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("thread-01 get: " + iht.get());
System.out.println("thread-01 get: " + iht2.get());
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread-01 get: " + iht.get());
}
}, "thread-01").start();
// 模拟在main线程中将iht中的值进行修改,发现在thread-01线程中并不会受影响
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
iht.set("second value");
System.out.println("main get: " + iht.get());
}
}