第十章:聊聊ThreadLocal
创始人
2024-03-04 06:50:49
0

  • 是什么?
  • 能干嘛?
  • 常用API
    • 案例一
    • 以上代码存在的问题?
    • 演示线程池复用本地变量的情况
  • ThreadLocal源码
    • Thread,ThreadLocal,ThreadLocalMap 三者的关系?
    • ThreadLocal 的 get 方法
    • set、remove 方法
    • 总结
  • ThreadLocal 之内存泄漏
    • 四大引用
    • ThreadLocal为什么要使用弱引用
    • ThreadLocal使用弱引用就没有问题了吗?
  • 总结

是什么?

官网描述

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

能干嘛?

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题

在之前多个线程共享同一个数据就有可能存在数据安全问题,因此使用 synchronized、lock/unlock、CAS 保证安全

而ThreadLocal可以让每个线程都有一份数据,这样就避免了多个线程同时操作同一个数据而带来的线程安全问题。

常用API

构造器:

Constructor and Description
ThreadLocal() 创建线程局部变量。

常用方法:

Modifier and TypeMethod and Description
Tget() 返回当前线程的此线程局部变量的副本中的值。
protected TinitialValue() 返回此线程局部变量的当前线程的“初始值”。
voidremove() 删除此线程局部变量的当前线程的值。
voidset(T value) 将当前线程的此线程局部变量的副本设置为指定的值。
static ThreadLocalwithInitial(Supplier supplier) 创建线程局部变量。

案例一

通过一个案例熟悉使用 ThreadLocal

某房地产公司要求统计销售员总共卖出的房子(假设有5个销售员)

代码

public class SaleHouseDemo {public static void main(String[] args) {SaleHouse house = new SaleHouse();for (int i = 1; i <= 5; i++) {new Thread(() -> {int size = new Random().nextInt(5);System.out.println(Thread.currentThread().getName() + " 卖出 " + size);for (int i1 = 0; i1 < size; i1++) {house.saleHouse();}}, String.valueOf(i)).start();}try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("总共卖出: " + house.saleCount);}
}// 资源类
class SaleHouse {int saleCount = 0 ;public synchronized void saleHouse() {++saleCount;}
}5 卖出 4
3 卖出 4
1 卖出 2
2 卖出 4
4 卖出 3
总共卖出: 17

此时修改一下需求,不参与统计,每个销售员自己算自己的。

就需要使用 ThreadLocal 为每一个线程绑定属于自己的变量。不需要别的线程干涉。

ThreadLocal 初始化变量的俩种方式

  • 重写 initialValue 方法
    • 不推荐这种方式,太繁琐。
    // 原始版本,不推荐ThreadLocal threadLocal = new ThreadLocal(){@Overrideprotected Integer initialValue() {return 0; // 变量的初始值为0}};
  • 调用静态方法 withInitial
    // 调用静态方法: 推荐ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0);

代码演示

public class SaleHouseDemo {public static void main(String[] args) {SaleHouse house = new SaleHouse();for (int i = 1; i <= 5; i++) {new Thread(() -> {int size = new Random().nextInt(5);for (int i1 = 0; i1 < size; i1++) {house.saleHouseByThreadLocal();}System.out.println(Thread.currentThread().getName() + " 卖出 " + house.threadLocal.get());}, String.valueOf(i)).start();}try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}// System.out.println("总共卖出: " + house.saleCount);}
}// 资源类
class SaleHouse {int saleCount = 0 ;public synchronized void saleHouse() {++saleCount;}// 原始版本,不推荐// ThreadLocal threadLocal = new ThreadLocal(){//     @Override//     protected Integer initialValue() {//         return 0;//     }// };// 调用静态方法: 推荐ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0);public void saleHouseByThreadLocal() {threadLocal.set(1 + threadLocal.get());}
}5 卖出 1
2 卖出 3
1 卖出 1
4 卖出 1
3 卖出 4

以上代码存在的问题?

在使用完本地变量后,要记着使用 remove 方法清除。否则会造成 内存泄漏 的问题。

其实在以上场景中,你会发现并没有出现问题,因为我们每次创建线程都是新 new 的,线程也没有重复使用。但是在线程池的场景下,就可能出现问题了,请看阿里巴巴规范的说明。

阿里巴巴规范

image-20221129221743063

当使用完 本地变量,要在 finally 语句块里清除。

修改以上代码
image-20221129222958892

演示线程池复用本地变量的情况

阿里规范中说明:如果本地变量使用完未清除,在线程池的场景下,很可能会出现重复利用的问题。

演示代码

public class ThreadLocalTest {public static void main(String[] args) {Resource resource = new Resource();ExecutorService executorService = Executors.newFixedThreadPool(3);try {for (int i = 0; i < 10; i++) {executorService.submit(() -> {Integer begin = resource.threadLocal.get();resource.add();Integer end = resource.threadLocal.get();System.out.println(Thread.currentThread().getName() + " begin = " + begin + " end = " + end);});}} finally {executorService.shutdown();}}
}class Resource {int num = 0;ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 0);public void add() {threadLocal.set(1 + threadLocal.get());}
}

输出结果:

pool-1-thread-1 begin = 0 end = 1
pool-1-thread-3 begin = 0 end = 1
pool-1-thread-2 begin = 0 end = 1
pool-1-thread-1 begin = 1 end = 2
pool-1-thread-2 begin = 1 end = 2
pool-1-thread-3 begin = 1 end = 2
pool-1-thread-2 begin = 2 end = 3
pool-1-thread-1 begin = 2 end = 3
pool-1-thread-2 begin = 3 end = 4
pool-1-thread-3 begin = 2 end = 3

在每次执行的任务中,有的任务重复利用了之前任务的变量值,这很有可能影响后序的逻辑,因此在使用完本地变量时,要记着清除~!

image-20221129224107694

执行结果:

在这里插入图片描述

ThreadLocal源码

Thread,ThreadLocal,ThreadLocalMap 三者的关系?

线程类 Thread 中存在有 ThreadLocalMap的属性,每次新建Thread 都会有一个新的 ThreadLocalMap, 这就是为什么 每个线程独有一份 本地变量

image-20221130113918695

ThreadLocalMap 是 ThreadLocal 的一个静态内部类,并且静态内部类Entry 继承了 WeakReference 弱引用

image-20221130115219017

三者关系图
在这里插入图片描述

  • threadLocalMap实际上就是一个以 threadLocal实例为key,任意对象为value的Entry对象。
  • 当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放

如果对这俩句话还是不懂的话,继续往下看源码。

ThreadLocal 的 get 方法

get 方法用于获取当前线程本地变量的副本

    public T get() {// 首先获取当前线程Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap,具体的获取方法就是调用Thread类中的 threadlocals 属性。参考图1ThreadLocalMap map = getMap(t);if (map != null) {// 如果不为空,就获取 ThreadLocal对象的 Entry 对象ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")// 如果 e 不为null,就通过Entry对象获取value,也就是本地变量的值。T result = (T)e.value;return result;}}// 如果 map = null ,也就是在我们创建 ThreadLocal 没有进行初始化。会执行这个方法进行初始化return setInitialValue();}

在这里插入图片描述

初始化 ThreadLocalMap

    private T setInitialValue() {// 参考图二:对本地变量进行初始化 null ,这就是为什么说在创建ThreadLocal时一定要初始化变量,否则变量值就是 nullT value = initialValue();// 获取当前线程Thread t = Thread.currentThread();// 获取当前线程的 ThreadLocalMapThreadLocalMap map = getMap(t);if (map != null)// map 不为空,将当前对象(ThreadLocal)作为key,本地变量值作为 value 增加到map中map.set(this, value);else// 如果map为空,就新创建一个Map,参考图三createMap(t, value);return value;}

image-20221130114906192

创建一个 ThreadLocalMap,仍然是以 ThreadLocal对象为 key,本地变量值为 value

在这里插入图片描述

set、remove 方法

弄清楚了 get() 方法,set和 remove 方法就很好懂了。

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);}

总结

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:

image-20221130120651236

JVM内部维护了一个线程版的Map(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中), 每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

ThreadLocal 之内存泄漏

阿里巴巴的规范手册中说明:

如果使用完本地变量后,不进行回收,可能会造成内存泄漏

在这里插入图片描述

为什么会造成内存泄漏呢?谁造成的呢?

正是存入 ThreadLocalMap 中的 Entry 对象。

回顾一下ThreadLocalMap

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:
(1)第一层包装是使用 WeakReference> 将ThreadLocal对象变成一个 弱引用对象;

(2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference>

在这里插入图片描述

四大引用

关于什么是 四大引用,我在 JVM 篇章中也有过介绍,这里不做多说明。包括 对象的finalization机制

弱引用和ThreadLocal的关系

image-20221130150840933

每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储:

调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象

ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~

ThreadLocal为什么要使用弱引用

看一下这个ThreadLocal代码:

    public void function01() {ThreadLocal tl = new ThreadLocal<>();tl.set(1);tl.get();}
 

对应的内存关系图

局部变量 tl 会保存虚拟机栈中,指向堆中的 THreadLocal 的对象,这就是强引用。

而在执行 set 方法时,该线程的 ThreadLocalMap 的key 会指向ThreadLocal 对象。

  • 若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏
  • 若这个key引用是弱引用就大概率会减少内存泄漏的问题(还有一个key为null的雷)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。

image-20221130153025854

ThreadLocal使用弱引用就没有问题了吗?

虽然 ThreadLocal 使用了若引用减少了内存泄漏的几率。但还可能会存在内存泄漏。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(比如正好用在线程池),这些key为null的Entry的value就会一直存在一条强引用链。

虽然弱引用,保证了key指向的ThreadLocal对象能被及时回收,但是v指向的value对象是需要ThreadLocalMap调用get、set时发现key为null时才会去回收整个entry、value,因此弱引用不能100%保证内存不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。

总结起来就俩句话

  • ThreadLocalMap使用 ThreadLocal的弱引用作为 key,当 ThreadLocal没有外部引用 指向它的时候,能够被 GC 回收,也就是 将 key 设置为 null
  • 虽然 ThreadLocal 能被回收,但是 value 还是存在的(和HashMap一样,允许key为null 存在)。因此使用弱引用不能百分百保证不会出现内存泄漏,还需要在使用完 ThreadLocal 后调用remove方法清除

在上面说,set、get方法会去检查所有键为null的Entry对象,那么在源码中时如何体现的?

get方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hd5d12xm-1669802720879)(https://images-1313160403.cos.ap-beijing.myqcloud.com/MarkDown/202211301804535.png)]

getEntryAfterMiss 调用了 expungeStaleEntry 方法,在此方法中,将value设置为了null

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F1FclbVe-1669802720880)(https://images-1313160403.cos.ap-beijing.myqcloud.com/MarkDown/202211301804536.png)]

set 方法

image-20221130160326978

remove 方法:

同样调用了expungeStaleEntry 方法

image-20221130160500960

总结

从前面的set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。

总结

ThreadLocal 使用建议:

  1. 一定要进行初始化避免空指针问题ThreadLocal.withInitial(()- > 初始化值); 【强制】
  2. 建议把ThreadLocal修饰为static【建议】
  3. 用完记得手动remove 【强制】

阿里巴巴规范

image-20221130160824536

ThreadLocal能够实现线程数据隔离,不在于他自己本身,而是在于 Thread类中的ThreadLocalMap 。

所以 THreadLocal 可以只初始化一次,只分配一块内存空间即可。没必要作为成员变量初始化多次。


  • ThreadLocal 并不解决线程间共享数据的问题
  • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal 通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个只属于自己的专属Map并维护了ThreadLocal对象与具体实例的映射,
  • 该Map由于只被持有它的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用,避免了ThreadLocal对象无法被回收的问题
  • 都会通过expungeStaleEntry, cleanSome Slots,replaceStaleEntry这三个方法回收键为 null 的 Entry
    .(img-CkDpd1kt-1669802720882)]

ThreadLocal能够实现线程数据隔离,不在于他自己本身,而是在于 Thread类中的ThreadLocalMap 。

所以 THreadLocal 可以只初始化一次,只分配一块内存空间即可。没必要作为成员变量初始化多次。



各位彭于晏,如有收获点个赞不过分吧…✌✌✌

Alt


gongzhonghao 回复 [JUC] 获取MarkDown笔记

相关内容

热门资讯

AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWR报告解读 WORKLOAD REPOSITORY PDB report (PDB snapshots) AW...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
群晖外网访问终极解决方法:IP... 写在前面的话 受够了群晖的quickconnet的小水管了,急需一个新的解决方法&#x...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
Azure构建流程(Power... 这可能是由于配置错误导致的问题。请检查构建流程任务中的“发布构建制品”步骤,确保正确配置了“Arti...