1. 简介2. 基本使用3. ThreadLocal原理说明4. ThreadLocal源码解析
4.1 成员变量4.2 构造方法4.3 成员方法
4.3.1 get()及相关的方法4.3.2 set(T value)4.3.3 remove()方法 5. ThreadLocalMap源码解析
5.1 成员变量5.2 内部类5.3 构造方法5.4 成员方法
5.4.1 nextIndex(int i, int len)5.4.2 prevIndex(int i, int len)5.4.3 getEntry(ThreadLocal> key)5.4.4 getEntryAfterMiss(ThreadLocal> key, int i, Entry e)方法5.4.5 expungeStaleEntry(int staleSlot)方法5.4.6 set(ThreadLocal> key, Object value)方法5.4.7 replaceStaleEntry(ThreadLocal> key, Object value,int staleSlot)方法5.4.8 cleanSomeSlots(int i, int n)方法5.4.9 rehash()方法5.4.10 resize()方法5.4.11 remove(ThreadLocal> key)方法
1. 简介ThreadLocal用于提供线程独有的变量,只有当前自身线程可以访问,而其他线程不能访问,极大的方便了一些逻辑的实现。
ThreadLocal(不同的线程有不同的数据,不存在并发问题)和Synchronized都可以解决并发问题,但是ThreadLocal是用于数据隔离的,而Synchronized是用于数据共享的。
2. 基本使用package ThreadLocal; public class Demo01 { static ThreadLocalthreadLocal = new ThreadLocal<>(); // 获取当前线程的值 public static void getThread(){ System.out.println(threadLocal.get()); threadLocal.remove(); } // 设置不同线程的值 public static void setThread(){ threadLocal.set(Thread.currentThread().getName()); } public static void main(String[] args) { new Thread(() -> { Demo01.setThread(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } Demo01.getThread(); }, "A").start(); new Thread(() -> { Demo01.setThread(); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } Demo01.getThread(); }, "B").start(); } }
结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CZp95M1H-1642907859817)(结果.png)]
从上面的结果确实可以看出ThreadLocal实现了线程之间数据的隔离。接下来我们就来说说ThreadLocal是怎样实现的吧!
3. ThreadLocal原理说明每个Thread内部都有一个Map(也就是下面要讲的ThreadLocalMap),每当调用ThreadLocal对象的get()方法时,相当于往Thread的ThreadLocalMap里面传入了一个键值对(key-value),key就是ThreadLocal对象,value就是我们要存储在线程中的值。
4. ThreadLocal源码解析 4.1 成员变量// 这个是ThreadLocal对象的哈希值,通过调用nextHashCode()方法获取 private final int threadLocalHashCode = nextHashCode(); // 存储哈希值的变量,属性是AtomicInteger原子类,是一个静态变量 private static AtomicInteger nextHashCode = new AtomicInteger(); // 每一次nextHashCode增加的数值大小,表示哈希值的增量 // 每创建一个ThreadLocal对象,nextHashCode就会增长HASH_INCREMENT // HASH_INCREMENT是一个黄金分割数,哈希增量为这个值 // 可以使Map中的数分布均匀 private static final int HASH_INCREMENT = 0x61c88647; // 获取哈希值 private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } // 初始化一个起始value,,一般这个方法都是会重写的 protected T initialValue() { return null; }4.2 构造方法
// 只有一个空参构造方法 public ThreadLocal() { }4.3 成员方法 4.3.1 get()及相关的方法
获取当前线程内部ThreadLocalMap中key为ThreadLocal对象的value。
public T get() { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程内部的ThreadLocalMap ThreadLocalMap map = getMap(t); // 如果当前线程已经初始化了自己的ThreadLocalMap对象,就尝试在里面寻找值 if (map != null) { // 获取ThreadLocalMap中key为ThreadLocal对象的键值对 ThreadLocalMap.Entry e = map.getEntry(this); // 如果键值对不等于null,直接返回值 if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // 执行到这里有几种情况 // 1. 当前线程没有初始化过自己的ThreadLocalMap对象 // 2. 当前线程初始化了自己的ThreadLocalMap对象,但是没有生成当前线程与ThreadLocal相关联的变量 // setInitialValue() 会先检查当前Thread对象是否初始化了自己的ThreadLocalMap,如果没有初始化 // 就调用createMap(t, value)初始化并在createMap方法内部添加线程与ThreadLoca相关联的值 // 如果已经初始化,就调用ThreadLocalMap对象的set方法设置值 return setInitialValue(); } private T setInitialValue() { // 获取当前初始化的值 T value = initialValue(); // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的ThreadLocalMap对象 ThreadLocalMap map = getMap(t); // 如果已经初始化,就调用ThreadLocalMap的set方法往里面设置值 if (map != null) map.set(this, value); else // 如果没有初始化,就调用createMap(t, value)方法在里面抵用ThreadLocalMap的 // 构造方法(构造方法会初始化并设置值) createMap(t, value); // 返回值 return value; } // 获取线程的ThreadLocalMap对象,也就是threadLocals属性 ThreadLocalMap getMap(Thread t) { return t.threadLocals; } // 调用ThreadLocalMap的构造方法创建并在里面设置一个值 void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }4.3.2 set(T value)
设置线程与ThreadLocal对象相关联的值,对于同一个ThreadLocal对象和同一个Thread来说,如果多次调用,会覆盖掉以前的值,只会保留最后一次的值。
public void set(T value) { // 获取当前线程 Thread t = Thread.currentThread(); // 获取当前线程的ThreadLocal对象 ThreadLocalMap map = getMap(t); // 如果当前线程的ThreadLocalMap对象不为空,就调用ThreadLocal对象的set方法设置一个值 if (map != null) map.set(this, value); else // 如果当前线程的ThreadLocalMap对象为空,就调用createMap(t, value)方法(createMap对象内部会初始化ThreadLocalMap // 并设置一个值) createMap(t, value); }4.3.3 remove()方法
移除当前线程与当前ThreadLocal对象相关联的值。
public void remove() { // 获取当前线程的ThreadLocalMap对象 ThreadLocalMap m = getMap(Thread.currentThread()); // 如果ThreadLocalMap对象不为空,调用ThreadLocalMap对象的remove方法移除 if (m != null) m.remove(this); }
ThreadLocal的方法很少,最主要的是ThreadLocalMap内部的方法,ThreadLocalMap是ThreadLocal的内部类。
5. ThreadLocalMap源码解析ThreadLocalMap是ThreadLocal的内部类,每一个Thread内部都会有一个ThreadLocalMap对象,ThreadLocalMap对象可以看成一个Map,但是会与我们平常使用的Map有一些区别,ThreadLocalMap内部采用线性探测法处理哈希冲突,这些我们下面都会讲到。
5.1 成员变量// ThreadLocalMap内部是一个Entry数组,INITIAL_CAPACITY就是这个Entry数组的容量大小 private static final int INITIAL_CAPACITY = 16; // table就是ThreadLocalMap内部的Entry数组 private Entry[] table; // 当前数组占用情况 private int size = 0; // 扩容阈值,初始值为 len * 2 / 3 // 触发后调用rehash()方法,rehash()方法 // 先做一次全面检查过期数据,把数组中所有过期的Entry移除, // 如果移除之后,数组中的Entry个数仍然达到threshold的3/4就扩容 private int threshold; // Default to 0 // 将扩容阈值设置为当前Entry数组长度的2/3 private void setThreshold(int len) { threshold = len * 2 / 3; }5.2 内部类
内部类Entry继承了弱引用,弱引用的对象下一次垃圾回收时一定会把回收掉。
key采用的是弱引用,value采用的是强引用。
Entry的key为什么设置为弱引用呢?
如果key为强引用,当ThreadLocal对象被回收时,ThreadLocalMap中的Entry如果没有被手动回收,会造成内存泄漏问题。但是如果key为弱引用,当ThreadLocal对象被回收时,ThreadLocalMap中的Entry可以被自动回收。还有一点当ThreadLocalMap的一些key被回收后,ThreadLocalMap可以区分哪些是过期的,那些不是过期的。
Entry的value为什么设置为强引用呢?
Entry的value设置为强引用的原因是如果采用弱引用假如往ThreadLocalMap里面存了一个value,GC过后value便消失了,也就达不到存储全局的效果了。
static class Entry extends WeakReference5.3 构造方法> { Object value; Entry(ThreadLocal> k, Object v) { super(k); value = v; } }
线程的ThreadLocalMap是延迟初始化的,只有当线程第一次存储key-value键值对时才会进行初始化。
// 传入一个键值对,键为ThreadLocal对象,值为当前线程与当前ThreadLocal对象相关联的值 // firstKey:ThreadLocal对象 // firstValue:当前线程与当前ThreadLocal对象相关联的值 ThreadLocalMap(ThreadLocal> firstKey, Object firstValue) { // 创建一个容量为初始容量INITIAL_CAPACITY(16)的Entry数组 table = new Entry[INITIAL_CAPACITY]; // 获取键值对应该存储在Entry数组中的位置 // 通过ThreadLocal对象的哈希值(也就是ThreadLocal对象的threadLocalHashCode实现) // & // Entry数组的长度-1(也就是INITIAL_CAPACITY - 1)求取 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); // 设置存储的元素个数为1 size = 1; // 设置扩容阈值,将当前Entry数组的长度传进去,在setThreshold方法内部会将阈值设置为Entry数组长度 * 2 / 3 setThreshold(INITIAL_CAPACITY); } // 传入一个ThreadLocalMap对象 // 内部其实就是创建了一个和传入的ThreadLocalMap内部的Entry数组相同长度的Entry数组 // 并将值一个一个赋值进去 private ThreadLocalMap(ThreadLocalMap parentMap) { // 获取传入的ThreadLocalMap对象内部的Entry数组 Entry[] parentTable = parentMap.table; // 获取传入的ThreadLocalMap对象内部的Entry数组长度 int len = parentTable.length; // 设置扩容阈值 setThreshold(len); // 在当前ThreadLocalMap里面new一个相同大小的Entry数组 table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; // 这里还检查了一下是否过期 if (e != null) { @SuppressWarnings("unchecked") ThreadLocal5.4 成员方法 5.4.1 nextIndex(int i, int len)
获取Entry数组索引i处的下一个索引位置
// 回环询问,如果i + 1 < len, 就返回len,否则返回0 // 这个方法其实是为了后面处理哈希冲突的线性探测法做准备的 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); }5.4.2 prevIndex(int i, int len)
获取Entry数组索引i处的上一个索引位置
private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); }5.4.3 getEntry(ThreadLocal> key)
由键获取对应的键值对,键其实就是ThreadLocal对象。
// 参数key:ThreadLocal对象 private Entry getEntry(ThreadLocal> key) { // 获取该键对应的键值对应该存储在的位置 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; // 如果对应的Entry不为空,且Entry对象对应的key和传入的key相等就返回这个Entry对象,说明找到了 if (e != null && e.get() == key) return e; else // 否则调用getEntryAfterMiss(key, i, e)方法 // 执行到这里有以下几种情况: // 1. 对应位置的Entry对象不存在 // 2. 存在,但是发生哈希冲突,调用了nextIndex获取了新的位置 return getEntryAfterMiss(key, i, e); }5.4.4 getEntryAfterMiss(ThreadLocal> key, int i, Entry e)方法
如果调用ThreadLocalMap的getEntry(ThreadLocal> key)方法没有找到对应的键值对就会调用这个方法进行后续 *** 作。
private Entry getEntryAfterMiss(ThreadLocal> key, int i, Entry e) { // 首先得到Entry数组 Entry[] tab = table; // 然后得到Entry数组的长度 int len = tab.length; // 如果对应位置的Entry不为空,就一个一个往后找同时清除过期数据,直到找到或下一个Entry对象为空为止 while (e != null) { ThreadLocal> k = e.get(); // 找到了,直接返回这个Entry对象 if (k == key) return e; // 没找到往下执行 // Entry对象为空,就清除过期数据 if (k == null) expungeStaleEntry(i); else // Entry不为空,获取下一个Entry对象 i = nextIndex(i, len); e = tab[i]; } // 没找到返回null return null; }5.4.5 expungeStaleEntry(int staleSlot)方法
探测式清除过期数据,从下标staleSlot开始,直到碰到的Entry数组中的Entry对象为null时结束,并返回结束时的下标。
private int expungeStaleEntry(int staleSlot) { // 得到Entry数组 Entry[] tab = table; // 获取Entry数组的长度 int len = tab.length; // 帮助GC // 因为是从getEntryAfterMiss方法进来的,在里面已经判断过key为null才进来的 // 所以这里就不用设置key为null了,因为key一定为null tab[staleSlot].value = null; // 设置这个Entry对象为null tab[staleSlot] = null; // Entry数组中存储的键值对数量减1 size--; // 表示当前遍历到的键值对结点 Entry e; // 表示当前遍历到的索引 int i; // 从staleSlot+1开始搜索过期数据,直到碰到的Entry对象为null位置 for (i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) { ThreadLocal> k = e.get(); // 如果当前Entry对象的key为null,说明是过期数据,要清理 if (k == null) { // 当前Entry对象的key为空,说明这个是过期数据,这个Entry对象要回收 e.value = null; // 设置当前Entry对象对应的索引处为null tab[i] = null; // 设置Entry数组中存储的元素数量减1 size--; } // 当前Entry对象的key不为null,说明当前Entry不是过期数据, // 因为当前Entry对象存储的位置可能是发生了哈希冲突后往后移而存储的位置, // 这个时候就应该尝试去优化位置,让这个位置更靠近本来的位置 // 这样的话,查询的时候效率才会更高 else { // 获取本来应该存储的位置 int h = k.threadLocalHashCode & (len - 1); // h != i,说明当前Entry对象是发生哈希冲突后往后移之后存储的位置 // 这个时候就应该去存储到本来的位置 if (h != i) { // 将移动后的位置设为null,再重新找一个更加接近本来位置的地方 tab[i] = null; while (tab[h] != null) h = nextIndex(h, len); // 将Entry对象存储到最接近本来位置的地方,也有可能就是本来的位置 tab[h] = e; } } } return i; }5.4.6 set(ThreadLocal> key, Object value)方法
给ThreadLocalMap独享添加键为key,值为value的键值对
// 参数key:键 // 参数value:值 private void set(ThreadLocal> key, Object value) { // 获取Entry数组 Entry[] tab = table; // 获取Entry数组的长度 int len = tab.length; // 获取Entry应该存储的位置 int i = key.threadLocalHashCode & (len-1); // 从应该存储的位置开始寻找 for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) { ThreadLocal> k = e.get(); // 如果 k == key,说明本来已经存储了一个一样的键,则只更新值,更新之后返回 if (k == key) { e.value = value; return; } // 碰到 k == null,说明碰到过期数据了,执行replaceStaleEntry(key, value, i)方法去替换过期数据 if (k == null) { replaceStaleEntry(key, value, i); return; } } // 碰到了一个Entry还没有找到,就将键值对添加到这个位置 tab[i] = new Entry(key, value); // Entry存储的元素数量加1 int sz = ++size; // 做一次启发式清理,如果cleanSomeSlots(i, sz)返回false,说明未清理到任何Entry对象,判断是否达到扩容阈值了 // 如果达到扩容阈值,就去扩容 if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }5.4.7 replaceStaleEntry(ThreadLocal> key, Object value,int staleSlot)方法
调用ThreadLocalMap的set方法往里面添加值时当碰到了过期数据,调用replaceStaleEntry方法替换过期数据
private void replaceStaleEntry(ThreadLocal> key, Object value,int staleSlot) { // 获取Entry数组 Entry[] tab = table; // 获取Entry数组的长度 int len = tab.length; Entry e; // 获取起始位置 int slotToExpunge = staleSlot; // 一直往前找,如果还可以找到过期数据,就继续往前找,直到找到的Entry对象不为空,并将过期数据的索引位置 // 赋值给slotToExpunge,slotToExpunge其实是为了下面的探测式清除数据准备的 for (int i = prevIndex(staleSlot, len);(e = tab[i]) != null;i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // 从staleSlot往后找,直到碰到null为止 for (int i = nextIndex(staleSlot, len);(e = tab[i]) != null;i = nextIndex(i, len)) { ThreadLocal> k = e.get(); // 条件成立,尝试去替换 if (k == key) { e.value = value; // 替换逻辑 tab[i] = tab[staleSlot]; tab[staleSlot] = e; // 条件成立说明向前查找并未找到过期的Entry if (slotToExpunge == staleSlot) slotToExpunge = i; // 启发式清理 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } // key == null说明当前Entry为过期数据,并且slotToExpunge == staleSlot说明 // 向前查找过期数据也没有找到,这时设置slotToExpunge为i表示从这里开始清除数据 if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } // 什么时候执行到这里? // 向后查找过程中并未发现 k == key的Entry,说明当前set是一个添加逻辑,按照普通的添加逻辑就好 tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); // 发现了过期数据,要清理过期数据 if (slotToExpunge != staleSlot) cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); }5.4.8 cleanSomeSlots(int i, int n)方法
启发式清理过期数据
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; // e != null 并且 e.key == null,说明当前Entry一定是一个过期数据,就从当前位置开始探测式清理数据 if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; }5.4.9 rehash()方法
先清理一下过期数据,如果清理之后的元素数量大于阈值的3/4,就执行真正的扩容方法。
private void rehash() { // 清除列表里所有过期数据 expungeStaleEntries(); // 如果当前数组里的元素数量还是大于阈值的3/4,就执行真正的扩容方式 if (size >= threshold - threshold / 4) resize(); }5.4.10 resize()方法
真正的扩容方法
private void resize() { // 获取旧的Entry数组 Entry[] oldTab = table; // 获取旧的Entry数组的长度 int oldLen = oldTab.length; // 设置扩容后的Entry数组为原来的两倍 int newLen = oldLen * 2; // 初始化一个Entry数组 Entry[] newTab = new Entry[newLen]; int count = 0; // 接下来就是一个一个填充了,同时碰到过期数据就清理了,过期数据不用往新的Entry数组填 for (int j = 0; j < oldLen; ++j) { Entry e = oldTab[j]; if (e != null) { ThreadLocal> k = e.get(); if (k == null) { e.value = null; // Help the GC } else { int h = k.threadLocalHashCode & (newLen - 1); while (newTab[h] != null) h = nextIndex(h, newLen); newTab[h] = e; count++; } } } setThreshold(newLen); size = count; table = newTab; }5.4.11 remove(ThreadLocal> key)方法
移除ThreadLocalMap中键为key的键值对
private void remove(ThreadLocal> key) { 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)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
文章学习于:
史上最全ThreadLocal 详解
ThreadLocal源码分析_02 内核(ThreadLocalMap)
【JDK源码】线程系列之ThreadLocal
深挖ThreadLocal
ThreadLocal原理及内存泄露预防
ThreadLocal原理详解——终于弄明白了ThreadLocal
ThreadLocal使用与原理
史上最全ThreadLocal 详解
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)