文章目录
- ThreadLocal ^1.2^ 的作用
- 使用场景
- 示例1
- ThreadLocal 变量初始化
- ThreadLocal 源码分析
- 源码分析总结
- 内存泄漏问题
- 示例说明
- new Thread 方式 执行结果
- pool 方式执行结果
- 原因解析
- 总结
ThreadLocal 1.2 的作用
ThreadLocal 为每个线程提供单独的变量副本。每个变量副本都是线程私有的,线程之间互不影响。当线程执行完成后,对应的变量副本也会被垃圾回收。
在线程中通过 ThreadLocal 的 get、set 方法获取或设置变量的值
使用场景
- 每个线程需要有自己单独的变量
- 变量需要在多个方法中共享,但不希望被多线程共享
示例1
还是用我们的 Car 对象来说明,比如同一个品牌同一个型号的汽车,可能只有颜色不一样,这时我们就可以使用 ThreadLocal 来定义颜色字段,使他在不同的线程中有不同的副本
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ThreadLocalTest1 {
public static void main(String[] args) {
// 线程间共享的对象
Car car = new Car();
new Thread(()->{
car.setSkeleton("骨架1");
car.setEngine("引擎1");
car.setTire("轮胎1");
car.setColor("颜色1");
log.debug("{}",car);
},"t1").start();
new Thread(()->{
car.setSkeleton("骨架2");
car.setEngine("引擎2");
car.setTire("轮胎2");
car.setColor("颜色2");
log.debug("{}",car);
},"t2").start();
new Thread(()->{
car.setSkeleton("骨架3");
car.setEngine("引擎3");
car.setTire("轮胎3");
car.setColor("颜色3");
log.debug("{}",car);
},"t3").start();
}
@Slf4j
@Getter
@Setter
@ToString
static class Car{
/**
* 骨架
*/
private String skeleton;
/**
* 发动机
*/
private String engine;
/**
* 轮胎
*/
private String tire;
/**
* 颜色
*/
private ThreadLocal<String> color = new ThreadLocal<>();
public void setColor(String color) {
this.color.set(color);
}
public String getColor() {
return this.color.get();
}
}
}
执行结果:
12:01:14.655 [t1] DEBUG com.yyoo.thread.ThreadLocalTest1 - ThreadLocalTest1.Car(skeleton=骨架3, engine=引擎3, tire=轮胎3, color=颜色1)
12:01:14.655 [t2] DEBUG com.yyoo.thread.ThreadLocalTest1 - ThreadLocalTest1.Car(skeleton=骨架3, engine=引擎3, tire=轮胎3, color=颜色2)
12:01:14.655 [t3] DEBUG com.yyoo.thread.ThreadLocalTest1 - ThreadLocalTest1.Car(skeleton=骨架3, engine=引擎3, tire=轮胎3, color=颜色3)
通过结果可以发现,没有使用 ThreadLocal 的字段每个线程是共享使用的,但color 字段是每个线程独有的
注:这里我们还是使用的 new Thread ,而没有使用线程池,因为使用线程池有一个问题,我们下面再说。
ThreadLocal 变量初始化
我们上面的示例,直接使用的 new ThreadLocal<>(),其初始化的值为:null。有些时候我们需要初始化为其他值
- 用 set 方法直接设置
- 继承 ThreadLocal ,重写 protected T initialValue() 方法
在第一次调用 get 方法时,会调用此方法返回初始值。(前提是没有调用 set 方法)
- 调用 ThreadLocal 的静态方法 withInitial(Supplier<? extends S> supplier)
该方法返回一个 ThreadLocal 的静态内部类 SuppliedThreadLocal,其最终方式就是继承 ThreadLocal ,重写 initialValue 方法。
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
private final Supplier<? extends T> supplier;
SuppliedThreadLocal(Supplier<? extends T> supplier) {
this.supplier = Objects.requireNonNull(supplier);
}
@Override
protected T initialValue() {
return supplier.get();
}
}
此处建议使用 withInitial 方法来初始化 ThreadLocal 变量的值
注:如果不初始化 ThreadLocal 变量的值,那么他默认就是 ThreadLocal 的 initialValue 方法返回的值(也就是 null),这样可能引发NPE(空指针异常)。
ThreadLocal 源码分析
我们先从 get 方法入手
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 通过当前线程对象获取 ThreadLocalMap
// ThreadLocalMap 对象是 ThreadLocal 的一个静态内部类,其实现和 Map 差不多
ThreadLocalMap map = getMap(t);
if (map != null) { // 如果 map 不为空,说明当前线程已经有副本值了
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {// 如果当前副本值不为空,则返回该值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果当前副本值为空,则执行初始化方法 setInitialValue
return setInitialValue();
}
setInitialValue 方法逻辑
private T setInitialValue() {
// 执行 initialValue 方法,获取初始值
T value = initialValue();
Thread t = Thread.currentThread();
// 获取当前线程的 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);// 如果 ThreadLocalMap 为空,则执行 createMap 创建该对象
return value;
}
以上涉及到两个 ThreadLocalMap 的操作,getMap 和 createMap
ThreadLocalMap getMap(Thread t) {
// 直接返回 t 线程的 threadLocals 属性
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
// 设置 t 线程的 threadLocals 属性为 new ThreadLocalMap
// ThreadLocalMap 的 key 值为 this(当前 ThreadLocal 对象)
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
在 Thread 类中对应的 threadLocals 字段定义如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal set 方法逻辑
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
threadLocals 在 Thread 初始化时,是为空的,在某个 ThreadLocal 对象第一次调用 get 或 set 方法时,创建并赋值的。
源码分析总结
内存泄漏问题
在此,请大家先回顾一下我们在 JVM 专栏《JVM GC 垃圾收集器》一章中提到的几种引用的类型(强软弱虚),以及它们在 GC 时的特点,这样有助于理解该问题。
其实 ThreadLocal 在我们上面的示例中的引用关系,除了在线程中(我们上面总结的引用关系图),还有就是在我们的 car 对象中,所以最终我们的引用图是这样的
来说明一下:
- threadLocals:是 Thread 对象中的字段强引用,类型为:ThreadLocal.ThreadLocalMap,其中存放了 ThreadLocal.ThreadLocalMap.Entry
- color:是我们自定义对象 Car 中的字段强引用,类型为:ThreadLocal,它用作 ThreadLocal.ThreadLocalMap.Entry 的 Key
示例说明
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadLocalTest2 {
public static void main(String[] args) throws InterruptedException {
/*
核心线程为:2
最大线程数为:2
救急线程最长空闲时间:0 秒
阻塞队列使用固定容量的 LinkedBlockingQueue ,容量为 10
线程工厂使用 Spring 提供的 CustomizableThreadFactory ,并定义线程的前缀为:my-thread-pool-
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(2,2,0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),new CustomizableThreadFactory("my-thread-pool-"));
// pool 线程池方式
pool.execute(()->log.debug("{}",new Car()));
// new Thread 方式
// new Thread(()->log.debug("{}",new Car())).start();
// 等待以上线程执行完成(当然我们可以使用 FutureTask 来 get 这里我们只是示例而已,请不要较真)
TimeUnit.SECONDS.sleep(2);
System.gc();
log.debug("GC 方法已执行");
// new Thread 方式在此处要等待 GC 完成
// pool 方法不用是因为,我们没有关闭线程池,当前程序不会结束
TimeUnit.SECONDS.sleep(2);
}
@Slf4j
@Getter
@Setter
@ToString
static class Skeleton{
/**
* 颜色
*/
private String color;
public Skeleton(String color) {
this.color = color;
}
@Override
protected void finalize() throws Throwable {
log.debug("{},被 GC 了,方法对象:{}",this,"Skeleton");
}
}
@Slf4j
@Getter
@Setter
@ToString
static class Car{
/**
* 我们这里给 ThreadLocal 一个初始化值
*/
private ThreadLocal<Skeleton> skeleton = new ThreadLocal<Skeleton>(){
@Override
protected Skeleton initialValue() {
return new Skeleton("黑色");
}
// 重写 ThreadLocal 的 finalize 方法,看看它什么时候被 GC
@Override
protected void finalize() throws Throwable {
log.debug("{},被 GC 了,方法对象:{}",this,"ThreadLocal");
}
};
public Car(){
}
public Car(Skeleton skeleton){
this.setSkeleton(skeleton);
}
public void setSkeleton(Skeleton skeleton) {
this.skeleton.set(skeleton);
}
public Skeleton getSkeleton() {
return this.skeleton.get();
}
@Override
protected void finalize() throws Throwable {
log.debug("{},被 GC 了,方法对象:{}",this,"Car");
}
}
}
new Thread 方式 执行结果
14:59:45.990 [Thread-0] DEBUG com.yyoo.thread.ThreadLocalTest2 - ThreadLocalTest2.Car(skeleton=ThreadLocalTest2.Skeleton(color=黑色))
14:59:48.004 [main] DEBUG com.yyoo.thread.ThreadLocalTest2 - GC 方法已执行
14:59:48.004 [Finalizer] DEBUG com.yyoo.thread.ThreadLocalTest2$Skeleton - ThreadLocalTest2.Skeleton(color=黑色),被 GC 了,方法对象:Skeleton
14:59:48.004 [Finalizer] DEBUG com.yyoo.thread.ThreadLocalTest2$Car - com.yyoo.thread.ThreadLocalTest2$Car$1@6dfb5a1c,被 GC 了,方法对象:ThreadLocal
14:59:48.004 [Finalizer] DEBUG com.yyoo.thread.ThreadLocalTest2$Car - ThreadLocalTest2.Car(skeleton=ThreadLocalTest2.Skeleton(color=黑色)),被 GC 了,方法对象:Car
new Thread 执行后,可以看到 GC 时
- Car 对象被回收了
- ThreadLocal 对象本身被回收了
- ThreadLocal 对应的 value Skeleton 也被回收了。
代码中用到的对象都正常回收完成。
pool 方式执行结果
15:24:08.012 [my-thread-pool-1] DEBUG com.yyoo.thread.ThreadLocalTest2 - ThreadLocalTest2.Car(skeleton=ThreadLocalTest2.Skeleton(color=黑色))
15:24:10.040 [main] DEBUG com.yyoo.thread.ThreadLocalTest2 - GC 方法已执行
15:24:10.040 [Finalizer] DEBUG com.yyoo.thread.ThreadLocalTest2$Car - com.yyoo.thread.ThreadLocalTest2$Car$1@6dfb5a1c,被 GC 了,方法对象:ThreadLocal
15:24:10.040 [Finalizer] DEBUG com.yyoo.thread.ThreadLocalTest2$Car - ThreadLocalTest2.Car(skeleton=ThreadLocalTest2.Skeleton(color=黑色)),被 GC 了,方法对象:Car
- Car 对象被回收了
- ThreadLocal 对象本身被回收了
- ThreadLocal 对应的 value Skeleton 没有被回收
原因解析
new Thread 方式正常回收,是因为,线程执行完成后,Thread 对象执行完了,会被回收。而 pool 方式执行完成后,线程会回到线程池,而不是关闭线程,该线程还可能执行其他任务。这意味着这个线程对应的 threadLocals 不会被释放(如果我们没有手动将其赋值为 null 手动释放的话)。我们上面提到了 threadLocals 是强引用,所以它是不会被释放的,threadLocals 没有被释放,那么 ThreadLocal 对象为啥被释放了?它不是 threadLocals ThreadLocal.ThreadLocalMap.Entry 的key 值吗?我们来看一眼 ThreadLocal.ThreadLocalMap.Entry 的源码定义
static class Entry extends WeakReference<ThreadLocal<?>> {
/** ThreadLocal 对应到 value 值 */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
通过 Entry 对象的定义,我们知道 Entry 对象是一个弱引用对象,其引用的值是 ThreadLocal 对象,而 ThreadLocal 对象是 Entry 的 key 值。弱引用对象在每次 GC 时,都会被回收。所以 GC 之后,ThreadLocal 对象被回收了,Entry 对应的 key 值就为空了。但是,这里的 Object value 是不会被回收的,因为通过 threadLocals ThreadLocal.ThreadLocalMap,还是能引用过来,导致 value 值还存在。
总结
所以,在使用线程池的情况下使用 ThreadLocal 我们需要按照如下方式来使用
pool.execute(() -> {
ThreadLocal<String> tl = null;
try {
tl = ThreadLocal.withInitial(() -> "测试");
}finally {
if(tl != null){
// 通过 remove 方法清除 ThreadLocal 对象的局部变量值
// 调用 remove 过后,如果再次 get,会再次重新执行 initialValue 方法,重新初始化
tl.remove();
}
}
});
所以,我们得将我们的示例程序修改一下:
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Slf4j
public class ThreadLocalTest2 {
public static void main(String[] args) throws InterruptedException {
/*
核心线程为:2
最大线程数为:2
救急线程最长空闲时间:0 秒
阻塞队列使用固定容量的 LinkedBlockingQueue ,容量为 10
线程工厂使用 Spring 提供的 CustomizableThreadFactory ,并定义线程的前缀为:my-thread-pool-
*/
ThreadPoolExecutor pool = new ThreadPoolExecutor(2,2,0, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),new CustomizableThreadFactory("my-thread-pool-"));
// pool 线程池方式
pool.execute(()-> {
Car car = null;
try {
car = new Car();
log.debug("{}", car);
}finally {
if(car != null) {
car.remove();
}
}
});
// new Thread 方式
// new Thread(()->log.debug("{}",new Car())).start();
// 等待以上线程执行完成(当然我们可以使用 FutureTask 来 get 这里我们只是示例而已,请不要较真)
TimeUnit.SECONDS.sleep(2);
System.gc();
log.debug("GC 方法已执行");
// new Thread 方式在此处要等待 GC 完成
// pool 方法不用是因为,我们没有关闭线程池,当前程序不会结束
TimeUnit.SECONDS.sleep(2);
}
@Slf4j
@Data
static class Skeleton{
/**
* 颜色
*/
private String color;
public Skeleton(String color) {
this.color = color;
}
@Override
protected void finalize() throws Throwable {
log.debug("{},被 GC 了,方法对象:{}",this,"Skeleton");
}
}
@Slf4j
@Data
static class Car{
/**
* 我们这里给 ThreadLocal 一个初始化值
*/
private ThreadLocal<Skeleton> skeleton = new ThreadLocal<Skeleton>(){
@Override
protected Skeleton initialValue() {
return new Skeleton("黑色");
}
// 重写 ThreadLocal 的 finalize 方法,看看它什么时候被 GC
@Override
protected void finalize() throws Throwable {
log.debug("{},被 GC 了,方法对象:{}",this,"ThreadLocal");
}
};
public Car(){
}
public Car(Skeleton skeleton){
this.setSkeleton(skeleton);
}
public void setSkeleton(Skeleton skeleton) {
this.skeleton.set(skeleton);
}
public Skeleton getSkeleton() {
return this.skeleton.get();
}
public void remove(){
this.skeleton.remove();
}
@Override
protected void finalize() throws Throwable {
log.debug("{},被 GC 了,方法对象:{}",this,"Car");
}
}
}
执行结果:
08:29:52.113 [my-thread-pool-1] DEBUG com.yyoo.thread.ThreadLocalTest2 - ThreadLocalTest2.Car(skeleton=ThreadLocalTest2.Skeleton(color=黑色))
08:29:54.133 [main] DEBUG com.yyoo.thread.ThreadLocalTest2 - GC 方法已执行
08:29:54.133 [Finalizer] DEBUG com.yyoo.thread.ThreadLocalTest2$Skeleton - ThreadLocalTest2.Skeleton(color=黑色),被 GC 了,方法对象:Skeleton
08:29:54.133 [Finalizer] DEBUG com.yyoo.thread.ThreadLocalTest2$Car - com.yyoo.thread.ThreadLocalTest2$Car$1@6dfb5a1c,被 GC 了,方法对象:ThreadLocal
08:29:54.133 [Finalizer] DEBUG com.yyoo.thread.ThreadLocalTest2$Car - ThreadLocalTest2.Car(skeleton=ThreadLocalTest2.Skeleton(color=黑色)),被 GC 了,方法对象:Car
很多地方(阿里开发规约中也有)都有这样一个建议:ThreadLocal 对象建议使用 static 修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。
在我们的示例下,如果将 ThreadLocal 对象用 static 修饰,我们即便调用了 remove 方法,value 对象也不会被释放,且 ThreadLocal 对象都不会被释放了。 此处请大家自行判断吧,或许此处有我没有理解到的地方,请大家指正,但我依照我们的示例代码结果来总结,虽然用 static 修饰,在分配时只需要一块存储空间,但它无法释放已分配的空间,也可能导致内存泄漏。