java-ThreadLocal
ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题。
考虑一个问题:线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作
synchronzed或者lock控制线程对临界区资源的同步顺序从而解决线程安全的问题。
ThreadLocal:表示线程的“本地变量”,即每个线程都拥有该变量副本,每个线程都使用自己的“共享资源”,这就是一种“空间换时间”的方案。
ThreadLocal的内部结构
下图是ThreadLocal的内部结构,同时也简单的给出来Thread对它的引用。
通过图以及源码 ThreadLocal内部是基于一个ThreadLocalMap来实现,而ThreadLocalMap内部又是一个Entry的数据结构。Entry的数据结构是基于弱引用来使用。源码结构如下
public class ThreadLocal {
static class ThreadLocalMap {
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
...
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
...
}
ThreadLocal的数据结构分析
ThreadLocalMap详解
ThreadLocal内部定义了ThreadLocalMap静态内部类,数据真正存放在ThreadLocalMap当中,所以threadLocal的get,set和remove方法实际上具体是通过threadLocalMap的getEntry,set和remove方法实现的。
Entry详解
ThreadLocalMap是threadLocal一个静态内部类,和大多数容器一样内部维护了一个数组,同样的threadLocalMap内部维护了一个Entry类型的table数组。
Entry是一个以ThreadLocal为key,Object为value的键值对,另外需要注意的是这里的threadLocal是弱引用,因为Entry继承了WeakReference,在Entry的构造方法中,调用了super(k)方法就会将threadLocal实例包装成一个WeakReferenece。
threadLocal内存泄漏问题
每个线程实例中可以通过threadLocals获取到threadLocalMap,而threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry数组。当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放。需要注意的是Entry中的key是弱引用,当threadLocal外部强引用被置为null(threadLocalInstance=null
),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。在实际开发中,会使用线程池去维护线程的创建和复用,比如固定大小的线程池,线程为了复用是不会主动结束的,所以,threadLocal的内存泄漏问题
ThreadLocal的数据操作分析
ThreadLocal的数据结构与内部组件。通过提供的核心api了解这些内部组件是如何进行数据存储。
T get()
get方法是获取当前线程中threadLocal变量的值,具体步骤如下
- 获取当前线程的实例对象
- 获取当前线程的threadLocalMap
- if -> map != null 获取map中当前threadLocal实例为key的值的entry
- 当前entitiy不为null的话,就返回相应的值value
- 若map为null或者entry为null的话通过setInitialValue方法初始化,并返回该方法返回的value
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();
}
threadLocal 实线程隔离,其实就是Thread内部自己维护ThreadLocalMap的一个成员变量,getMap(t);方法也是证明的这个理论。
public class Thread implements Runnable {
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
setInitialValue 看下setInitialValue主要做了些什么事情?
逻辑和set方法几乎一致,另外值得关注的是initialValue方法:protected修饰的也就是说继承ThreadLocal的子类可重写该方法,实现赋值为其他的初始值
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 T initialValue() {
return null;
}
get()总结:
- 通过当前线程thread实例获取到它所维护的threadLocalMap,
- 然后以当前threadLocal实例为key获取该map中的键值对(Entry),若Entry不为null则返回Entry的value。
- 如果获取threadLocalMap为null或者Entry为null的话,就以当前threadLocal为Key,value为null存入map后,并返回null。
void set(T value)
set方法设置在当前线程中threadLocal变量的值.方法的逻辑很简单,数据value是真正的存放在了ThreadLocalMap这个容器中了,并且是以当前threadLocal实例为key
-
- 获取当前线程实例对象;
-
- 通过当前线程实例获取到ThreadLocalMap对象;
-
- 如果Map不为null,则以当前threadLocl实例为key,值为value进行存入;
- 4.map为null,则新建ThreadLocalMap并存入value
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
分析
线程使用 ThreadLocalMap 来存储每个线程副本变量,它是 ThreadLocal 里的一个静态内部类。ThreadLocalMap 也是采用的散列表(Hash)思想来实现的,但是实现方式和 HashMap 不太一样。
补充:散列表1
理想状态下,散列表就是一个包含关键字的固定大小的数组,通过使用散列函数,将关键字映射到数组的不同位置。
在理想状态下,哈希函数可以将关键字均匀的分散到数组的不同位置,不会出现两个关键字散列值相同(假设关键字数量小于数组的大小)的情况。但是在实际使用中,经常会出现多个关键字散列值相同的情况(被映射到数组的同一个位置),我们将这种情况称为散列冲突。为了解决散列冲突,主要采用下面两种方式:
分离链表法(separate chaining)
开放定址法(open addressing)
补充:分离链表法
- 分散链表法使用链表解决冲突,将散列值相同的元素都保存到一个链表中。当查询的时候,首先找到元素所在的链表,然后遍历链表查找对应的元素。
- index key = hash_key % table.size;
补充:开放定址法
- 开放定址法不会创建链表,当关键字散列到的数组单元已经被另外一个关键字占用的时候,就会尝试在数组中寻找其他的单元,直到找到一个空的单元。探测数组空单元的方式有很多,这里介绍一种最简单的 -- 线性探测法。线性探测法就是从冲突的数组单元开始,依次往后搜索空单元,如果到数组尾部,再从头开始搜索(环形查找);
- 如下图:
ThreadLocalMap 中使用开放地址法来处理散列冲突,而 HashMap 中使用的分离链表法。之所以采用不同的方式主要是因为:在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突。并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。
看一下set方法。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;
//根据threadLocal的hashCode确定Entry应该存放的位置
int i = key.threadLocalHashCode & (len-1);
//采用开放地址法,hash冲突的时候使用线性探测
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal> k = e.get();
//覆盖旧Entry
if (k == key) {
e.value = value;
return;
}
//当key为null时,说明threadLocal强引用已经被释放掉,那么就无法
//再通过这个key获取threadLocalMap中对应的entry,这里就存在内存泄漏的可能性
if (k == null) {
//用当前插入的值替换掉这个key为null的“脏”entry
replaceStaleEntry(key, value, i);
return;
}
}
//新建entry并插入table中i处
tab[i] = new Entry(key, value);
int sz = ++size;
//插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
threadLocal的hashcode?
从源码中我们可以清楚的看到threadLocal实例的hashCode是通过nextHashCode()方法实现的,该方法实际上总是用一个AtomicInteger加上0x61c88647来实现的。0x61c88647这个数是有特殊意义的,它能够保证hash表的每个散列桶能够均匀的分布,这是Fibonacci Hashing
,也正是能够均匀分布,所以threadLocal选择使用开放地址法来解决hash冲突的问题。
怎样确定新值插入到哈希表中的位置?
key.threadLocalHashCode & (len-1)
,同hashMap和ConcurrentHashMap等容器的方式一样,利用当前key(即threadLocal实例)的hashcode与哈希表大小相与,因为哈希表大小总是为2的幂次方,所以相与等同于一个取模的过程,这样就可以通过Key分配到具体的哈希桶中去。而至于为什么取模要通过位与运算的原因就是位运算的执行效率远远高于了取模运算。
怎样解决hash冲突?
源码中通过nextIndex(i, len)
方法解决hash冲突的问题,该方法为((i + 1 < len) ? i + 1 : 0);
,也就是不断往后线性探测,当到哈希表末尾的时候再从0开始,成环形。
怎样解决“脏”Entry?
在set方法的for循环中寻找和当前Key相同的可覆盖entry的过程中通过replaceStaleEntry方法解决脏entry的问题。如果当前table[i]为null的话,直接插入新entry后也会通过cleanSomeSlots来解决脏entry的问题。
如何进行扩容?
private int threshold; // Default to 0
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
第一次为threadLocal进行赋值的时候会创建初始大小为16的threadLocalMap,并且通过setThreshold方法设置threshold,其值为当前哈希数组长度乘以(2/3),也就是说加载因子为2/3(加载因子是衡量哈希表密集程度的一个参数,如果加载因子越大的话,说明哈希表被装载的越多,出现hash冲突的可能性越大,反之,则被装载的越少,出现hash冲突的可能性越小。同时如果过小,很显然内存使用率不高,该值取值应该考虑到内存使用率和hash冲突概率的一个平衡,如hashMap,concurrentHashMap的加载因子都为0.75)。这里threadLocalMap初始大小为16,加载因子为2/3,所以哈希表可用大小为:16*2/3=10,即哈希表可用容量为10。
从set方法中可以看出当hash表的size大于threshold的时候,会通过resize方法进行扩容
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
//新数组为原数组的2倍
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal> k = e.get();
//遍历过程中如果遇到脏entry的话直接另value为null,有助于value能够被回收
if (k == null) {
e.value = null; // Help the GC
} else {
//重新确定entry在新数组的位置,然后进行插入
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
//设置新哈希表的threshHold和size属性
setThreshold(newLen);
size = count;
table = newTab;
}
新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的entry并将其插入到新的hash数组中,主要注意的是,在扩容的过程中针对脏entry的话会令value为null,以便能够被垃圾回收器能够回收,解决隐藏的内存泄漏的问题。
set()总结:
- 通过当前线程对象thread获取该thread所维护的threadLocalMap;
- 若threadLocalMap不为null,则以threadLocal实例为key,值为value的键值对存入threadLocalMap;
- 若threadLocalMap为null的话,就新建threadLocalMap然后在以threadLocal为键,值为value的键值对存入即可。
remove
从map中删除数据,先获取与当前线程相关联的threadLocalMap然后从map中删除该threadLocal实例为key的键值对即可。
-
- 获取当前线程的threadLocalMap;
-
- 从map中删除以当前threadLocal实例为key的键值对.
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}