深入了解ThreadLocal底层原理-高并发架构
目录
- 什么是ThreadLocal
- 应用场景
- 需求实现
- ThreadLocal核心源码解读
- Thread 、ThreadLocal、ThreadLocalMap 三者的关系
- 四大引用-强软弱虚类型
- ThreadLocal内存泄漏
- ThreadLocal为什么需要设计成弱引用?并且ThreadLocal用完需要remove呢?
- 原因
什么是ThreadLocal
- 全称
thread local variable
(线程局部变量)功用非常简单,使用场合主要解决多线程中数据因并发产生不一致问题 ThreadLocal
为每一个线程都提供了变量的副本,使得每个线程在某时间访问到的并不是同一个对象- 这样就
隔离了多个线程对数据的数据共享
,这样的结果是耗费了内存
- 但是大大
减少了线程同步所带来性能消耗
,也减少了线程并发控制的复杂度
- 总结概括起来就是:
同个线程共享数据
- 注意:
ThreadLocal
不能使用原子类型
,只能使用Object类型
应用场景
- ThreadLocal 用作每个线程内需要独立保存信息,方便同个线程的其他方法获取该信息的场景
- 每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal 直接获取到
- 避免了传参,类似于全局变量的概念
- 比如用户登录令牌解密后的信息传递(用户权限信息、从用户系统获取到的用户名、用户ID)
- 比如用户登录令牌解密后的信息传递(用户权限信息、从用户系统获取到的用户名、用户ID)
需求实现
- 小作坊举办了德州扑克游戏,只要all in一次就可以挣钱
- 开发一个程序,记录每个人all in 多次后,输出每个人赢得钱总额
public class PokerGame {
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->0);
public void makeMoney(){
String name = Thread.currentThread().getName();
Integer amount = threadLocal.get();
threadLocal.set(++amount);
System.out.println(name+" all in");
}
public void showMoney(){
String name = Thread.currentThread().getName();
System.out.println(name+"总共挣钱:"+threadLocal.get());
}
public static void main(String[] args) {
PokerGame pokerGame = new PokerGame();
new Thread(()->{
for(int i=0;i<10;i++){
pokerGame.makeMoney();
}
pokerGame.showMoney();
},"张三").start();
new Thread(()->{
for(int i=0;i<4;i++){
pokerGame.makeMoney();
}
pokerGame.showMoney();
},"李四").start();
new Thread(()->{
for(int i=0;i<7;i++){
pokerGame.makeMoney();
}
pokerGame.showMoney();
},"王二").start();
}
}
- 上文代码有啥问题
- 是的,用完需要remove
new Thread(() -> {
try {
for (int i = 0; i < 10; i++) {
pokerGame.makeMoney();
}
pokerGame.showMoney();
} finally {
pokerGame.threadLocal.remove();
}
}, "王二").start();
- 为什么需要remove呢,继续往下看
ThreadLocal核心源码解读
Thread 、ThreadLocal、ThreadLocalMap 三者的关系
- 关系
- Thread类,里面有一个ThreadLocalMap类型的变量,变量名字叫
threadLocals
,是ThreadLocalMap类型 ThreadLocal
类,里面有一个ThreadLocalMap
静态内部类- 提供一系列方法操作ThreadLocalMap,比如get/set/remove
- 隔离
Thread
和ThreadLocalMap
,防止直接创建ThreadLocalMap
- 自身的get/set内部会判断当前线程是否已经绑定一个ThreadLocalMap,有就继续使用,没有就为其自身绑定
- ThreadLocalMap 就是保存ThreadLocal的map结构,key就是ThreadLocal本身
- 所以一个线程只能存储一个值,可以理解为JVM内部维护的一个Map<Thread, Object>
- 当线程需要用到Object,就用【当前线程】去Map里面获取对应的Object
- Thread类,里面有一个ThreadLocalMap类型的变量,变量名字叫
- 操作
- Thread类里面有一个ThreadLocalMap类型的变量,不能直接操作这个ThreadLocalMap
- 需要通过【工具箱】ThreadLocal才可以操作ThreadLocalMap
- 一个Thread只能有一个ThreadLocalMap
- ThreadLocalMap以ThreadLocal为键存储数据
- 总结
- ThreadLocal本身并不存储值 ( 是一个壳子 ), 它只是自己作为一个key来让线程从ThreadLocalMap获取value
- 因为这个原理,所以ThreadLocal能够实现 “每个线程之间的数据隔离”,获取当前线程的局部变量值,不受其他线程影响
四大引用-强软弱虚类型
-
强引用
- 强引用是使用最普遍的引用,当一个对象被强引用关联后,它就不会被垃圾回收器回收
- 比如String str = “abc”,变量str就是字符串“abc”的强引用
- 即使在【内存不足】的情况下,JVM宁愿抛出OutOfMemoryError,也不会回收这种对象
-
软引用
- 软引用是用来描述一些还有用但非必需的对象,当系统内存资源不足时,垃圾回收器会回收这些对象
- 只有当内存不足时,才会回收软引用关联的对象;当内存资源充足时,不会回收软引用关联的对象,直接调用GC也不回收
- 一般在高速缓存中会使用,内存不够时则回收相关对象释放内存
- 使用 SoftReference< > 包装对象就可以转换为软引用
-
弱引用
- 弱引用也是用来描述非必需对象,但是它的强度比软引用更弱一些,只能生存到下一次垃圾收集发生之前
- 只要垃圾回收器工作时,无论内存是否充足,都会回收被弱引用关联的对象。
- 使用了WeakReference类来实现弱引用
-
虚引用
- 最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例
- 一个对象设置虚引用关联,目的就是能在这个对象被收集器回收时收到一个系统通知
- 使用 PhantomReference类来实现虚引用,必需要组合使用一个引用队列ReferenceQueue
- 当垃圾回收器要回收一个对象时,如果发现它还有虚引用,会在回收对象的内存之前,把这个虚引用加入到关联的引用队列中
- 在虚引用对象传到它的引用队列之前会调用对象的finalize方法
引用类型 | 被垃圾回收时刻 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 在内存不足时 | 对象简单,缓存,文件缓存,图片缓存 | 内存不足时终止 |
弱引用 | 在gc垃圾回收时 | 对象简单,缓存,文件缓存,图片缓存 | gc运行后终止 |
虚引用 | 任何时候都可能被垃圾回收器回收 | 基本不写,虚拟机使用, 用来跟踪对象被垃圾回收器回收的活动 | 未知 |
ThreadLocal内存泄漏
ThreadLocal为什么需要设计成弱引用?并且ThreadLocal用完需要remove呢?
- ThreadLocal中的一个内部类ThreadLocalMap,这个类没有实现map接口,是一个普通的Java类,但是实现的类似map的功能
- 每个数据用Entry保存,继承WeakReference 指向**ThreadLocal(所以是弱引用)键值对存储,键为ThreadLocal的自身引用**
- 每个线程持有一个ThreadLocalMap对象,每一个新的线程Thread都会实例化一个ThreadLocalMap
- 并赋值给成员变量threadLocals,使用时若已经存在threadLocals,则直接使用已经存在的对象
原因
- 两个原因分析 ThreadLocalMap 的key和value造成内存泄露解决方案
- Key问题回收
- 如果ThreadLocal的引用丢失,ThreadLocalMap的Key如果是强引用,则没法被回收, 造成泄露
- 所以设计成弱引用,这个时候触发GC时,Key必定会被回收
- 这个操作是ThreadLocal自身设计进行了解决
- Value问题回收
- 由于Key是弱引用被回收了,然后key是null,但是value是强引用对象没法被回收和访问,就导致内存泄露
- 所以用完需要remove相关的value,这个操作需要开发人员进行操作
- 开发中需要注意的点
- 常规使用的线程,如果线程对象结束被回收,则上面的key和value都可以被回收
- 但是在实际业务里面多数是使用线程池,就导致线程不能被回收,从而如果没remove对应的值,则会导致OOM
- 常规set/get方法里面也会清除key为null的entry对象的方法,但实际开发还是需要直接调用remove方法删除