文章目录:
1.什么是ThreadLocal?
1.1 api介绍
1.2 最简单的案例认识ThreadLocal
1.3 线程池结合ThreadLocal案例
2.Thread &ThreadLocal & ThreadLocalMap
3.ThreadLocal内存泄漏问题
3.1 四大引用之强引用
3.2 四大引用之软引用
3.3 四大引用之弱引用
3.4 四大引用之虚引用
3.5 ThreadLocal为什么是弱引用?
4.小总结
1.什么是ThreadLocal?
ThreadLocal提供线程局部变量,这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。 ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。
1.1 api介绍
ThreadLocal是位于 java.lang 包下的,从jdk官方文档中可以看到它所有的api。而它的构造方法其实就是 withInitial(Supplier)。
1.2 最简单的案例认识ThreadLocal
希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计。比如某房产中介销售都有自己的销售额指标,自己专属于自己的,不和别人掺和。
- 使用ThreadLocal
- 先初始化,给个0值
- 利用set get方法
- ------注意,也要调用remove() 接口,不然容易导致内存泄漏。(在阿里巴巴Java开发手册中也有说明)
package com.juc.threadlocal;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @author: SongZiHao
* @date: 2023/2/13
*/
class House {
public int saleCount = 0;
public synchronized void saleHouse() {
++saleCount;
}
// ThreadLocal<Integer> saleVolume = new ThreadLocal<Integer>() {
// @Override
// protected Integer initialValue() {
// return 0;
// }
// };
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
public void saleVolumeByThreadLocal() {
saleVolume.set(saleVolume.get() + 1);
}
}
public class ThreadLocalDemo1 {
public static void main(String[] args) {
House house = new House();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
int size = new Random().nextInt(5) + 1;
try {
for (int j = 0; j < size; j++) {
house.saleHouse();
house.saleVolumeByThreadLocal();
}
System.out.println(Thread.currentThread().getName() + " 号销售卖出多少套:" + house.saleVolume.get());
} finally {
house.saleVolume.remove();
}
}, String.valueOf(i)).start();
}
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 共计卖出多少套:" + house.saleCount);
}
}
1.3 线程池结合ThreadLocal案例
根据阿里规范,需要对自定义的ThreadLocal进行回收,否则容易造成内存泄漏和业务逻辑问题(因为线程池中的线程会复用)。
package com.juc.threadlocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author: SongZiHao
* @date: 2023/2/13
*/
class MyData {
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void add() {
threadLocal.set(threadLocal.get() + 1);
}
}
public class ThreadLocalDemo2 {
public static void main(String[] args) {
MyData data = new MyData();
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
for (int i = 0; i < 10; i++) {
threadPool.submit(() -> {
try {
Integer beforeValue = data.threadLocal.get();
data.add();
Integer afterValue = data.threadLocal.get();
System.out.println(Thread.currentThread().getName() + " before: " + beforeValue + ", after: " + afterValue);
} finally {
data.threadLocal.remove();
}
});
}
} catch (Exception ex) {
ex.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
如果上面的代码没有 finally + remove 了话,那么每个线程的value值就会累积,变成2、3、4等等,这是因为这些线程会被复用。
2.Thread &ThreadLocal & ThreadLocalMap
我们如果随便翻翻源码了话,可能会经常看到三个类:Thread,ThreadLocal,ThreadLocalMap。
- 根据官方API,Thread是程序中执行的线程;ThreadLocal类提供线程局部变量。
- 先打开
Thread.java
类,发现每个Thread类里面有一个ThreadLocal
类,而ThreadLocalMap
是ThreadLocal
的一个静态内部类。
下面是ThreadLocalMap这个静态内部类,熟悉HashMap的同学看到这里肯定不陌生,还是熟悉的配方、还是熟悉的味道,设置k-v键值对,初始化容量16。
threadLocalMap
实际上就是一个以threadLocal
实例为key,任意对象为value的Entry对象
。
当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放。
JVM内部维护了一个线程版的Map<Thread, T>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中,每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。
每个Thread对象维护着一个ThreadLocalMap的引用。ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储。
- 调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key是ThreadLocal对象,值Value是传递进来的对象。
- 调用ThreadLocal的get()方法时,实际上就是往ThreadLocalMap获取值,key是ThreadLocal对象。
- ThreadLocal本身并不存储值,它只是自己作为一个key来让线程从ThreadLocalMap获取value,正因为这个原理,所以ThreadLocal能够实现“数据隔离”,获取当前线程的局部变量值,不受其他线程影响。
Thread就是一个大活人,ThreadLocal就是身份证原件,ThreadLocalMap就是身份证上的信息(姓名:张三;出生年月:xxxx年y月z日)这不就是个k-v的Map吗?
那么Thread,ThreadLocal,ThreadLocalMap三者可以用下面这张图概括。
3.ThreadLocal内存泄漏问题
3.1 四大引用之强引用
Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。
官方API: finalize()的通常目的是在对象被不可撤销地丢弃之前执行清理操作。(我自己理解,某个对象被干掉前还会调用finalize()清理一下)
- Reference是强引用
- SoftReference是软引用
- WeakReference是弱引用
- PhantomReference是虚引用
强引用:
- 当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。
- 强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。
- 对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。
package com.juc.threadlocal;
import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author: SongZiHao
* @date: 2023/2/13
*/
class MyObject {
//finalize方法一般不会重写,也不需要我们手动调用,这里为了演示案例才这样写
@Override
protected void finalize() throws Throwable {
//在对象被不可撤销的丢弃之前执行清理操作
System.out.println("---- invoke finalize method ----");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
strongReference();
}
/**
* 默认模式:强引用
*/
private static void strongReference() {
MyObject obj = new MyObject();
System.out.println("gc before: " + obj);
obj = null;
System.gc(); //演示案例这里手动gc,一般不会这样做
System.out.println("gc after: " + obj);
}
}
3.2 四大引用之软引用
软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。
对于只有软引用的对象来说,当系统内存充足时它 不会 被回收,当系统内存不足时它 会 被回收。
软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!
演示案例先调整一下JVM的参数如下:👇👇👇
package com.juc.threadlocal;
import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author: SongZiHao
* @date: 2023/2/13
*/
class MyObject {
//finalize方法一般不会重写,也不需要我们手动调用,这里为了演示案例才这样写
@Override
protected void finalize() throws Throwable {
//在对象被不可撤销的丢弃之前执行清理操作
System.out.println("---- invoke finalize method ----");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
softReference();
}
/**
* 软引用
*/
private static void softReference() {
SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
System.out.println("gc before 内存够用: " + softReference.get());
System.gc(); //演示案例这里手动gc,一般不会这样做
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("gc after 内存仍够用: " + softReference.get()); //软引用,gc之后内存够用就不会回收对象
try {
byte[] bytes = new byte[20 * 1024 * 1024]; //20MB对象,内存肯定不够用了
} catch (Exception ex) {
ex.printStackTrace();
} finally {
System.out.println("gc after 内存不够用: " + softReference.get()); //gc之后内存不够用就会回收对象
}
}
}
3.3 四大引用之弱引用
弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,
对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。
package com.juc.threadlocal;
import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author: SongZiHao
* @date: 2023/2/13
*/
class MyObject {
//finalize方法一般不会重写,也不需要我们手动调用,这里为了演示案例才这样写
@Override
protected void finalize() throws Throwable {
//在对象被不可撤销的丢弃之前执行清理操作
System.out.println("---- invoke finalize method ----");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
weakReference();
}
/**
* 弱引用
*/
private static void weakReference() {
WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
System.out.println("gc before 内存够用: " + weakReference.get());
System.gc();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("gc after 内存够用: " + weakReference.get());
}
}
3.4 四大引用之虚引用
- 虚引用必须和引用队列 (ReferenceQueue)联合使用。虚引用需要java.lang.ret.PhantomReterence类来实现,顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用。
- PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被 finalize以后,做某些事情的通知机制。
- 处理监控通知使用,换句话说,设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作。
案例如下,记得先给JVM参数设置成
-Xms10m -Xmx10m
。
package com.juc.threadlocal;
import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @author: SongZiHao
* @date: 2023/2/13
*/
class MyObject {
//finalize方法一般不会重写,也不需要我们手动调用,这里为了演示案例才这样写
@Override
protected void finalize() throws Throwable {
//在对象被不可撤销的丢弃之前执行清理操作
System.out.println("---- invoke finalize method ----");
}
}
public class ReferenceDemo {
public static void main(String[] args) {
phantomReference();
}
/**
* 虚引用
*/
private static void phantomReference() {
MyObject obj = new MyObject();
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();
PhantomReference<MyObject> phantomReference = new PhantomReference<>(obj, referenceQueue);
System.out.println(phantomReference.get()); //虚引用get方法总是返回null
List<byte[]> list = new ArrayList<>();
new Thread(() -> {
while (true) {
//一共10M内存,死循环每次向list中存1MB数据
list.add(new byte[1 * 1024 * 1024]);
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(phantomReference.get() + " list add ok....");
}
}, "t1").start();
new Thread(() -> {
while (true) {
Reference<? extends MyObject> reference = referenceQueue.poll();
if (reference != null) {
System.out.println("有虚对象被回收加入了队列....");
break;
}
}
}, "t2").start();
}
}
由于这里的代码执行结果不一定会成功(打印有虚对象被回收加入了队列这句话),所以我直接把周阳老师的图抓了过来。
3.5 ThreadLocal为什么是弱引用?
当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象。
若这个key引用是强引用,就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;
若这个key引用是弱引用,就大概率会减少内存泄漏的问题。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null。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对象后,手动调用方法来删除它,尤其是在线程池中,不仅仅是内存泄露的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug。
- 结论:在不使用某个ThreadLocal对象后,手动调用remove方法来删除它(尤其是防止线程池下的复用)
- 从前面的set,get,remove方法的源码中可以看出,在threadLocal的生命周期里,针对threadLocal存在的内存泄漏的问题,都会通过expungeStaleEntry、cleanSomeSlots、replaceStaleEntry 这三个方法清理掉key为null的脏entry。
4.小总结
- 使用ThreadLocal一定要进行初始化,避免空指针问题。 ThreadLocal.withInitial(() -> 0)
- 建议把ThreadLocal修饰为static。
- 用完记得手动remove。