线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,而大多数解决线程安全问题的方法都是通过加锁的方式,让同一时间只有一个线程能过访问到共享资源。这篇文章介绍另种解决线程安全的思路——ThreadLocal:
线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,大多数解决线程安全问题的方法都是通过加锁的方式,让同一时间只有一个线程能过访问到共享资源。
ThreadLocal提供了另一种解决思路,让每个线程拥有自己私有的内存空间,将线程私有的数据存入这个私有空间内,线程与线程之间相互隔离,这样就不会有线程安全问题。
每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。
threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。
ThreadLocal核心方法:
get():返回当前线程副本中该ThreadLocal对应的值。
initialValue():返回当前线程副本中的该ThreadLocal对应对应的“初始值”。
remove():移除当前线程副本中该ThreadLocal对应的值。
set(T value):当前线程副本中该ThreadLocal对应的值为value。
每个线程保存一个私有的int值count,5个线程count从0加到10,线程之间互不影响。
/**
* 每个线程保存一个私有的int值count
* 5个线程count从0加到10,线程之间互不影响
*/
public class ThreadLocalDemo {
private static ThreadLocal<Integer> countLocal = new ThreadLocal<Integer>(){
public Integer initialValue() {
return 0;
}
};
public static void main(String[] args){
for (int i = 1; i <= 5; i++) {
new Thread("Thread_" + i) {
public void run() {
for (int j = 1; j <= 10; j++) {
countLocal.set(countLocal.get() + 1);
System.out.println(getName() + ": count=" + countLocal.get());
}
};
}.start();
}
}
}
输出结果如下:
Thread_3: count=1
Thread_5: count=1
Thread_5: count=2
Thread_4: count=1
Thread_4: count=2
Thread_2: count=1
Thread_2: count=2
Thread_2: count=3
Thread_2: count=4
Thread_1: count=1
Thread_2: count=5
Thread_4: count=3
Thread_5: count=3
Thread_5: count=4
Thread_3: count=2
Thread_5: count=5
Thread_5: count=6
Thread_5: count=7
Thread_5: count=8
Thread_4: count=4
Thread_4: count=5
Thread_4: count=6
Thread_2: count=6
Thread_2: count=7
Thread_2: count=8
Thread_2: count=9
Thread_1: count=2
Thread_2: count=10
Thread_4: count=7
Thread_5: count=9
Thread_3: count=3
Thread_3: count=4
Thread_5: count=10
Thread_4: count=8
Thread_1: count=3
Thread_4: count=9
Thread_3: count=5
Thread_4: count=10
Thread_1: count=4
Thread_3: count=6
Thread_1: count=5
Thread_3: count=7
Thread_1: count=6
Thread_3: count=8
Thread_1: count=7
Thread_3: count=9
Thread_1: count=8
Thread_3: count=10
Thread_1: count=9
Thread_1: count=10
可以看到,即使5个线程并发执行,但是每个线程内部的count都是按1-10的顺序相加的。
每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。
threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。
/**
* 线程局部变量threadLocals为ThreadLocal.ThreadLocalMap类型
*/
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}
/**
* ThreadLocal$ThreadLocalMap 散列表结构
* key=ThreadLocal value=Object
*/
static class ThreadLocalMap {
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
在理解的ThreadLocal的存储结构之后,再看get()和set()方法就很简单了。
public T get() {
// 获取当前线程私有的map thread.threadLocals
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 获取map的value值
if (map != null) {
// map的key是ThreadLocal类型
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果map=null,初始化map,下文有讲解
return setInitialValue();
}
/**
* 返回ThreadLocalMap类型的thread.threadLocals
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* 初始化value,map=null时会初始化map
*/
private T setInitialValue() {
T value = initialValue();// 初始值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);// 将初始值存入map
else
createMap(t, value);// map=null时初始化map
return value;
}
/**
* 返回map中value的初始值
* 默认为null,一般需要重写该方法以获得非null值
*/
protected T initialValue() {
return null;
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
线性探测解决Hash冲突:根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。
ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是采用线性探测的方式,所谓ThreadLocalMap解决Hash冲突的方式就是简单的步长加1或减1,寻找下一个相邻的位置。
如下,ThreadLocalMap.set()方法:
private void set(ThreadLocal<?> key, Object value) {
// 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
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();
// key 存在,直接覆盖
if (k == key) {
e.value = value;
return;
}
/*
* key=null而value!=null,因为key是弱引用
* 用新的key-value将旧的null-value替换掉
*/
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
// 清除陈旧的Entry(key == null)
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以每个线程最好只存ThreadLocal。
ThreadLocal使用中会有内存泄露问题。
ThreadLocalMap的key是弱引用,而Value是强引用。源码如下:
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
ThreadLocalMap的key是弱引用,发生GC时弱引用key会被回收;而value是强引用,GC时不会被回收。
所以ThreadLocalMap中就会出现key为null的Entry,因为key为null,这Entry是不能被访问到的。如果当前线程一直没结束的话,一直有这个引用链:Thread --引用--> ThreaLocalMap --引用--> Entry --引用--> value,这个value就无法被回收,导致内存泄露。
解决:
同步机制是通过控制线程访问共享对象的顺序,类似“时间换空间”,同一时刻共享对象只能被一个线程访问造成整体上响应时间增加,但是对象只占有一份内存。
而ThreadLocal是为每一个线程分配一个该对象,各用各的互不影响。类似“空间换时间”,为每个线程都分配了一份对象,自然而然内存使用率增加,但整体上时间效率要增加很多。
每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间。
threadLocals是一个key-value的map结构,key是ThreadLoacal对象,value就是线程需要保存的私有数据。
ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以每个线程最好只存ThreadLocal。
由于ThreadLocalMap的key是弱引用,ThreadLocal使用中会有内存泄露问题。在使用完ThreadLocal之后调用remove方法删除值,可避免内存泄露问题。