ThreadLocal的熟悉与使用

目录

  • 1.ThreadLocal介绍
  • 2.ThreadLocal源码解析
    • 2.1 常用方法
    • 2.2 结构设计
    • 2.3 类图
    • 2.4 源码分析
      • 2.4.1 set方法分析
      • 2.4.2 get方法分析
      • 2.4.3 remove方法分析
  • 3.ThreadLocal内存泄漏分析
    • 3.1 相关概念
      • 3.1.1 内存溢出
      • 3.1.2 内存泄漏
      • 3.1.3 强引用
      • 3.1.4 弱引用
    • 3.2 内存泄漏是否和key使用的弱引用有关
      • 3.2.1 假设key使用强引用
      • 3.2.2 假设key使用弱引用
      • 3.2.3 内存泄漏的真实原因
      • 3.2.4 ThreadLocalMap的key使用弱引用的原因
  • 4.ThreadLocal使用场景

1.ThreadLocal介绍

ThreadLocal 是Java JDK中提供的一个类,用于提供线程内部的局部变量,这种变量在多线程下的环境下去访问时能保证各个线程的变量独立于其他线程的变量。也就是说,使用ThreadLocal 可以提供线程内部的局部变量(通过ThreadLocal的set() 和 get() 方法),不同的线程之间不会互相干扰,这种变量在线程的生命周期内起作用,可以减少同一个线程内多个函数或者组件之间一些公共变量传递的复杂度。听起来好像挺复杂的,下面我们使用一个简单的案例来解释一下ThreadLocal的作用。

案例说明: 演示的代码将会使用一个TestData类表示存放在线程里面的数据,然后开启10个线程,在每个线程中设置数据后紧接着获取数据,并且使用Thread.currentThread().getName()标识对应的线程。然后为每个线程设置名称,方便我们观察线程的数据情况。

在不使用ThreadLocal和加锁的情况下:

public class ThreadLocalDemo {public static void main(String[] args) {TestData testData = new TestData();for (int i = 0; i < 10; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {testData.setData("数据XXX,当前线程是===>"+ Thread.currentThread().getName());System.out.println("当前线程是: "+ Thread.currentThread().getName()+ ",存放的数据是:" + testData.getData());}});thread.setName("线程===>" + i);thread.start();}}static class TestData {private String data;public void setData(String data) {this.data = data;}public String getData() {return data;}}
}

运行上面的代码结果如下:
在这里插入图片描述
如上图所示:我们发现有的线程拿到的数据是其他线程的,也就是各个线程之间的数据错乱了,这种情况是一种错误,因为线程之间的数据发生了相互干扰的情况。比如上图中选中的部分,线程1存放的数据被线程4拿到了。正确的情况应该是,线程1存放的数据,应该也是由线程1取。即各个线程之间不应该相互干扰

解决上面线程间错误的问题有两种方法,一是加锁,二是使用ThreadLocal,接下来看加锁的方案,代码如下:

    public static void main(String[] args) {TestData testData = new TestData();for (int i = 0; i < 10; i++) {Thread thread = new Thread(new Runnable() {@Overridepublic void run() {// 加锁解决线程间数据错乱的问题synchronized (ThreadLocalDemo.class){testData.setData("数据XXX,当前线程是===>"+ Thread.currentThread().getName());System.out.println("当前线程是: "+ Thread.currentThread().getName()+ ",存放的数据是:" + testData.getData());}}});thread.setName("线程===>" + i);thread.start();}}

在每个线程的run方法中添加一个synchronized锁,在多个线程的情况下,限制每次只能有一个线程存取数据,这样就能解决线程间数据干扰的问题,运行结果如下:
在这里插入图片描述
如上图所示,使用加锁的方案后,线程存数据和取数据的线程都是同一个了,不会出现线程1的数据被线程2取到了
然后我们再看下使用ThreadLocal的方式解决线程间数据相互干扰的问题,代码如下:

    static class TestData {private ThreadLocal<String> tl = new ThreadLocal<>();public void setData(String data) {tl.set(data);}public String getData() {return tl.get();}}

运行结果如下:
在这里插入图片描述

如上图所示,存储数据时使用TreadLocal的set方法,取数据时使用ThreadLocal的get()方法,这样也能解决线程间数据相互干扰的问题,具体原理会在后面源码分析部分解析

看完上面的例子,可能会有小伙伴心中有疑问,既然加锁可以解决线程间数据相互干扰的问题,那么为啥还需要设计出一个ThreadLocal呢?其实这得联系synchronized和ThreadLocal的区别,synchronized是一种同步机制,采用以“时间换空间”的方式,只是提供一份数据,让不同的线程排队使用,它的侧重点在于多个线程之间同步访问资源。而ThreadLocal则是以“空间换时间”,为每一个线程都提供了一份数据的副本,从而实现同时访问而互不干扰,它侧重于多线程中让每个线程之间的数据的相互隔离。在上面的例子中我们强调的是线程数据隔离的问题,使用synchronized不仅消耗性能(加锁会使程序的性能降低),而且加锁更加使用于数据共享的场景,用在此处并不合适,使用TreadLocal可以使程序获得更高的并发性。

通过上面的例子,相信读者已经可以简单的理解ThreadLocal是啥以及它的作用了,接下来我们将从源码分析ThreadLocal,一点点解开其背后的神秘面纱。

2.ThreadLocal源码解析

2.1 常用方法

方法描述
ThreadLocal()构造方法,创建ThreadLocal对象
public void set(T value)设置当前线程绑定的数据
public T get()获取当前线程绑定的数据
public void remove()移除当前线程绑定的数据

2.2 结构设计

这里我们说的ThreadLocal都是JDK 1.8之后的,在JDK1.8中,每个Thread维护一个ThreadLocalMap哈希表,这个Hash表的Key是ThreadLocal本身,value是要存储的数据Object具体的过程如下:

1.每个Thread都有一个Map,名为ThreadLocalMap
2.ThreadLocalMap里面存储了ThreadLocal对象(Key)和线程的数据副本(Value)
3.Thread内部的ThreadLocalMap是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的数据
4.对于不同的线程,每次获取副本数据时,别的线程并不能获取当前线程的副本数据,实现了副本数据的隔离。

ThreadLocal的结构如下所示:


在这里插入图片描述

2.3 类图


在这里插入图片描述

ThreadLocalMap 是ThreadLocal的内部类,没有实现Map接口,而是使用独立的方式实现了Map的功能,其内部的Entry也是独立实现的。并且继承自弱引用的接口

2.4 源码分析

下面先解释下ThreadLocal中会用到的存储结构,Entry类,代码如下所示:

    static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}

从Entry的代码中我们可以得知,Entry继承自WeakReference,并且使用ThreadLocal作为key.并且这个key只能是ThreadLocal对象.

补充:在ThreadLocal中会自定义一个ThreadLocalMap以Key、Value的方式保存值,类似于HashMap。我们都知道HashMap会有Hash冲突,而解决HashMap冲突的方法是链地址法,而ThreadLocalMap解决冲突的方法是线性探测法,该方法一次探测一个地址,直到有空地址可插入,若是整个空间都找不到空余的地址,则产生溢出。比如,假设当前数组的长度为16,如果计算出来的索引为14,而数组中位置为14的地方已经有值了,并且这个值的key和当前待插入数据的key不一样,那么此时就发生了哈希冲突。线性探测法就是说这时候可以通过一个线性的函数,将当前的位置作为输入,经过线性函数运算后得到一个输出,,比如我们确定这个线性函数为y = x + 1,y为计算后的索引值,x为输入的索引值,我们这时候可以将14输入线性函数,得到新的索引为15,取数组中位置为15的位置的值判断,如果还是冲突,则会溢出,这时候可以判断溢出的时候就从位置0继续使用线性探测法查找可以插入数据的位置。

2.4.1 set方法分析

当我们使用ThreadLocal的时候,首先会通过new关键字创建一个ThreadLocal对象,然后调用ThreadLocal的set方法保存我们想要保存的值,set方法执行过程的源码如下:
使用ThreadLocal对象调用set方法存储数据的时候,会首先调用下面的set方法,下面的方法会将当前线程对象和需要保存的数据一起传入内部的set(Thread,value)方法里。

 public void set(T value) {// 调用内部的set方法,并且将当前的线程对象和要保存的值传递过去set(Thread.currentThread(), value);if (TRACE_VTHREAD_LOCALS) {dumpStackIfVirtualThread();}}

set(Thread,value)方法会首先去获取下当前线程是否已经关联了ThreadLocalMap,如果已经关联了就直接取出这个Map,调用其set方法保存数据,否则创建一个新的ThreadLocalMap,并保存数据,并且将创建的ThreadLocalMap赋值给当前线程里面的threadLocals变量。

  private void set(Thread t, T value) {// 获取和当前线程相关联的ThreadLocalMapThreadLocalMap map = getMap(t);// 如果获取到的ThreadLocalMap不为空,则直接调用ThreadLocalMap的set方法直接赋值// 否则使用当前线程和需要保存的值直接创建ThreadLocalMap对象,需要注意的是,这里不用再// 调用ThreadLocalMap的set方法了,因为值的保存操作会在ThreadLocalMap的构造函数中完成if (map != null) {map.set(this, value);} else {createMap(t, value);}}// 通过线程去拿与其关联的ThreadLocalMap,从下面的代码// 可以看出,ThreadLocalMap被作为了一个成员变量声明到了线程// Thread类中ThreadLocalMap getMap(Thread t) {return t.threadLocals;}// 使用线程和需要保存的数据创建一个ThreadLocalMap对象,// 并将其赋值给当前线程的threadLocals变量void createMap(Thread t, T firstValue) {t.threadLocals = new ThreadLocalMap(this, firstValue);}

当线程中关联的ThreadLocalMap为空时会使用ThreadLocal作为key,需要保存的数据作为Value去新建一个ThreadLocalMap对象,下面是ThreadLocalMap的构造方法。

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {// table 和HashMap中的table类似,这里的INITIAL_CAPACITY是16,必须为2的幂次方,// 原因后面会介绍// 这里主要是初始化table数组,数据的元素类型是Entry的,初始容量是16table = new Entry[INITIAL_CAPACITY];// 和HashMap一样,使用key的HashCode和长度减一做与操作,计算出一个索引值int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);// 将要保存的数据包装成Entry后保存到索引对应的位置table[i] = new Entry(firstKey, firstValue);// 记录当前ThreadLocalMap的大小size = 1;// 设置扩容的阈值setThreshold(INITIAL_CAPACITY);}// 设置阈值,当阈值达到设置长度的2/3时进行扩容操作private void setThreshold(int len) {threshold = len * 2 / 3;}

如果线程中的ThreadLocalMap不为空的情况下,会被取出来调用其set(ThreadLocal<?> key, Object value) 方法保存数据,这个方法的具体解析如下面代码中的注释所示。

        private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;// 记录下table的长度int len = tab.length;// 计算待插入元素在table数组中的索引,使用的是和HashMap类似的,使用key的hash值和// 数组的长度减一做与操作。这里的数组长度需要是2的幂次方int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();// 如果待插入的key已经存在,则直接使用待插入的数据覆盖原来的数据即可if (k == key) {e.value = value;return;}// 如果key为null,但是数据value不为null,则说明之前的ThreadLocal对象已经被回收了if (k == null) {// 使用新的元素替换之前的元素replaceStaleEntry(key, value, i);return;}}// ThreadLocal对应的key不存在并且没有找到旧的元素,则在空元素的位置新建一个Entrytab[i] = new Entry(key, value);// 增加ThreadLocalMap的sizeint sz = ++size;// cleanSomeSlots用于清除e.get == null的元素// 因为这种数据key关联的对象已经被回收,所以Entry(table[index])可以被置为null,// 如果没有清除任何的Entry,并且当前的使用量达到了负载因子所定义的(长度的2/3),// 那么进行再次哈希计算的逻辑(rehash),执行一次全表的扫描清理工作if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}// 循环获取数组的下一个索引private static int nextIndex(int i, int len) {return ((i + 1 < len) ? i + 1 : 0);}

在上面的代码中我们提到了数组的长度需要是2的幂次方,原因如下:
我们都知道通常情况下如果想要将某个Hash值映射到数组的索引上,通常会使用到取模(%)运算符,因为这可以确保生成的索引在数组的范围类,例如生成数组长度为16,计算的结果会在0到15之间。,那我们在HashMap和ThreadLocalMap中要规定数组的长度必须是2的幂次方呢?那是因为计算数组的索引没有采用传统的取模(%)运算,而使用的是与(&)操作,如下图所示:
在这里插入图片描述
使用与操作会比取模操作快很多,但是只有当数组长度为2的幂次方时,hashcode&(数组长度 - 1) 才等价于 hashcode % 数组长度,,其次保证数组长度为2的幂次方也恶意减少冲突的次数,提高查询的效率。例如,若数组长度为2的幂次方,则数组长度减一转为二进制必定是1111…的形式,在和hashcode二进制做与操作时效率会非常高,而且空间不浪费。举个反例,假设数组的长度不是2的幂次方,不妨设为15,则数组的长度减一为14,对应的二进制为1110,在与hashcode做“与操作”时,由于最后一位都是0,这就会导致数组位置索引最后一位为“1”的位置(如0001,0101,1011,1101)永远无法存放元素,浪费空间,并且导致数组可使用的位置比数组长度小很多,发生哈希冲突的几率增大,并且降低了查询效率。
注意:这里的hashcode不是指通过对象的hashCode()方法获取到的值,而是经过一些算法得到的一个哈希值

set方法代码的执行流程

  1. 根据key的hashcode计算出索引"i",然后查找到"i"位置上的Entry
  2. 若Entry存在,并且key等于传入的key,那么直接给找到的Entry赋新的value值
  3. 若Entry存在,但是key为null,则调用replaceStaleEntry()方法更换key为空的Entry
  4. 若不存在上面的情况,则开启循环检测,直到遇到为null的位置,在这个null位置新建一个Entry,然后插入,同时将ThreadLocalMap的size增加1
  5. 调用cleanSomeSlots方法,清理Key为null的Entry,最后返回是否清理了Entry的结果,然后再判断ThreadLocalMap的size是否大于等于扩容的阈值,如果达到了,需要执行rehash函数进行全表扫描清理,清理完ThreadLocalMap的size还是大于阈值的3/4的化,那么就需要进行扩容。扩容操作会将数组的长度扩容为之前的两倍

2.4.2 get方法分析

get方法是获取当前线程中保存的值,调用的方式就是使用ThreadLocal的对象调用get方法,ThreadLocal中的get方法如下所示:

   public T get() {// 获取当前线程Thread t = Thread.currentThread();// 根据当前线程拿到ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null) {// ThreadLocalMap不为空的情况下,调用ThreadLocalMap的getEntry方法,// 传入当前的ThreadLocal(key),拿到当前ThreadLocal对应的数据并返回给调用者ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")// 将数据做一个类型转换,然后返回T result = (T)e.value;return result;}}// ThreadMap为空的情况下,会调用setInitialValue方法返回一个值return setInitialValue();}// 拿到线程对应的ThreadLocalMap对象ThreadLocalMap getMap(Thread t) {return t.threadLocals;}// 设置ThreadLocal的初始值private T setInitialValue() {// 通过initialValue方法获取初始值,initialValue是一个可供// 子类重写的方法,子类可以重写initialValue方法提供一个默认// 值,不重写的情况下为null.T value = initialValue();// 获取当前线程Thread t = Thread.currentThread();// 根据当前线程拿到ThreadLocalMapThreadLocalMap map = getMap(t);// 如果ThreadLocalMap不为空,则将通过initialValue获取的值设// 置给ThreadLocalMapif (map != null) {map.set(this, value);} else {// 如果通过当前线程没有获取到ThreadLocalMap,则创建一个ThreadLocalMap并将通过// initialValue方法获取到的值设置给它createMap(t, value);}// 不是重点,不分析,这里是为了解决ThreadLocal引用泄漏的问题的,TerminatingThreadLocal // 提供了一种机制,可以在线程终止时自动清理其绑定的数据。if (this instanceof TerminatingThreadLocal) {TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);}return value;}// ThreadLocalMap中的getEntry方法,参数:keyprivate Entry getEntry(ThreadLocal<?> key) {// 通过key的threadLocalHashCode计算出数组table中的索引位置int i = key.threadLocalHashCode & (table.length - 1);// 通过计算出的索引值拿到对应的Entry元素Entry e = table[i];// 若拿到的元素不为null,并且元素的key和当前传入的key相同,则证明找到了// 传入的key对应的Entry元素,直接返回if (e != null && e.get() == key)return e;else// 否则可能是在插入数据时有冲突被放到了其他位置了,通过getEntryAfterMiss方法// 继续查找其他位置return getEntryAfterMiss(key, i, e);}// 查找Entry元素,参数key:待查找元素的key,i: 当前元素的索引,索引i对应的元素Entryprivate Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {// 拷贝一份当前的table数组Entry[] tab = table;// 记录当前数组的长度int len = tab.length;// 若是元素Entry不为null,就循环查找,直到找到待查找元素key对应的Entry为止while (e != null) {ThreadLocal<?> k = e.get();// 如果e的key等于待查找的元素的key,证明找到了直接返回就行if (k == key)return e;// 如果e的key为null,则调用expungeStaleEntry方法替换if (k == null)// 清除key为null的EntryexpungeStaleEntry(i);else// 通过线性探测法继续寻找下一个位置,插入值的时候如果有冲突也是通过这个方法// 解决的,所以查询值的时候,如果不在通过key的hashcode值计算出的索引位置// 就可以通过这个函数继续寻找下一个位置。直到找到待查找key对应的数据为止i = nextIndex(i, len);e = tab[i];}// 如果没有找到就返回nullreturn null;}

2.4.3 remove方法分析

ThreadLocal的remove方法用于删除当前线程中保存的ThreadLocal对应的Entry,代码如下所示:

 public void remove() {// 首先通过当前线程获取到TheadLocalMap,不为null的情况下// 删除当前ThreadLocal保存的Entry;如果为null,表示// 不需要删除ThreadLocalMap m = getMap(Thread.currentThread());if (m != null) {// 调用ThreadLocalMap的remove方法删除保存的Entrym.remove(this);}}// ThreadLocalMap中删除ThreadLocal对应的Entryprivate void remove(ThreadLocal<?> key) {// 拷贝一份table数组Entry[] tab = table;// 记录数组的长度int len = tab.length;// 根据当前的key计算出Entry的索引位置"i"int i = key.threadLocalHashCode & (len-1);// 在数组中遍历查找key对应的entryfor (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {// 查找到key对应的entryif (e.get() == key) {// 调用Entry的clear方法清理掉Entry,其实就是将当前的Entry引用置为null// 等待垃圾回收器回收e.clear();// 清除key为null的EntryexpungeStaleEntry(i);return;}}// Entry中的clear方法public void clear() {this.referent = null;}

3.ThreadLocal内存泄漏分析

3.1 相关概念

3.1.1 内存溢出

内存溢出(Memory overflow)是指没有足够的内存提供给申请者使用

3.1.2 内存泄漏

内存泄漏(Memory leak)指的时程序中已经动态分配的堆内存由于某种原因未释放或者是无法释放,造成系统内存的浪费,从而导致程序运行速度减慢升值系统崩溃的严重后果,内存泄漏的堆积最终将会导致内存溢出

3.1.3 强引用

强引用(Strong Reference)就是我们常见的普通对象的引用,比如:Object strongRef = new Object();就是一种强引用,只要某个对象有强引用指向它,垃圾回收器(Garbage Collector) 就不会回收该对象。

3.1.4 弱引用

弱引用(Weak Reference) 是一种特殊的引用类型,用于改善内存管理。弱引用允许垃圾回收器回收被其引用的对象,即使该对象仍然有活动的弱引用存在。它通常用于缓存、引用监听器和防止内存泄漏的场景。

3.2 内存泄漏是否和key使用的弱引用有关

有读者可能会猜测ThreadLocal的内存泄漏可能会和Entry中使用了弱引用的key有关系,其实这个猜测不太准确,下面就从两个方面分析下ThreadLocal内存泄漏的原因

3.2.1 假设key使用强引用

假设ThreadLocalMap中的key使用了强引用,,则此时ThreadLocal的内存图如下所示:
在这里插入图片描述

如上图所示,假设在业务代码中使用玩ThreadLocal后,ThreadLocal引用被回收了,但是因为ThreadLocalMap的Entry强引用ThreadLocal,会造成ThreadLocal无法被回收,这时在没有手动删除Entry和CurrentThread的情况下,始终会有强引用链:CurrentThread引用===>CurrentThread===> ThreadLocalMap===>Entry.最终导致Entry无法被回收导致内存泄漏,所以ThreadLocalMap中的key使用了强引用是无法完全避免内存泄漏的

3.2.2 假设key使用弱引用

假设key使用了弱引用,ThreadLocal的内存图如下所示
在这里插入图片描述

假设业务代码中使用完ThreadLocal后,然后ThreadLocal被回收了,此时由于ThreadLocalMap只持有ThreadLcoal的弱引用,并且没有任何的强引用指向ThreadLocal实例,所以ThreadLocal实例可以顺利的被垃圾回收器回收,此时就会导致Entry的key为null,这时候如果我们没有手动删除这个Entry以及CurrentThread仍然运行的前提下,也存在强引用链:CurrentThread引用===>CurrentThread===> ThreadLocalMap===>Entry===>Value,而这里的Value不会被回收,但是这块Value永远不会被访问到了,因为key已经被回收了,导致Value内存泄漏,所以ThreadLocalMap中的key使用了弱引用,也有可能导致内存泄漏。

3.2.3 内存泄漏的真实原因

通过上面的两种对key使用强引用和弱引用的方式分析,我们发现ThreadLocal的内存泄漏和ThreadLocalMap的key是否使用弱引用是没有关系的,真正引起内存泄漏的原因主要有两点,第一点是当ThreadLocal被回收后,没有手动删除ThreadLocalMap的Entry,这时只要我们使用完后调用ThreadLocal的remove方法删除对应的Entry就可以避免内存泄漏。第二点是当ThreadLocal被回收后,CurrentThread依然在运行。由于ThreadLocalMap是Thread的一个属性,被当前线程引用,所以它的生命周期和Thread一样长,那么在使用完ThradLocal,如果当前的线程也一起随之结束,那么ThreadLocalMap就可以被垃圾回收器回收,从根源上避免了内存泄漏

结合上面的分析可以知道,ThreadLocal内存泄漏的真实原因是,由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应的Entry就会导致内存泄漏。

3.2.4 ThreadLocalMap的key使用弱引用的原因

有的读者可能会问,既然ThreadLocalMap的key使用强引用和弱引用都无法避免内存泄漏,那么为啥偏偏选择使用弱引用呢?经过前面的分析我们发现ThreadLocalMap的key无论使用强引用还是弱引用都无法完全避免内存泄漏,如果想要避免内存泄漏,主要有两种方式:

  1. 使用完ThreadLocal后,调用其remove方法删除对应的Entry
  2. 使用完ThreadLocal后,当前线程也随之结束

第一种方式相对简单,直接调用ThreadLocal提供的remove方法就行,但是第二种方式就不是那么好控制了,因为如果在使用线程池的场景,第二种方式就会出问题,因为线程池中的线程有复用的情况。那么回到问题,为啥ThreadLocalMap的key偏偏要使用弱引用。其实在ThreadLocal中的get/set/getEntry方法中会对key为null的情况进行判断,如果key为null,则value也会被置为null,这时候使用弱引用就会多一层保障,假设在ThreadLocal,CurrentThread依然运行的情况下,如果忘记调用了ThreadLocal的remove方法,ThreadLocalMap的key由于是弱引用,所以可以被回收,这时候key就为null,然后在下一次ThreadLocalMap调用set、get、getEntry中的任何一个方法都会将key为null的Entry清除掉,从而避免了内存泄漏,这就是为什么ThreadLocalMap的key要使用弱引用的原因。

4.ThreadLocal使用场景

ThreadLocal目前使用最常见的就是Android中Handler机制中的Looper,

    @UnsupportedAppUsagestatic final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();// 创建Looper时需要调用Looper的prepare方法private static void prepare(boolean quitAllowed) {if (sThreadLocal.get() != null) {throw new RuntimeException("Only one Looper may be created per thread");}sThreadLocal.set(new Looper(quitAllowed));}

我们都知道Android的Handler机制中,每个线程有一个Looper,并且每个线程的Looper是互相不干扰的,要实现这种功能就得借助ThreadLocal,创建Looper的时候先判断当前创建Looper的线程是否已经有了Looper,没有再创建,有的化就直接使用。

另外还有一种情景就是在服务端开发的时候,读取MySQL的数据库连接对象,如果是多个线程去读取的情况下,每个线程都需要维护一个自己的数据库连接,这样使用完后就释放自己的连接,不会影响到其他线程的数据连接。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.xdnf.cn/news/11979.html

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

振弦式表面式应变计数据要怎么采集

振弦式表面应变计是一种专门用于测量结构表面应变的传感器&#xff0c;其数据采集过程通常涉及以下步骤&#xff1a; 一、设备准备与连接 设备检查&#xff1a;确保振弦式表面应变计及其相关设备(如MCU自动测量单元、数据传输线等)处于良好工作状态&#xff0c;无损坏或故障。 …

pitest.org使用简介

pitest.org PIT生成的报告是一种易于阅读的格式&#xff0c;结合线路覆盖和变异覆盖信息。 pitest.org官网提供了四种使用方式&#xff1a; Maven快速入门 命令行快速启动 蚂蚁快速启动 Gradle快速启动&#xff08;外部链接&#xff09; 我所使用的是Maven的方式进行构建项…

我们所有人际关系的痛苦根源,都源于缺乏边界感

在现实生活里&#xff0c;我们常会遇到这样的情况&#xff1a;对方总是越界&#xff0c;而你又不知如何拒绝&#xff0c;这种不快就会积压在心底。于是&#xff0c;我们可能会想要从其他方面突破对方的界限作为报复&#xff0c;这时关系就会变得紧张。 没有界限的关系容易让人…

JS之正则表达式

一、什么是正则表达式 <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </…

泷羽sec学习打卡-Windows基础virus

声明 学习视频来自B站UP主 泷羽sec,如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 关于windows virus的那些事儿 一、Windows-Virus资源耗尽之无限弹窗cmd-virus测试锁机virus测试无限重启…

【风力发电】基于虚拟惯性控制+一次调频+下垂控制的DFIG双馈风力发电机三机九节点仿真模型

摘要 随着风力发电在电力系统中的渗透率逐渐提高&#xff0c;如何增强风电系统的动态响应能力成为关键问题。本文针对双馈感应发电机(DFIG)&#xff0c;提出一种结合虚拟惯性控制、一次调频和下垂控制的综合控制策略&#xff0c;以改善其在电网扰动条件下的稳定性和频率响应性…

智慧社区可视化解决方案:科技引领社区服务与管理新篇章

随着社会的发展&#xff0c;智慧社区作为新型城镇化发展目标和社区服务体系建设的重要举措&#xff0c;正逐步改变着我们的生活方式。智慧社区通过综合运用现代科学技术&#xff0c;整合区域资源&#xff0c;提升社区治理和服务水平&#xff0c;为居民提供更为便捷、高效、安全…

消息队列高级

目录 消息可靠性 生产者消息确认 第一步&#xff1a;修改application.yml配置文件信息 第二步&#xff1a;定义发送者确认confirm回调方法 第三步&#xff1a;创建消息发送者回执return回调方法&#xff08;确保消息从交换机到消息队列&#xff09; 总结&#xff1a; 消息持…

乐鑫USB方案助力设备互联和数据传输,启明云端乐鑫一级代理商

USB USB 是一种通用的总线标准&#xff0c;用于连接主机和外部设备。 乐鑫 USB 方案为用户提供了方便快捷的设备互联和数据传输方式。乐鑫 SoC 通过将 USB 作为标配外设之一&#xff0c;提供 USB 2.0 OTG 或 USB-Serial-JTAG 接口&#xff0c;支持主机 (Host) 和设备 (Device…

linux详解,基本网络枚举

基本网络枚举 一、基本网络工具 ifconfig ifconfig是一个用于配置和显示网络接口信息的命令行工具。它可以显示网络接口的P地址、子网掩码、MC地址等信息&#xff0c;还可以用于启动、停止或配置网络接口。 ip ip也是用于查看和管理网络接口的命令。 它提供了比ifconfig更…

✬宁波TISAX:✬信息安全管理、✬风险评估与✬数据保护✬的集成宝典✬

&#x1f600;宁波TISAX&#xff1a;&#x1f575;️‍♀️信息安全管理、&#x1f469;‍&#x1f4bb;风险评估与&#x1f937;&#x1f3fb;‍♂️数据保护的集成宝典&#x1f468;&#x1f3fb;‍&#x1f393; &#x1f432;在当今数字化时代&#xff0c;&#x1f4bb;信息…

【软考】系统架构设计师-计算机系统基础(1):计算机硬件

知识点汇总 1、指令集 精简指令集RISC&#xff1a;寄存器&#xff0c;硬布线&#xff0c;效率高&#xff1b;复杂指令集CISC&#xff1a;微程序控制技术&#xff0c;效率低&#xff1b; 2、奇偶校验码&#xff1a;码距是2&#xff08;出错位校验位&#xff09;&#xff0c;只…

关于分治法左右区间单调遍历应该如何设计

阅读以下文章&#xff0c;首先至少要求通过一道分治法的题目或听过一道该类型的讲解。 对于分治的题目&#xff0c;想必你应该知道&#xff0c;通常我们是对于一个区间拆分两个部分&#xff0c;而最小子问题通常是只包含一个元素的区间数组。为了后续方便处理更大范围的区间&am…

Mybatis的分页插件的使用方式

插件介绍: 使用mabatis中一个名为PageHelper的插件,会把我们后面的一条SQL进行一个动态的拼接,通过拦截器对sql动态的添加limit,从而实现分页的效果 使用方式: 1.先导入相关的依赖 2.在项目中的Mapper层中对应的Mapper.xml中写动态SQL 3.在项目中的Serviceimpl层通过PageHel…

计算机信息处理技术

信息技术基础知识 数据和信息 数据 “数据是对事实、概念或指令的一种特殊表达形式&#xff0c;这种特殊表达形式可以用人工的方式或者用自动化的装置进行通信&#xff0c;翻译转换或者进行加工处理。”根据这个定义&#xff0c;数字、文字、图形、图像、声音等都是数据。数…

基于Python的膳食健康系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、Vue项目源码、SSM项目源码、微信小程序源码 精品专栏&#xff1a;…

Kafka面试题(三)

1、kafka是如何做到高效读写 1&#xff09;Kafka 本身是分布式集群&#xff0c;可以采用分区技术&#xff0c;并行度高。 2&#xff09;读数据采用稀疏索引&#xff0c;可以快速定位要消费的数据。&#xff08;mysql中索引多了之后&#xff0c;写入速度就慢了&#xff09;。 …

【Pikachu】任意文件上传实战

将过去和羁绊全部丢弃&#xff0c;不要吝惜那为了梦想流下的泪水。 1.不安全的文件上传漏洞概述 不安全的文件上传漏洞概述 文件上传功能在web应用系统很常见&#xff0c;比如很多网站注册的时候需要上传头像、上传附件等等。当用户点击上传按钮后&#xff0c;后台会对上传的…

C++【STL容器系列(二)】vector的模拟实现

文章目录 1. vector的结构2. vector的默认成员函数2.1构造函数2.1.1 默认构造2.1.2 迭代器构造2.1.3 用n个val初始化构造 2.2 拷贝构造2.3 析构函数2.4 operator 3. vector iterator函数3.1 begin 和 cbegin函数3.2 end() 和 cend()函数 4. vector的小函数4.1 size函数4.2 capa…

边缘检测的100种方法

文章目录 什么是边缘检测 ?一、边缘检测算子&#xff1a;Sobel算子、Scharr算子、Laplacian算子、Canny算子二、梯度计算 顶帽 黑帽 拉普拉斯金字塔三、相位一致性&#xff08;Phase Congruency&#xff0c;PC&#xff09;3.1、底层代码&#xff08;2D&#xff09;3.2、ski…