ThreadLocal探析
ThreadLocal是常出现的Java基础面试题,掌握ThreadLocal不仅仅是为了面试,还可以为线程安全提供多一种思路。
是什么
一句话来说,ThreadLocal是用于实现线程本地变量的类,这里的线程本地变量不是指对象分配在栈上,而是指不同的线程中包含的是各自的实例,而非从进程处继承来的变量值。
关键设计
在讲解如何实现线程本地变量之前,我们需要明确一些关键设计。
Thread中的threadLocals成员
Thread类是JDK提供的线程类,用于ThreadPoolExecutor底层实现。在Thread中有如下代码:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
可以看得出这里每个Thread对象都将拥有自己的threadLocals成员变量,这个是实现线程本地变量的基础,说白了就是线程自己的变量存在自己的threadLocals中。
ThreadLocalMap类
再来近看一眼ThreadLocalMap这个类,从上面代码其实已经可以看出,它是ThreadLocal的一个内部类:
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;
//省略其他代码
......
}
这里可以看出ThreadLocal的基本结构,一个Entry类型的数组用于存储具体数据。Entry本身是一种弱引用,它引用的对象为ThreadLocal变量,并拓展了value成员变量用于存放所谓的线程本地对象。
如何协作
有了上面的知识,我们离真相也就进了一步, 但是Thread的threadLocals成员变量究竟是如何去存取值的呢?这里就要请出本厂的主角ThreadLocal类本尊了。
ThreadLocal的set方法
剪短来说,其实只要关注ThreadLocal的get和set方法就能找到入口了:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public void set(T value) {
/*
这里是关键,ThreadLocal的set方法其实就是根据这里实现了线程本地特性的,
getMap方法会返回当前线程的ThreadLocalMap成员,即threadLocals用于存放
本地变量
*/
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//存入变量。map不存在则创建,这里值得注意的是,map的key不会为null
//key的本身是当前ThreadLocal对象
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap的set方法
再进一步看一下ThreadLocalMap的set方法:
private void set(ThreadLocal<?> key, Object value) {
//ThreadLocalMap本质是Entry数组
Entry[] tab = table;
//计算下标位置用于存放Entry,这里注意下标i与Entry数组长度有关
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//如果下标位置为非空Entry对象,则进入循环
for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
//获取Entry包含的弱引用
ThreadLocal<?> k = e.get();
//弱引用存在,且变量与要设置的值一致,直接返回
if (k == key) {
e.value = value;
return;
}
//弱引用不存在,说明旧值已被回收,换为新值
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果为经过循环,说明本位置Entry值为null,直接设置为要存的值
tab[i] = new Entry(key, value);
int sz = ++size;
//之前提到下标和数组长度有关,这是此处rehash的原因
//rehash的部分不再此处说明
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocal的get方法
public T get() {
//熟悉的操作
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//之前set方法中说过,key是当前ThreadLocal本身,所以用this做key获取到值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//map未初始化,则初始化之,将值设置为初始默认值,并返回该值
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
//这个方法时protected的,使用ThreadLocal时需要覆写
protected T initialValue() {
return null;
}
实例演示
有了之前的一些知识,我们可以来上代码了。还是从逃不过的DateFormat开始
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
public class DateTimeUtil {
//Java 8特性加持写法
private static ThreadLocal<SimpleDateFormat> sdf = ThreadLocal.withInitial(
()->{
System.out.println(Thread.currentThread().getName() + " trigger initialValue");
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
);
//第二种写法
private static ThreadLocal<SimpleDateFormat> sdf2= new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue(){
System.out.println(Thread.currentThread().getName() + " trigger initialValue");
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static Date parse(String dateStr) throws ParseException {
//打印当前线程以及线程中的值可知每个线程不一样
System.out.println(Thread.currentThread().getName() + " is calling parse!" + dateStr + " ref:" + System.identityHashCode(sdf.get()));
return sdf.get().parse(dateStr);
}
}
如果使用两个线程来调用parse方法可以明显看到结果不同,大致如下
thread 1 trigger initialValue
thread 1 is calling parse!1990-1-1 00:00:00 ref:218991134
thread 2 trigger initialValue
thread 2 is calling parse!2019-8-1 16:04:02 ref:131510706
可以从ref看出两者的SimpleDateFormat不是同一个。
如果用线程池,让一个线程多调用几次又会如何呢?我创建了一个核心线程和最大线程数均为1的线程池,反复调用,结果如下:
pool-1-thread-1 trigger initialValue
pool-1-thread-1 is calling parse!2019-8-1 16:04:02 ref:848068023
pool-1-thread-1 is calling parse!2019-8-1 16:07:31 ref:848068023
...
pool-1-thread-1 is calling parse!2019-8-1 16:07:31 ref:848068023
可知该ThreadLocal不断在复用,这有什么好处呢?
这里之所以选用SimpleDateFormat来讲,其实是因为他是线程不安全的,笔者之前就掉过坑,具体的原因很多文章有说,这里就不再详述,既然线程不安全,那么就不能再多线程中使用,那该咋办,最简单的方法可以想得到:
public static Date parse(String dateStr) throws ParseException {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateStr);
}
上面是一种方法,每次new一个。但是在方法被高频调用的情况下,对象的创建和销毁成本就会增加,对内存你的消耗也会上升。这个时候再回想ThreadLocal,我们会发现,ThreadLocal既解决了线程安全问题,也能保证对象创建和销毁成本较低,一举两得,毕竟仅在初次get的时候回调用setInitialValue方法初始化对象。现在再回头看,是否能理解我再最初说的为ThreadLocal为线程安全问题提供了多一种思路呢?毕竟对象时自己的,不是混用了,自然就没有数据竞争问题。