官网描述:
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
在之前多个线程共享同一个数据就有可能存在数据安全问题,因此使用 synchronized、lock/unlock、CAS 保证安全
而ThreadLocal可以让每个线程都有一份数据,这样就避免了多个线程同时操作同一个数据而带来的线程安全问题。
构造器:
Constructor and Description |
---|
ThreadLocal() 创建线程局部变量。 |
常用方法:
Modifier and Type | Method and Description |
---|---|
T | get() 返回当前线程的此线程局部变量的副本中的值。 |
protected T | initialValue() 返回此线程局部变量的当前线程的“初始值”。 |
void | remove() 删除此线程局部变量的当前线程的值。 |
void | set(T value) 将当前线程的此线程局部变量的副本设置为指定的值。 |
static | withInitial(Supplier extends S> 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 初始化变量的俩种方式:
// 原始版本,不推荐ThreadLocal threadLocal = new ThreadLocal(){@Overrideprotected Integer initialValue() {return 0; // 变量的初始值为0}};
// 调用静态方法: 推荐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 的,线程也没有重复使用。但是在线程池的场景下,就可能出现问题了,请看阿里巴巴规范的说明。
阿里巴巴规范:
当使用完 本地变量,要在 finally 语句块里清除。
修改以上代码:
阿里规范中说明:如果本地变量使用完未清除,在线程池的场景下,很可能会出现重复利用的问题。
演示代码:
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
在每次执行的任务中,有的任务重复利用了之前任务的变量值,这很有可能影响后序的逻辑,因此在使用完本地变量时,要记着清除~!
执行结果:
线程类 Thread 中存在有 ThreadLocalMap的属性,每次新建Thread 都会有一个新的 ThreadLocalMap, 这就是为什么 每个线程独有一份 本地变量
ThreadLocalMap 是 ThreadLocal 的一个静态内部类,并且静态内部类Entry 继承了 WeakReference 弱引用
三者关系图:
如果对这俩句话还是不懂的话,继续往下看源码。
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;}
创建一个 ThreadLocalMap,仍然是以 ThreadLocal对象为 key,本地变量值为 value
弄清楚了 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对象:
JVM内部维护了一个线程版的Map
阿里巴巴的规范手册中说明:
如果使用完本地变量后,不进行回收,可能会造成内存泄漏
为什么会造成内存泄漏呢?谁造成的呢?
正是存入 ThreadLocalMap 中的 Entry 对象。
回顾一下ThreadLocalMap :
ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象:
(1)第一层包装是使用 WeakReference
(2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference
关于什么是 四大引用,我在 JVM 篇章中也有过介绍,这里不做多说明。包括 对象的finalization机制
弱引用和ThreadLocal的关系
每个Thread对象维护着一个ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储:
调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象
调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象
ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响~
看一下这个ThreadLocal代码:
public void function01() {ThreadLocal
对应的内存关系图:
局部变量 tl 会保存虚拟机栈中,指向堆中的 THreadLocal 的对象,这就是强引用。
而在执行 set 方法时,该线程的 ThreadLocalMap 的key 会指向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。
总结起来就俩句话:
在上面说,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 方法:
remove 方法:
同样调用了expungeStaleEntry 方法
总结
从前面的set,getEntry,remove方法看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法清理掉key为null的脏entry。
ThreadLocal 使用建议:
阿里巴巴规范:
ThreadLocal能够实现线程数据隔离,不在于他自己本身,而是在于 Thread类中的ThreadLocalMap 。
所以 THreadLocal 可以只初始化一次,只分配一块内存空间即可。没必要作为成员变量初始化多次。
ThreadLocal能够实现线程数据隔离,不在于他自己本身,而是在于 Thread类中的ThreadLocalMap 。
所以 THreadLocal 可以只初始化一次,只分配一块内存空间即可。没必要作为成员变量初始化多次。
各位彭于晏,如有收获点个赞不过分吧…✌✌✌
gongzhonghao 回复 [JUC] 获取MarkDown笔记
下一篇:低代码助力生产管理:车间管理系统