咖啡厅网站开发目标,品牌推广经典案例,网站建设合同要交印花吗,企业关键词优化价格ThreadLocal简介JDK源码对ThreadLocal类的注释如下#xff1a;ThreadLocal提供线程局部变量#xff0c;使得每个线程都有自己的、独立初始化的变量副本ThreadLocal实例通常是类中的private static字段#xff0c;用于将状态与线程相关联#xff0c;如用户ID、事务ID只要线程…ThreadLocal简介JDK源码对ThreadLocal类的注释如下ThreadLocal提供线程局部变量使得每个线程都有自己的、独立初始化的变量副本ThreadLocal实例通常是类中的private static字段用于将状态与线程相关联如用户ID、事务ID只要线程处于活动状态并且ThreadLocal实例是可访问的每个线程都将持有对线程局部变量副本的隐式引用当线程终止线程所绑定的线程局部变量都将被垃圾回收ThreadLocal的使用场景保存线程上下文信息在需要的地方进行获取实际开发中使用较少框架中使用较多如Spring的事务管理一般使用ThreadLocal管理数据库连接、Session会话等保证每一个线程中使用的连接是同一个每个线程需要自己独立的实例且该实例需要在多个方法中被使用保证线程安全避免同步操作带来的性能损耗局限性ThreadLocal实现了线程隔离自然也就无法解决多线程间共享对象的更新问题下图可以增强理解2、ThreadLocal与Synchronized的区别ThreadLocal和Synchonized都用于解决多线程并发访问但是ThreadLocal与synchronized有本质的区别Synchronized用于线程间的数据共享而ThreadLocal则用于线程间的数据隔离。Synchronized是利用锁的机制使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本使得每个线程在某一时间访问到的并不是同一个对象这样就隔离了多个线程对数据的数据共享。3、使用方法名描述public void set( T value)设置当前线程绑定的局部变量public T get()获取当前线程绑定的局部变量public T remove()移除当前线程绑定的局部变量该方法可以帮助JVM进行GCprotected T initialValue()返回当前线程局部变量的初始值public class ThreadLocalDemo {private ThreadLocalString threadLocal new ThreadLocal();private String name;public static void main(String[] args) {ThreadLocalDemo demo new ThreadLocalDemo();for (int i 0; i 5; i) {new Thread(() -{demo.setName(Thread.currentThread().getName() 的数据);System.out.println(Thread.currentThread().getName() : demo.getName());},线程_ i).start();}}public String getName() {return threadLocal.get();}public void setName(String name) {threadLocal.set(name);}
}线程_0 : 线程_0的数据
线程_2 : 线程_2的数据
线程_3 : 线程_3的数据
线程_1 : 线程_1的数据
线程_4 : 线程_4的数据4、ThreadLocal源码解析4.1 set() public void set(T value) {// 获取当前线程对象Thread t Thread.currentThread();// 获取此线程对象中维护的ThreadLocalMap对象ThreadLocalMap map getMap(t);// 判断map是否存在if (map ! null)map.set(this, value);else// 初始化 thradLocalMap 并赋值createMap(t, value);}/*** 获取当前线程Thread对应维护的ThreadLocalMap * 从这里可以看出为什么说ThreadLocal 是线程本地变量来的了*/ThreadLocalMap getMap(Thread t) {return t.threadLocals;}/*** 创建当前线程Thread对应维护的ThreadLocalMap */void createMap(Thread t, T firstValue) {t.threadLocals new ThreadLocalMap(this, firstValue);}执行流程获取当前线程并根据当前线程获取ThreadLocalMap如果获取的ThreadLocalMap不为空则将值 set 到ThreadLocalMap中当前ThreadLocal的引用作为key如果ThreadLocalMap为空则给该线程创建 ThreadLocalMap并将第一个值存放进去4.2 get() public T get() {// 获取当前线程对象Thread t Thread.currentThread();// 获取此线程对象中维护的ThreadLocalMap对象ThreadLocalMap map getMap(t);// 如果此 map 存在if (map ! null) {// 以当前的 ThreadLocal 为 key调用 getEntry 获取对应的存储实体eThreadLocalMap.Entry e map.getEntry(this);// 对e进行判空 if (e ! null) {SuppressWarnings(unchecked)// 获取存储实体 e 对应的 value值T result (T)e.value;return result;}}/*初始化 : 有两种情况执行下面代码第一种情况: map不存在表示此线程没有维护的ThreadLocalMap对象第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry*/return setInitialValue();}/*** 初始化*/private T setInitialValue() {// 调用initialValue获取初始化的值此方法可以被子类重写, 如果不重写默认返回nullT value initialValue();// 获取当前线程对象Thread t Thread.currentThread();// 获取此线程对象中维护的ThreadLocalMap对象ThreadLocalMap map getMap(t);// 判断map是否存在if (map ! null)// 存在则调用set方法设置值map.set(this, value);else// 初始化 thradLocalMap 并赋值createMap(t, value);// 返回设置的值valuereturn value;}/*** 该方法是一个protected的方法显然是为了让子类覆盖而设计的*/protected T initialValue() {return null;}执行流程获取当前线程, 根据当前线程获取ThreadLocalMap如果获取的ThreadLocalMap不为空则在ThreadLocalMap中以ThreadLocal的引用作为key来在ThreadLocalMap中获取对应的Entry e如果e不为null则返回e.value。如果获取的ThreadLocalMap 不为空但是 Entry 为空则通过initialValue函数获取初始值value调用set方法设置值如果获取的ThreadLocalMap 为空则通过initialValue函数获取初始值value然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的ThreadLocalMap4.3 remove() public void remove() {// 获取当前线程对象中维护的ThreadLocalMap对象ThreadLocalMap m getMap(Thread.currentThread());// 如果此map存在if (m ! null)// 以当前ThreadLocal为key删除对应的实体entrym.remove(this);}
5、ThreadLocalMapThreadLocalMap是ThreadLocal的内部类没有实现Map接口用独立的方式实现了Map的功能其内部的Entry也是独立实现的而Entry又是ThreadLocalMap的内部类且集成弱引用(WeakReference)类。 /*** Entry 的key 是一个弱引用也就意味这可能会被垃圾回收器回收掉*/static class Entry extends WeakReferenceThreadLocal? {Object value;Entry(ThreadLocal? k, Object v) {super(k);value v;}}// 初始容量必须是2的整次幂private static final int INITIAL_CAPACITY 16;// 存放数据的tableprivate Entry[] table;// 数组里面存放entrys的个数用于判断table当前使用量是否超过阈值。private int size 0;// 进行扩容的阈值使用量大于它的时候进行扩容。private int threshold;5.1 弱引用弱引用的出现就是为了垃圾回收服务的。它引用一个对象但是并不阻止该对象被回收。如果使用一个强引用的话只要该引用存在那么被引用的对象是不能被回收的。弱引用则没有这个问题。在垃圾回收器运行的时候如果一个对象的所有引用都是弱引用的话该对象会被回收Entry的Key为什么是弱引用如果key使用强引用业务代码中使用完ThreadLocal ThreadLocal Ref被回收了,因为ThreadLocalMap的Entry强引用了threadLocal造成threadLocal无法被回收,在没有手动删除这个Entry以及CurrentThread依然运行的前提下始终有强引用链 Thread ref-currentThread-threadLocalMap-entryEntry就不会被回收Entry中包括了ThreadLocal实例和value导致Entry内存泄漏如果key使用弱引用业务代码中使用完ThreadLocal threadLocal Ref被回收了,由于只有ThreadLocalMap的Entry这个弱引用指向ThreadLocal没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收此时Entry中的keynull但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下也存在有强引用链 threadRef-currentThread-threadLocalMap-entry-value不会被回收 而这块value永远不会被访问到了导致value内存泄漏。value内存泄漏的补救措施看源码你会发行在调用get、set或者remove()操作的时候都有机会执行回收无效entry的操作。但是这也不是一个十全十美的方法考虑这样的场景线程在后续的执行中没有ThreadLocal对象执行get、set或remove方法线程的ThreadLocalMap中的过期entry将无法被清理value的强引用链将一直存在内存泄漏也将随之发生主动调用ThreadLocal的remove方法实现 Entry -- key、Entry -- value、 ThreaLocalMap -- Entry三大引用链的断开避免内存泄漏的问题5.2 为什么ThreadLocalMap 采用开放地址法来解决哈希冲突?JDK中大多数的类都是采用了链地址法来解决hash 冲突为什么ThreadLocalMap 采用开放地址法来解决哈希冲突呢首先我们来看看这两种不同的方式链地址法这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表并将单链表的头指针存在哈希表的第i个单元中因而查找、插入和删除主要在同义词链中进行。开放地址法这种方法的基本思想是一旦发生了冲突就去寻找下一个空的散列地址(这非常重要源码都是根据这个特性必须理解这里才能往下走)只要散列表足够大空的散列地址总能找到并将记录存入。比如说我们的关键字集合为{12,33,4,5,15,25},表长为10。 我们用散列函数f(key) key mod l0。当计算前S个数{12,33,4,5}时都是没有冲突的散列地址直接存入蓝色代表为空的可以存放数据计算key 15时发现f(15) 5此时就与5所在的位置冲突。于是我们应用上面的公式f(15) (f(15)1) mod 10 6。于是将15存入下标为6的位置。这其实就是房子被人买了于是买下一间的作法链地址法和开放地址法的优缺点开放地址法容易产生堆积问题不适于大规模的数据存储。散列函数的设计对冲突会有很大的影响插入时可能会出现多次冲突的现象。删除的元素是多个冲突元素中的一个需要对后面的元素作处理实现较复杂。链地址法处理冲突简单且无堆积现象平均查找长度短。链表中的结点是动态申请的适合构造表不能确定长度的情况。删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。指针需要额外的空间故当结点规模较小时开放定址法较为节省空间。ThreadLocalMap 采用开放地址法原因ThreadLocal 中看到一个属性 HASH_INCREMENT 0x61c88647 0x61c88647 是一个神奇的数字让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] tableThreadLocal 往往存放的数据量不会特别大而且key 是弱引用又会被垃圾回收及时让数据量更小这个时候开放地址法简单的结构会显得更省空间同时数组的查询效率也是非常高加上第一点的保障冲突概率也低5.3 ThreadLoaclMap 构造器 // 初始化ThreadLocalMap,并添加 firstValue到里面ThreadLocalMap(ThreadLocal? firstKey, Object firstValue) {//初始化tabletable new ThreadLocal.ThreadLocalMap.Entry[INITIAL_CAPACITY];//计算索引// (INITIAL_CAPACITY - 1) 相当于取模运算 hashCode % size 的一个更高效的实现int i firstKey.threadLocalHashCode (INITIAL_CAPACITY - 1);//设置值table[i] new ThreadLocal.ThreadLocalMap.Entry(firstKey, firstValue);size 1;//设置阈值setThreshold(INITIAL_CAPACITY);}
构造函数首先创建一个长度为16的Entry数组然后计算出firstKey对应的索引然后存储到table中并设置size和threshold // ThreadLocal类//AtomicInteger是一个提供原子操作的Integer类通过线程安全的方式操作加减private static AtomicInteger nextHashCode new AtomicInteger();//特殊的hash值private static final int HASH_INCREMENT 0x61c88647; private final int threadLocalHashCode nextHashCode();private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}这里定义了一个AtomicInteger类型每次获取当前值并加上HASH_INCREMENTHASH_INCREMENT 0x61c88647跟斐波那契数列黄金分割数有关其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,也就是Entry[] table中这样做可以尽量避免hash冲突。5.4 set()private void set(ThreadLocal? key, Object value) {ThreadLocal.ThreadLocalMap.Entry[] tab table;int len tab.length;//计算索引int i key.threadLocalHashCode (len-1);// 从索引i开始向后查找,直到遇到空slotfor (ThreadLocal.ThreadLocalMap.Entry e tab[i];e ! null;e tab[i nextIndex(i, len)]) {ThreadLocal? k e.get();if (k key) {e.value value;return;}// key为 null说明之前的 ThreadLocal 对象已经被回收了if (k null) {//用新元素替换陈旧的元素这个方法进行了不少的垃圾清理动作防止内存泄漏replaceStaleEntry(key, value, i);return;}}//key 和 value 都为null则在空元素的位置创建一个新的Entry。tab[i] new Entry(key, value);int sz size;// 清理过期的entry如果size超过阈值则需要扩容if (!cleanSomeSlots(i, sz) sz threshold)rehash();
}/*** 获取环形数组的下一个索引*/private static int nextIndex(int i, int len) {return ((i 1 len) ? i 1 : 0);}代码执行流程首先还是根据key计算出索引 i然后查找i位置上的Entry若是Entry已经存在并且key等于传入的key那么这时候直接给这个Entry赋新的value值若是Entry存在但是key为null则调用replaceStaleEntry来更换这个key为空的Entry不断循环检测直到遇到为null的地方这时候要是还没在循环过程中return那么就在这个null的位置新建一个Entry并且插入同时size增加1最后调用cleanSomeSlots清理key为null的Entry最后返回是否清理了Entry接下来再判断sz 是否 thresgold达到了rehash的条件达到的话就会调用rehash函数执行一次全表的扫描清理分析 ThreadLocalMap使用线性探测法来解决哈希冲突的该方法一次探测下一个地址直到有空的地址后插入若整个空间都找不到空余的地址则产生溢出假设当前table长度为16也就是说如果计算出来key的hash值为14如果table[14]上已经有值并且其key与当前key不一致那么就发生了hash冲突这个时候将14加1得到15取table[15]进行判断这个时候如果还是冲突会回到0取table[0],以此类推直到可以插入可以把Entry[] table看成一个环形数组5.4.1 replaceStaleEntry()replaceStaleEntry() 方法并非简单地使用新entry替换过期entry而是从过期entry所在的slotstaleSlot向前、向后查找过期entry并通过slotToExpunge 标记过期entry最早的index最后使用cleanSomeSlots() 方法从slotToExpunge开始清理过期entry private void replaceStaleEntry(ThreadLocal? key, Object value,int staleSlot) {Entry[] tab table;int len tab.length;Entry e;//表示开始探测式清理过期数据的开始下标默认从当前的staleSlot开始int slotToExpunge staleSlot;//从staleSlot的前一个位置开始向前查找过期entry并更新slotToExpunge直到遇到空slotfor (int i prevIndex(staleSlot, len);(e tab[i]) ! null;i prevIndex(i, len))if (e.get() null)slotToExpunge i;// 从staleSlot的后一个位置开始向后查找,直到遇到空slotfor (int i nextIndex(staleSlot, len);(e tab[i]) ! null;i nextIndex(i, len)) {ThreadLocal? k e.get();// 由于开放定址法可能相同的key存放于预期的位置staleSlot之后// 如果遇到相同的key则更新value并交换索引staleSlot与索引i的entry// 交换的原因让有效的entry占据预期的位置staleSlot避免重复key的情况if (k key) {e.value value;tab[i] tab[staleSlot];tab[staleSlot] e;// slotToExpunge staleSlot说明索引staleSlot处前一个entry为null // 未找到过期entry更新slotToExpunge为iif (slotToExpunge staleSlot)slotToExpunge i;// 从slotToExpunge开始清理一些过期entrycleanSomeSlots(expungeStaleEntry(slotToExpunge), len);return;}// 向后查找未找到过期entry更新slotToExpunge为当前indexif (k null slotToExpunge staleSlot)slotToExpunge i;}// 直到遇到空slot也未发现相同的key则在staleSlot的位置新建一个entrytab[staleSlot].value null;tab[staleSlot] new Entry(key, value);// 存在过期entry需要进行清理if (slotToExpunge ! staleSlot)cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);}/*** 获取环形数组的前一个索引*/private static int prevIndex(int i, int len) {return ((i - 1 0) ? i - 1 : len - 1);}
总体来说过期entry所在的staleSlot是key瞄准的位置如果key已经存在则需要将原entry更新、与staleSlot对应的过期entry交换使其位于staleSlot这个位置如果遇到了空slot都未发现key相等的entry说明key不存在 ⇒ \Rightarrow⇒ 直接在在staleSlot这个位置新建entry不管是哪种情况只要发现过期entry都需要通过cleanSomeSlots() 进行清理而过期entry存在的判断条件为slotToExpunge ! staleSlot为何需要staleSlot和key相等时的slot交换通过向后遍历数组找到了相同的key 说明发生了hash冲突 的情况让 key 存储到了预期位置的后面staleSlot和key相等时的slot交换让有效的entry占据预期的位置staleSlot在调用get方法获取时可直接通过hash len-1得到准确的索引而不用向后遍历去查找。5.4.2 expungeStaleEntry()清除当前过期entry到下一个空slot之间所有过期entry并将有效entry通过hash len-1重新计算索引位置可能会遇到slot被占用的情况开放地址法移位导致需要向后遍历找到空的slot放置返回空slot的indexprivate int expungeStaleEntry(int staleSlot) {Entry[] tab table;int len tab.length;// 清理过期的entrytab[staleSlot].value null;tab[staleSlot] null;size--;// 对后续entry进行rehash直到遇到空slotEntry e;int i;for (i nextIndex(staleSlot, len); (e tab[i]) ! null; i nextIndex(i, len)) {ThreadLocal? k e.get();if (k null) { // 过期entry继续清理e.value null;tab[i] null;size--;} else { // 有效entryrehash到合适的位置补齐空slotint h k.threadLocalHashCode (len - 1);// 期望的index与当前index不相等说明是开放地址法移位导致的需要将其放到最近的有效entry之后if (h ! i) { tab[i] null;while (tab[h] ! null)h nextIndex(h, len);tab[h] e;}}}return i; // 返回空slot的index
}
5.4.3 cleanSomeSlots()通过循环扫描尽可能多的清理ThreadLocalMap中的过期entryprivate 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];if (e ! null e.get() null) { // 遇到过期entry需要重置nn len;removed true;i expungeStaleEntry(i);}} while ( (n 1) ! 0); //无符号右移动一位可以简单理解为除以2return removed;
}
5.4.4 rehash()rehash之前仍然先清理一次过期entry如果size 3/4 threshold也就是size 1/2 table.length则进行扩容操作 threshold 2/3 * table.lengthprivate void rehash() {expungeStaleEntries();// Use lower threshold for doubling to avoid hysteresisif (size threshold - threshold / 4)resize();
}5.4.5 expungeStaleEntries()对数组进行整体的遍历清理过期的keycleanSomeSlots()是尽可能多的清理不一定清理的干净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)// 清除当前过期entry到下一个空slot之间所有过期entry并将有效entry进行 hash len-1,// 重新计算其在数组中的位置expungeStaleEntry(j);}
}5.4.6 resize将原数组扩大为原来的两倍将旧的桶数组中的entry移动到新的桶数组中重新计算其索引位置遇到过期entry直接断开entry对value的引用方便gc。private void resize() {Entry[] oldTab table;int oldLen oldTab.length;int newLen oldLen * 2;Entry[] newTab new Entry[newLen];int count 0;// 旧的桶数组中的entry移动到新的桶数组中// 对于过期entry直接断开entry对value的引用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;}}}// 更新threshold、size、table旧的桶数组等待GCsetThreshold(newLen);size count;table newTab;
}
5.5 getEntry()获取key对应的entry若直接命中则直接返回对应的entry否则需要通过getEntryAfterMiss() 方法往后遍历查找private Entry getEntry(ThreadLocal? key) {// 计算索引位置int i key.threadLocalHashCode (table.length - 1); Entry e table[i];if (e ! null e.get() key) // 直接命中return e;else // 否则往后遍历查找说明出现hash冲突问题return getEntryAfterMiss(key, i, e);
}
5.5.1 getEntryAfterMiss()private Entry getEntryAfterMiss(ThreadLocal? key, int i, Entry e) {Entry[] tab table;int len tab.length;// 从 e 的下一个索引处向后遍历遇到空slot结束while (e ! null) {ThreadLocal? k e.get();if (k key)return e;// 遇到过期的entry// 清除过期entry到下一个空slot之间所有过期entry并将有效entry通过hash len-1重新计算索引位置if (k null) expungeStaleEntry(i); elsei nextIndex(i, len);e tab[i];}return null;
}5.6 remove()private void remove(ThreadLocal? key) {Entry[] tab table;int len tab.length;// 计算索引位置int i key.threadLocalHashCode (len-1);// 向后遍历直到遇到空slotfor (Entry e tab[i];e ! null;e tab[i nextIndex(i, len)]) {// 找到keyif (e.get() key) {// 清除ee.clear();//清除过期entry到下一个空slot之间所有过期entry并将有效entry通过hash len-1重新计算索引位置expungeStaleEntry(i);return;}}
}参考文章被大厂面试官连环炮轰炸的ThreadLocal 吃透源码的每一个细节和设计原理 - 掘金 (juejin.cn)(1条消息) ThreadLocal学习_晓之木初的博客-CSDN博客(1条消息) 史上最全ThreadLocal 详解一_倔强的不服的博客-CSDN博客_threadlocal