线程本地变量——ThreadLocal
ThreadLocal,即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。
可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,多线程环境下可以有效防止自己的变量被其他线程篡改。
ThreadLocal的使用场景
SimpleDateFormat线程不安全的场景
我们都知道SimpleDateFormat在多线程场景下是线程不安全的,因为我们在将SimpleDataFormat设置为静态变量时,在多线程场景下,会共用SimpleDateFormat中的Calendar对象:
public abstract class DateFormat extends Format {
// DateFormat中的Calendar对象
protected Calendar calendar;
而SimpleDateFormat对日期的转换就是通过这个Calendar对象来操作的,而且这个calendar这个成员变量即被用于format方法也被用于parse方法,下面是parse中的方法片段:
// ...
try {
parsedDate = calb.establish(calendar).getTime();
// ...
}
// CalendarBuilder中的establish方法
Calendar establish(Calendar cal) {
// ...
cal.clear();
// ...
}
这里我们可以看到parse()方法中调用了CalendarBuilder中的establish()方法,而该方法中有一步针对calendar的清除操作——calendar.clear()。如果这时有另一个线程进入到parse方法用到了SimpleDateFormat对象中的calendar,就会产生线程安全问题。
同样的,SimpleDateFormat中的format()方法调用了Calendar的setTime()方法,同样可能会在多线程场景下出现问题。
解决方案
这里使用ThreadLocal创建线程独有的SimpleDateFormat,来避免日期工具在多线程环境下的不安全问题:
public class DateUtilSafe {
private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = ThreadLocal.withInitial(() ->
new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
);
private String parse(Date date) {
return THREAD_LOCAL.get().format(date);
}
}
Spring中ThreadLocal实现事务隔离
Spring采用ThreadLocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。代码如下:
public abstract class TransactionSynchronizationManager {
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly =
new NamedThreadLocal<>("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
new NamedThreadLocal<>("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive =
new NamedThreadLocal<>("Actual transaction active");
//...
}
其他场景
比如项目中存在一个线程横跨多个方法调用,时常需要传递一些对象,如用户身份信息等,就会存在过渡传参的问题。这时,就可以使用ThreadLocal去改造,即调用前ThreadLocal设置参数,其他地方使用get()方法获取。
ThreadLocal实现原理
ThreadLocal主要使用set(T value)方法设置线程本地变量,然后通过get()方法获取,最后通过remove()方法清除,接下来我们来一起看一下这几个方法:
设置变量——set(T value)
我们先来看一下ThreadLocal的set(T value)方法:
public void set(T value) {
Thread t = Thread.currentThread();// 当前线程
ThreadLocalMap map = getMap(t);// 获取ThreadLocalMap对象
if (map != null)// 校验对象是否为空
map.set(this, value); // 不为空设置值
else
createMap(t, value);// 为空创建一个map
}
// 返回线程独有的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public class Thread implements Runnable {
...
// Thread中的ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
set()方法中需要注意的是通过getMap(t)获取的Map其实是Thread中的threadLocals,也就是线程独有的一个ThreadLocalMap,也就是说,每个线程自己维护了自己的ThreadLocalMap。
ThreadLocalMap
ThreadLocalMap是ThreadLocal中的一个静态内部类,其中维护了一个Entry:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
// Entry的值
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
...
private Entry[] table;
...
}
这里我们可以看到ThreadLocalMap内部的Entry继承了WeakReference(弱引用),目的是使得该Entry在仅有弱引用关联时,方便被垃圾回收器回收。
通过观察ThreadLocalMap的结构我们可以发现,ThreadLocalMap本质上是一个Entry数组,每一个Entry的key就是ThreadLocal的弱引用,value就是我们要设置的变量值。结构如下
ThreadLocalMap如何解决hash冲突?
我们一起来看一下ThreadLocalMap中的set方法:
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;// 获取当前Entry数组的长度
// 长度-1与运算
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();
}
// ThreadLocal中的threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();
// ThreadLocal中的nextHashCode()方法
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
从源码中可以看到,在set方法中:
-
首先会使用ThreadLocal对象的hash值,定位table的索引位置i,int i = key.threadLocalHashCode & (len-1)。
-
接着进入一个for循环,判断i索引位置的Entry是否为null,若e不为null,则进入循环
-
在for循环中会判断该Entry的key和当前ThreadLocal是否相等,相等则直接修改value
if (k == key) { e.value = value; return; }
-
若当前位置Entry的key为null,就初始化一个Entry放在当前索引位置。由replaceStaleEntry(key, value, i)中的逻辑我们可以看到,该方法会通过tab[staleSlot].value = null将原value值置为null,并设置新的Entry。其实平时在使用结束后,由于Key是WeakReference的,会被优先回收,若不及时remove(),则可能会造成内存泄漏的问题,所以在开发中我们要尽量避免这种情况。
if (k == null) { replaceStaleEntry(key, value, i); return; } private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { ... tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); ... }
获取变量——get()
这里我们可以看到get()方法实际上是调用了ThreadLocalMap的getEntry方法:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
有了前面set源码的经验,再看这里就很好懂了,大致流程是:根据ThreadLocal对象的Hash值,定位table的索引位置,若key和get的key一致,就返回该位置的value,如果不一致就进入getEntryAfterMiss()方法判断后续位置。
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;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}