Java多线程基础-ThreadLocal
Yuicon opened this issue · comments
我的博客 转载请注明原创出处。
序
在多线程环境下,访问非线程安全的变量时必须进行线程同步,例如使用synchronized
方式访问HashMap
实例。但是同步访问会降低并发性,影响系统性能。这时候就可以用空间换时间,如果我们给每个线程都分配一个独立的变量,就可以用非同步的方式使用非线程安全的变量,我们称这种变量为线程局部变量。
顾名思义,线程局部变量是指每个线程都有一份属于自己独立的变量副本,不会像普通局部变量一样可以被其他线程访问到。Java
并没有提供语言级的线程局部变量,而是在类库里提供了线程局部变量的功能,也就是这次的主角ThreadLocal
类。
ThreadLocal的使用
Java8
版本的ThreadLocal
有上图所示的4个public
方法和一个protected
的方法,第一个方法用于返回初始值,默认是null
。第二个静态方法withInitial(Supplier<? extends S> supplier)
是Java8
版本新添加的,后面三个实例方法则非常的简单。
在Java8
之前,使用ThreadLocal
时想要设置初始值时需要继承ThreadLocal
类覆盖protected T initialValue()
方法才行,例如:
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
在Java8版本可以使用新添加的静态方法withInitial(Supplier<? extends S> supplier)
,非常方便的设置初始值,例如:
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
System.out.println(threadLocal.get());
threadLocal.set(16);
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());
// 同一个线程的输出
0
16
0
Process finished with exit code 0
ThreadLocal的原理
那么ThreadLocal
是怎么实现线程局部变量的功能的呢?其实ThreadLocal
的基本原理并没有十分复杂。ThreadLocal
在内部定义了一个静态类ThreadLocalMap
,ThreadLocalMap
的键为ThreadLocal
对象,ThreadLocalMap
的值就是ThreadLocal
存储的值,不过这个ThreadLocalMap
是在Thread
类里维护的。我们来看一下ThreadLocal
的部分源码:
// ThreadLocal的set方法
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取Map
ThreadLocalMap map = getMap(t);
if (map != null)
// 设置值
map.set(this, value);
else
// 初始化Map
createMap(t, value);
}
// ThreadLocal的createMap方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
// Thread类定义的实例域
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
可以看出ThreadLocal
的核心实现就是ThreadLocalMap
的实现了,ThreadLocalMap
内部声明了一个Entry
类来存储数据:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
ThreadLocalMap
的实现与HashMap
的实现有相似的地方,比如同样是使用数组存储数据和自动扩容,不同的是hash
算法与hash
碰撞后的处理不一样。
// ThreadLocalMap的set方法
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
// 计算在Entry[]中的索引,每个ThreadLocal对象都有一个hash值threadLocalHashCode,每初始化一个ThreadLocal对象,hash值就增加一个固定的大小0x61c88647
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();
}
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
可以看到ThreadLocalMap
把Entry[]数组当成一个圆环。从计算出来的索引位置开始,如果该索引已经有数据了就判断Key
是否相同,相同就更新值。否则就直到找到一个空的位置把值放进去。获取值的时候也类似,从计算出来的索引位置开始一个一个检查Key
是否相同,这样hash
碰撞比较多的话可能性能就不是很好。
ThreadLocal的应用
ThreadLocal
的应用是非常广的,比如Java
工程师非常熟悉的Spring
框架中就使用了ThreadLocal
来把非线程安全的状态性对象封装起来,所以我们可以把绝大部分的Bean
声明为singleton
作用域。我们在编写多线程代码时也可以想想是用同步的方式访问非线程安全的状态性对象比较好,还是使用ThreadLocal
把非线程安全的状态性对象封装起来更好。
后记
本来下定决心准备一周一篇的,结果偷懒了一次后赶上了公司旅游。这一下子摸了两篇,只能后面慢慢补了……ThreadLocal
我很早就看到过了,一直没什么实感,直到在《精通Spring 4.X 企业应用开发实战》看到在Spring中的应用后才发现,我从来没想过为什么Spring
里的Dao
类可以声明为单例作用域……没有举一反三的能力就只能多看书了,活到老学到老。
参考资料:
- 《Java核心技术 卷一》
- 《精通Spring 4.X 企业应用开发实战》