ThreadLocal学习
< 返回列表时间: 2020-06-03来源:OSCHINA
public class TestThreadlocal { private static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){ @Override protected Integer initialValue() { return 1; } }; public static void main(String[] args) { for (int j = 0; j < 5; j++) { new Thread(() -> { Integer i = local.get(); local.set(i+=5); System.out.println(Thread.currentThread().getName() + "-" + local.get()); }).start(); } } }
一、ThreadLocal结构

二、内存模型
这里table数组的每个元素存放一个entry,一个线程中可能有多个ThreadLocal,就会有多个entry
ThreadLocal-ref是一个弱引用,如果ThreadLocal-ref = null时,堆里的threadLocal就会被GC回收,此时key的指向就是一个null。 value是强引用,此时如果thread-ref(线程池等场景)一直存在,则value的是不会清空,这样就会产生脏数据,造成内存泄漏。为了解决这种情况,在set的时候会清理部分脏数据。
三、set方法
1.set public void set(T value) { //1.获取当前线程 Thread t = Thread.currentThread(); //2.根据当前线程获取到一个ThreadLocalMap ThreadLocalMap map = getMap(t); //3.如果ThreadLocalMap不为空,则取值,否则创建一个ThreadLocalMap if (map != null) map.set(this, value); else createMap(t, value); }
2.createMap(t, value) 逻辑 初始化数组table,里面存放的是entry实力,默认16个长度 通过当前ThreadLocal对象的hash值 与上 15 计算出一个数组下标(hash值使用斐波拉契散列值保证key不重复,避免hash碰撞) 创建一个entry,key = ThreadLocal实例,value = set方法传过来的值。并发entry实力放到table数组对应的下标中。 图示
代码 //createMap void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } //new ThreadLocalMap(this, firstValue); 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); } //firstKey.threadLocalHashCode private final int threadLocalHashCode = nextHashCode(); private static final int HASH_INCREMENT = 0x61c88647; /** * Returns the next hash code. */ private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } //new Entry(firstKey, firstValue) static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
3.map.set(this, value) 逻辑 同样的算法计算出table数组的下标 从当前下标开始向右探测(线性探测(开放式寻址,防止hash冲突)),此时在i位置有四种情况 若table[i].key != this & table[i].key != null,则继续向右 若table[i].key = this ,则直接填充值value 若table[i].key = null,则替换脏entry(该情况是因为threadLocal被GC释放掉了) replaceStaleEntry(key, value, i) 若table[i]= null,则直接创建一个新的entry后填充到 i 位置 图示
代码 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; int i = key.threadLocalHashCode & (len-1); //1.向后寻址,找到!= null的entry for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //1.1.找到正确的填充位置,替换值 if (k == key) { e.value = value; return; } //1.2.key=null,则认为附近很大的可能有其他的脏数据,所以进行寻址清除 //key = null,说明被GC回收了,这个时候在做填值的动作的时候还做了一次替换,主要为了填入重复hash的entry的问题,具体详见后面的图 if (k == null) { replaceStaleEntry(key, value, i); return; } } //2.向后寻址找个一个null的节点 tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
4.replaceStaleEntry(key, value, i)
​ 这里,说明第一次寻址,走到table[i].key = null的逻辑,【i】位置已经是一个脏节点 逻辑 从【i-1】向左寻址 entry != null & entry.key != this,继续 entry != null & entry.key = null,标记slotToExpunge = i(待清除的位置) entry == null 结束
**备注:**向前寻址的作用是,当第一次寻址时发现脏节点,这个脏节点的附近很大可能还存在其他的脏节点,所有向前寻址,找出附近的脏节点(即entry != null & entry.key = null的节点),遇到entry = null结束寻址。 从【i+1】向右寻址 entry != null & entry.key != this,继续 entry != null & entry.key = this,填充值,并交换staleSlot 和 当前位置的entry。(一) 交互的目的:使有效节点左移 交换的原因:寻址方式为线性探测,防止下次set方法寻址时提前遇到null直接填充entry导致key相同的问题 同样这里也增加判断if (slotToExpunge == staleSlot) slotToExpunge = i; 清除脏节点 entry != null & entry.key = null。(二) 这里只是有可能后移清除标记:if (slotToExpunge == staleSlot) slotToExpunge = i; entry = null,直接填充new Entry(key, value)。(三) 最后清除脏节点 图示
向左寻址

向右寻址

(一)

(三)

​ 代码 private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; // Back up to check for prior stale entry in current run. // We clean out whole runs at a time to avoid continual // incremental rehashing due to garbage collector freeing // up refs in bunches (i.e., whenever the collector runs). int slotToExpunge = staleSlot; for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // Find either the key or trailing null slot of run, whichever // occurs first for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); // If we find key, then we need to swap it // with the stale entry to maintain hash table order. // The newly stale slot, or any other stale slot // encountered above it, can then be sent to expungeStaleEntry // to remove or rehash all of the other entries in run. if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; // Start expunge at preceding stale entry if it exists if (slotToExpunge == staleSlot) slotToExpunge = i; cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // If we didn't find stale entry on backward scan, the // first stale entry seen while scanning for key is the // first still present in the run. if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // If key not found, put new entry in stale slot tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // If there are any other stale entries in run, expunge them if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }
四、get方法
线性探测 写入:找到发生冲突最近的空闲单元 查找:从发生冲突的位置,向后查找
热门排行