ThreadLocal分析

来源:互联网 发布:照片合成软件下载 编辑:程序博客网 时间:2024/05/21 01:48

ThreadLocal分析

1、概述

当访问共享的可变数据时,通常需要使用同步。一种避免使用同步的方式就是不共享数据,这种技术被称为线程封闭。ThreadLocal<T>做到了线程的封闭,通过提供get和set方法,每个线程可以设置变量T的独立的副本,并随后获取。ThreadLocal<T>的实现可以想到一个简单的方案,通过在类中持有线程安全的ConcurrentHashMap<Thread,T>类型的实例变量map来实现,以当前线程Thread作为key,多个线程共享map容器,相关源码如下:
import java.util.Map;import java.util.concurrent.ConcurrentHashMap;public class ThreadLocal<T> {private Map<Thread, T> map = new ConcurrentHashMap<>();public void put(T t) {map.put(Thread.currentThread(), t);}public T get() {return map.get(Thread.currentThread());}}

但JDK下的实现远非如此,JDK的实现着重考虑了以下几点:
a)Local
每个Thread持有各自的实例变量map用以存储T,而不是多个Thread共享一个map,所以不同Thread的T存储在Thread的Local map下,避免并发
b)GC
Thread下的变量副本随着Thread的GC而消亡,另外ThreadLocal变量GC后Thread下实例变量map中当前T也会随着get或set被GC,避免内存泄露


每个Thread下持有一个ThreadLocalMap的实例变量,实例变量ThreadLocalMap以ThreadLocal实例作为key,如下图所示:



了解了基本结构后,开始看源码 。

2、源码分析

从源码注释中看到ThreadLocal实例通常被private static所修饰,多个线程可以共用一个ThreadLocal实例作为key进行存储。

ThreadLocal类中存在以下几个重要的方法:

1)protected T initialValue()

 protected T initialValue() {        return null;   }

被protected修饰,很显然当需要设定一个非null初始值的时候,需要子类去Override该方法,通常ThreadLocal的子类以匿名类的形式出现,如下:

private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){@Overrideprotected Integer initialValue() {// 根据业务设置初始值return Integer.MIN_VALUE;};};
该方法在第一次调用get且未调用过set方法时调用


2)public T get()

public T get() {// 获取当前线程实例Thread t = Thread.currentThread();// 获取ThreadLocalMap实例,该类为ThreadLocal下的内部类ThreadLocalMap map = getMap(t);// 当map不为空时,从map中获取value值if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null)return (T)e.value;}// 设置初始化值并返回return setInitialValue();}
ThreadLocalMap的获取是通过getMap(Thread t),如下:

ThreadLocalMap getMap(Thread t) {// 此处可以看出内部类ThreadLocalMap作为Thread下的实例变量 ThreadLocal.ThreadLocalMap threadLocals = null;return t.threadLocals;}

2.1 先来看map为null的情况,调用setInitialValue(),如下:

private T setInitialValue() {// 获取初始值T value = initialValue();// 通过当前线程实例获取mapThread t = Thread.currentThread();ThreadLocalMap map = getMap(t);// 如果map非空、则设置当前值if (map != null)map.set(this, value);else// 否则创建map,并设置value值createMap(t, value);// 返回初始值return value;}
当map为空时,调用createMap(Thread t, T firstValue)进行初始化,方法如下:

void createMap(Thread t, T firstValue) {// 延迟实例化Thread下的ThreadLocalMap,将当前ThreadLocal实例作为keyt.threadLocals = new ThreadLocalMap(this, firstValue);}
当map非空时,以当前ThreadLocal实例作为key值,设置当前value

2.2 再来看map非空时,以当前ThreadLocal实例作为key值去map中查找,下面看看内部类ThreadLocalMap的数据结构及相关实现,如下:

static class ThreadLocalMap{/**继承WeakReference,将ThreadLocal实例作为弱引用 *因此当ThreadLocal实例引用设置为null时,不影响ThreadLocal实例的GC*/static class Entry extends WeakReference<ThreadLocal> {Object value;Entry(ThreadLocal k, Object v) {super(k);value = v;}}// map初始容量private static final int INITIAL_CAPACITY = 16;// 数组table的length必须为2的n次方,通过特定算法实现不同实现private Entry[] table;// 当前map的大小private int size = 0;// 重新调整数组table的size时的一个阈值private int threshold;... ...}

由源码可以看出ThreadLocalMap通过数组实现,每个Entry都是一个WeakReference实例,引用ThreadLocal实例,所以当ThreadLocal实例引用设置为null时,不影响ThreadLocal实例的GC。而数组中的Entry非链表结构,那么ThreadLocalMap是如何解决hash冲突的呢?一个神奇的数字0x61c88647

public class ThreadLocal<T> {    private final int threadLocalHashCode = nextHashCode();// 类成员    private static AtomicInteger nextHashCode = new AtomicInteger();    /**     * The difference between successively generated hash codes - turns     * implicit sequential thread-local IDs into near-optimally spread     * multiplicative hash values for power-of-two-sized tables.     * 生成的hash值能够均匀的分布在2的n次方的数组tables中     */    private static final int HASH_INCREMENT = 0x61c88647;    private static int nextHashCode() {        return nextHashCode.getAndAdd(HASH_INCREMENT);    }... ...}

每当在JVM下生成一个ThreadLocal实例时,会初始化一个threadLocalHashCode作为当前ThreadLocal实例的唯一标示,而生成这个threadLocalHashCode是通过原子变量nextHashCode当前值 +0x61c88647 (二进制为:01100001110010001000011001000111)生成 。而当调用map的get方法时,如下:

private Entry getEntry(ThreadLocal key) {// threadLocalHashCode = 0//000000000000000000000000000000000//000000000000000000000000000001111// & = 0// threadLocalHashCode = 0x61c88647 + 0//001100001110010001000011001000111//000000000000000000000000000001111// & = 7// threadLocalHashCode = 0x61c88647 + 0x61c88647// 011000011100100010000110010001110// 000000000000000000000000000001111// & = 14int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}

通过将当前ThreadLocal实例的threadLocalHashCode与map中tables.length-1进行与(&)运算,计算出数组索引,该算法能确保计算出的索引值均匀的分布在数组中,实验如下:

import java.util.Arrays;import java.util.concurrent.atomic.AtomicInteger;public class HashTest {private final static int HASH_INCREMENT = 0x61c88647;private static AtomicInteger nextHashCode = new AtomicInteger();public static void main(String[] args) {genHashCode(8);nextHashCode.set(0);genHashCode(16);nextHashCode.set(0);genHashCode(32);}private static void genHashCode(int length) {int[] array = new int[length];for (int i = 0; i < length; i++) {array[i] = nextHashCode() & (length - 1);}System.out.println("排序前:" + Arrays.toString(array));Arrays.sort(array);System.out.println("排序后:" + Arrays.toString(array));}private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}}输出结果如下:===================================================排序前:[0, 7, 6, 5, 4, 3, 2, 1]排序后:[0, 1, 2, 3, 4, 5, 6, 7]排序前:[0, 7, 14, 5, 12, 3, 10, 1, 8, 15, 6, 13, 4, 11, 2, 9]排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]排序前:[0, 7, 14, 21, 28, 3, 10, 17, 24, 31, 6, 13, 20, 27, 2, 9, 16, 23, 30, 5, 12, 19, 26, 1, 8, 15, 22, 29, 4, 11, 18, 25]排序后:[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]

通过计算所得的索引值获取到数组中的Entry,如果当前entry不为null,并且entry中的ThreadLocal为当前实例,则返回;否则进行后续处理getEntryAfterMiss,如下:

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {Entry[] tab = table;int len = tab.length;while (e != null) {ThreadLocal k = e.get();// 再次判断是否为当前ThreadLocal实例if (k == key)return e;// 如果key为null,则删除当前索引处的entryif (k == null)expungeStaleEntry(i);else// 否则获取下一个索引处的entryi = nextIndex(i, len);e = tab[i];}// 如果entry为null 直接返回return null;}

其中删除当前索引处的entry方法expungeStaleEntry(int staleSlot)如下:

private int expungeStaleEntry(int staleSlot) {Entry[] tab = table;int len = tab.length;// 首先删除索引staleSlot下的entrytab[staleSlot].value = null;tab[staleSlot] = null;size--;// 并且继续判断后续索引处的值是否有效Entry e;int i;for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal k = e.get();// 如果索引key为null,删除索引i处的entryif (k == null) {e.value = null;tab[i] = null;size--;} else {// 否则重新计算当前索引int h = k.threadLocalHashCode & (len - 1);// 如果当前索引位置不等于计算的索引,则将当前索引i位置的entry设置为null,并且重新设置索引h处的元素为当前entry eif (h != i) {tab[i] = null;// Unlike Knuth 6.4 Algorithm R, we must scan until// null because multiple entries could have been stale.// 直到索引h处的entry为null,则设置h处的entry为当前entry ewhile (tab[h] != null)h = nextIndex(h, len);tab[h] = e;}}}return i;}

3)public void set(T value)

下面来看set方法,如下:

public void set(T value) {// 同样获取当前线程实例Thread t = Thread.currentThread();// 获取Thread下的ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null)// 如果map非空则设置值map.set(this, value);else// 否则创建mapcreateMap(t, value);}

在此主要分析下map非空时的逻辑,如下:

private void set(ThreadLocal key, Object value) {Entry[] tab = table;int len = tab.length;// 同样计算出当前索引值iint i = key.threadLocalHashCode & (len-1);// 判断当前索引i处是否存在entryfor (Entry e = tab[i] ; e != null ; e = tab[i = nextIndex(i, len)]) {// 如果存在ThreadLocal k = e.get();// 如果当前key等于e.get(),那么设置为当前的Valueif (k == key) {e.value = value;return;}// 如果当前entry的key为null,则替换if (k == null) {replaceStaleEntry(key, value, i);return;}}// 如果当前索引i处的entry为null,则创建entry,并赋值给当前索引tab[i] = new Entry(key, value);int sz = ++size;// 清理部分失效的元素,并判断达到重新调整数组大小的要求if (!cleanSomeSlots(i, sz) && sz >= threshold)// 重新计算rehash();}

3.1 其中当索引i处entry非空,但是key为null时,执行replaceStaleEntry,如下:

private void replaceStaleEntry(ThreadLocal key, Object value, int staleSlot) {Entry[] tab = table;int len = tab.length;Entry e;int slotToExpunge = staleSlot;// 获取当前索引staleSlot之前 且 entry不等于null 且entry的key为null的索引slotToExpungefor (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len))if (e.get() == null)slotToExpunge = i;// 获取当前索引staleSlot之后的下一个索引,如果entry不等于空for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null; i = nextIndex(i, len)) {ThreadLocal k = e.get();// 如果当前索引i下的key等于当前ThreadLocal ,则将当前entry的value设置为入参value,且设置数组索引staleSlot处的值为entryif (k == key) {e.value = value;// 将当前索引staleSlot的旧值赋值给tab[i]tab[i] = tab[staleSlot];// tab[i]下的值赋给当前tab[staleSlottab[staleSlot] = e;// 如果staleSlot前边的索引处不存在无效的元素(前边索引处的entry为null 或者 entry的key不为null)那么将需要清除的索引设置为iif (slotToExpunge == staleSlot)slotToExpunge = i;// 清除失效的索引元素cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);// 返回return;}// 如果索引i下entry的key等于null 并且 // 如果staleSlot前边的索引处不存在无效的元素(前边索引处的entry为null 或者 entry的key不为null)// 那么将需要清除的索引设置为iif (k == null && slotToExpunge == staleSlot)slotToExpunge = i;}// 如果入参中的key没有找到,则创建entrytab[staleSlot].value = null;tab[staleSlot] = new Entry(key, value);// 如果当前还存在失效的entry,那么清除他们if (slotToExpunge != staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}

其中方法expungeStaleEntry(slotToExpunge)已经分析过,作用就是清除索引slotToExpunge下的元素,并且循环判断索引slotToExpunge的下一个索引处是否存在无效的元素,存在则清除,然后该方法返回最后一次执行清理后的索引i,再调用cleanSomeSlots(int i, int n)方法,如下:

private boolean cleanSomeSlots(int i, int n) {boolean removed = false;Entry[] tab = table;int len = tab.length;do {// 获取下一个索引处的entryi = nextIndex(i, len);Entry e = tab[i];// 如果当前entry无效,那么删除当前索引i处的元素,并且n重新恢复到数组的长度lenif (e != null && e.get() == null) {n = len;removed = true;i = expungeStaleEntry(i);}// 当n除以2不等于0} while ( (n >>>= 1) != 0);return removed;}

3.2 如果当前索引i处的entry为null,则创建entry,并赋值给当前索引元素。当没有清理元素,并且当前数组的长度大于resize阈值时进行重新rehash操作,如下:

private void rehash() {// 进行重新rehash之前,先清理一下无效元素expungeStaleEntries();// 到达这个条件、则重新设置数组大小if (size >= threshold - threshold / 4)resize();}

重新rehash之前,再次清理一遍

// 该方法循环便利数组,查找所有无效的元素进行清理private void expungeStaleEntries() {Entry[] tab = table;int len = tab.length;for (int j = 0; j < len; j++) {Entry e = tab[j];if (e != null && e.get() == null)expungeStaleEntry(j);}}

resize方法如下:

private void resize() {Entry[] oldTab = table;int oldLen = oldTab.length;// 新数组变为原来的2倍,保证为2的n次方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();if (k == null) {// 此处如果key无效了,则设置为null,等待GCe.value = null;} else {// 重新计算索引hint h = k.threadLocalHashCode & (newLen - 1);while (newTab[h] != null)h = nextIndex(h, newLen);newTab[h] = e;count++;}}}// 重新设置当前resize阈值setThreshold(newLen);size = count;table = newTab;}

4)public void remove()

remove方法的代码就比较少了,如下:

public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)// 如果当前map非空,执行removem.remove(this);}
调用map下的remove方法,如下:

private void remove(ThreadLocal key) {Entry[] tab = table;int len = tab.length;// 获取当前key的索引int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {// 找到当前key后清理if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}}

注意在线程池中使用ThreadLocal,当业务处理完成时,最好显式的调用remove()方法


3、总结

就不总结了,懒!


原创粉丝点击