相信小伙伴们在工作中有听说过Threadlocal,或者在实际项目中有大量的使用Threadlocal,有些人可能没用过,不过没关系,通过本文你就能从小白到高手蜕变,如果使用过,同样也能收获不一样的知识点。
什么是ThreadLocal
全称thread local variable(线程局部变量)功用非常简单,使用场合主要解决多线程中数据因并发产生不一致问题。ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享,这样的结果是耗费了内存
但大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。 其核心只做了一件事情:同个线程共享数据。
使用时需注意的是ThreadLocal不能使用原子类型,只能使用Object类型
应用场景
ThreadLocal 用作每个线程内需要独立保存信息,方便同个线程的其他方法获取该信息的场景。 每个线程获取到的信息可能都是不一样的,前面执行的方法保存了信息后,后续方法可以通过ThreadLocal 直接获取到,避免了传参,类似于全局变量的概念,比如用户登录令牌解密后的信息传递(用户权限信息、从用户系统获取到的用户名、用户ID)。
假设利用Threadlocal实现赛跑Demo,其主要利用了线程外隔离,线程内变量共享的特性。
public class Runner {
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(()->0);
public void run(){
Integer distenc = threadLocal.get();
threadLocal.set(++distenc);
}
public void sore(){
String name = Thread.currentThread().getName();
Integer distenc = threadLocal.get();
System.out.println(name + " " + distenc);
}
public static void main(String[] args) {
Runner runner = new Runner();
// 张三一小时跑10公里
new Thread(()->{
for (int i = 0; i < 10; i++) {
runner.run();
}
runner.sore();
}, "张三").start();
// 李四一小时跑5公里
new Thread(()->{
for (int i = 0; i < 5; i++) {
runner.run();
}
runner.sore();
}, "李四").start();
// 王五一小时跑3公里
new Thread(()->{
for (int i = 0; i < 3; i++) {
runner.run();
}
runner.sore();
}, "王五").start();
}
}
当使用threadlocal完成后需要remove,如runner.threadLocal.remove();其remove原因后面将详细讲解
ThreadLocal核心源码解读
查看源码
三者之间关系(Thread 、ThreadLocal、ThreadLocalMap)
Thread类中维护着一个ThreadLocalMap类型的变量,变量名为threadLocals,它在ThreadLocal类中声明,并作为静态内部类来存在。ThreadLocal提供了一系列方法用于操作ThreadLocalMap(且仅能使用ThreadLocal操作),如get/set/remove等操作。它的作用用于隔离Thread和ThreadLocalMap,防止直接创建ThreadLocalMap,并且自身的get/set内部会判断当前线程是否已经绑定一个ThreadLocalMap,有就继续使用,没有就为其自身绑定。
ThreadLocalMap就是保存ThreadLocal的map结构,key就是ThreadLocal本身
所以一个线程只能存储一个值,可以理解为JVM内部维护的一个Map<Thread, Object>,当线程需要用到Object,就用【当前线程】去Map里面获取对应的Object。
简而言之,ThreadLocal本身并不存储值 ( 是一个壳子 ), 它只是自己作为一个key来让线程从ThreadLocalMap获取value。因此,ThreadLocal能够实现 “每个线程之间的数据隔离”,获取当前线程的局部变量值,不受其他线程影响。
JVM的四大引用类型
为了更加深入理解Theadlocal,我们需要先了解一下JVM的四大引用类型。
强引用
强引用是使用最普遍的引用,当一个对象被强引用关联后,它就不会被垃圾回收器回收
比如String str = “abc”,变量str就是字符串“abc”的强引用,即使在【内存不足】的情况下,JVM宁愿抛出OutOfMemoryError,也不会回收这种对象。
具体编码
1)创建User类,覆盖finalize()函数,在finalize()中输出打印信息,方便追踪,来完成“非内存资源”的清理工作。
2)finalize()是Object基类的一个方法,在JVM回收内存时执行的,是GC前对待回收的对象进行标记,标记成功后会回调此函数;
3)JVM并不保证在回收内存时一定会调用finalize(),可以用这个System.gc(),手动提醒GC,可以进行标记并回收垃圾。
public class User {
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("User对象被标记为垃圾了");
}
}
public static void main(String[] args) throws InterruptedException {
testReference();
}
/**
* 强引用测试
* @throws InterruptedException
*/
public static void testReference() throws InterruptedException {
User user = new User();
System.out.println("GC前 = "+user);
user = null;
System.gc();
TimeUnit.SECONDS.sleep(1);
System.out.println("GC后 = "+user);
}
弱引用
软引用是用来描述一些还有用但非必需的对象,当系统内存资源不足时,垃圾回收器会回收这些对象。只有当内存不足时,才会回收软引用关联的对象;当内存资源充足时,不会回收软引用关联的对象,直接调用GC也不回收。一般在高速缓存中会使用,内存不够时则回收相关对象释放内存。使用 SoftReference< > 包装对象就可以转换为软引用。
具体编码
/**
* 只有当内存不足时,才会回收 软引用 关联的对象。当内存资源充足时,不会回收软引用关联的对象, 不用手工调用GC
* -Xms100m -Xmx100m
*/
public static void testSoftReference() {
SoftReference<User> softReference = new SoftReference<>(new User());
System.out.println("内存够用,GC前 = " + softReference.get());
try {
TimeUnit.SECONDS.sleep(1);
consumeMemory();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("内存不够用,GC后 = " + softReference.get());
}
try {
TimeUnit.SECONDS.sleep(1);}catch (Exception e){e.printStackTrace();}
}
/**
* 消耗大量内存, -Xms100m -Xmx100m
*/
public static void consumeMemory() {
List<Byte[]> list = new ArrayList<>();
for (int i = 0; i < 99; i++) {
//1MB
Byte[] bytes = new Byte[1 * 1024 * 1024];
list.add(bytes);
}
}
//输出下面
内存够用,GC前 = thread.User@9c71l1g1
User对象被标记为垃圾了
内存不够用,GC后 = null
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at thread.ReferenceTest.consumeMemory(ReferenceTest.java:63)
at thread.ReferenceTest.testSoftReference(ReferenceTest.java:47)
at thread.ReferenceTest.main(ReferenceTest.java:19)
弱引用
弱引用也是用来描述非必需对象,但是它的强度比软引用更弱一些,只能生存到下一次垃圾收集发生之前,只要垃圾回收器工作时,无论内存是否充足,都会回收被弱引用关联的对象。使用了WeakReference类来实现弱引用。
/**
* 只要垃圾回收器工作时,无论内存是否充足,都会回收被弱引用关联的对象。
*/
public static void testWeakReference() {
WeakReference<User> weakReference = new WeakReference<>(new User());
System.out.println("内存够用,GC前 = " + weakReference.get());
try {
TimeUnit.SECONDS.sleep(1);
System.gc();
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println("无论内存是否充足,GC后 = " + weakReference.get());
}
try {
TimeUnit.SECONDS.sleep(1);
} catch (Exception e) {
e.printStackTrace();
}
}
虚引用
最弱的一种引用关系,一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。一个对象设置虚引用关联,目的就是能在这个对象被收集器回收时收到一个系统通知。使用PhantomReference类来实现虚引用,必需要组合使用一个引用队列ReferenceQueue。当垃圾回收器要回收一个对象时,如果发现它还有虚引用,会在回收对象的内存之前,把这个虚引用加入到关联的引用队列中。在虚引用对象传到它的引用队列之前会调用对象的finalize方法。
/**
* 弱引用和虚引用指向的对象在发生GC时一定会被回收,虚引用是得不到引用的对象实例
*/
public static void testPhantomReference() {
ReferenceQueue queue = new ReferenceQueue();
PhantomReference phantomReference = new PhantomReference(new User(), queue);
System.out.println("内存够用,GC前 = " + phantomReference.get());
System.gc();
consumeMemory();
}
整体分析
引用类型 | 被垃圾回收时刻 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 从来不会 | 对象的一般状态 | JVM停止运行时终止 |
软引用 | 在内存不足时 | 对象简单,缓存,文件缓存,图片缓存 | 内存不足时终止 |
弱引用 | 在gc垃圾回收时 | 对象简单,缓存,文件缓存,图片缓存 | gc运行后终止 |
虚引用 | 任何时候都可能被垃圾回收器回收 | 基本不写,虚拟机使用, 用来跟踪对象被垃圾回收器回收的活动 | 未知 |
使用完threadLocal为什么要remove?
前面已讲解到,ThreadLocal的ThreadLocalMap是一个静态内部类,每个线程都将持有一个ThreadLocalMap对象,即每一个新的线程Thread都会实例化一个ThreadLocalMap,并将其赋值给成员变量threadLocals,使用时若已经存在threadLocals,则直接使用已经存在的对象。
但通过源码可以发现这个类没有实现map接口,即是一个普通的Java类,但是该类实现了类似于map的功能,里面每个数据都用Entry来保存。
Key问题回收
其继承了WeakReference指向ThreadLocal(弱引用)键值对存储,键为ThreadLocal的自身引用。其设计为弱引用目的在于如果ThreadLocal的引用丢失,TheadLocalMap的key能够被GC回收,避免了内存泄露问题。
当执行以下代码时将会产生以下引用关系:
public static void main(String[] args) {
ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set(new User());
threadLocal = null;
}
Value问题回收
由于Key是弱引用被回收了,然后key是null,但是value是强引用对象没法被回收和访问,就导致内存泄露,所以使用完theadlocal就需要remove相关的value。
常规使用的线程,如果线程对象结束被回收,则上面的key和value都可以被回收,但是业务里面多数是使用线程池,就导致线程不能被回收,从而如果没remove对应的值,则会导致OOM。常规set/get方法里面也会清除key为null的entry对象的方法,但实际开发还是需要直接调用remove方法删除。
Theadlocal使用案例传送门:分布式应用下登录检验解决方案-CSDN博客