bilibili尚硅谷周阳老师JUC并发编程与源码分析课程笔记第九章——聊聊ThreadLocal

聊聊ThreadLocal

ThreadLocal简介

恶心的大厂面试题

  • ThreadLocal中ThreadLocalMap的数据结构和关系?
  • ThreadLocal的key是弱引用,这是为什么?
  • ThreadLocal内存泄漏问题你知道吗?
  • ThreadLocal中最后为什么要加remove方法?

是什么?

官网解释翻译:

ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事物ID)与线程关联起来

能干嘛?

实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不用麻烦别人,不和其他人共享,人人有份,人各一份)。

主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其改为当前线程所存的副本的值从而避免了线程安全问题。比如我们之前讲解8锁案例中,资源类是使用同一部手机,多个线程抢夺同一部手机,假如人手一份不是天下太平?

API介绍

在这里插入图片描述

initialValue()方法详细信息

protected T initialValue()

返回此线程局部变量的当前线程的“初始值”。该方法将被调用的第一次一个线程访问与可变get()方法,除非线程先前调用的set(T)方法,在这种情况下initialValue方法将不被调用的线程。通常,每个线程最多调用一次此方法,但如果后续调用remove()后跟get(),则可以再次调用此方法

这个实现只返回null;如果程房员希望线程局部变量具有除null之外的初始值,ThreadLocal必须对ThreadLocal进行子类化,并且重写此方法。通常,将使用匿名内部类

withInitial()方法详细信息

public static ThreadLocal withInitial(Supplier<? extends S> supplier)

创建一个线程局部变量,通过调用get上的Supplier方法确定变量的初始值

永远的helloworld讲起

需求描述

5个销售买房子,集团只关心销售总量的准确统计数,按照总销售额统计,方便集团公司给部分发送奖金——群雄逐鹿起纷争——为了数据安全只能加锁

Code
package com.bilibili.juc.threadlocal;

import java.util.Random;
import java.util.concurrent.TimeUnit;

public class ThreadLocalDemo {

    public static void main(String[] args) {
        House house = new House();
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5) + 1;
                System.out.println(size);
                for (int j = 1; j <= size; j++) {
                    house.saleHouse();
                }
            }, String.valueOf(i)).start();

        }
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出多少套: " + house.saleCount);
    }

}

class House {

    int saleCount = 0;

    public synchronized void saleHouse() {
        saleCount++;
    }

}

输出结果:
5
4
5
5
4
main	共计卖出多少套: 23
需求变更

希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计——比如房产中介销售都有自己的销售额指标,自己专属自己的,不和别人参和——人手一份天下安

阿里手册对使用ThreadLocal的规范

7.5 SimpleDateFormat 是线程不安全的类,一般不要定义为 static 变量,如果定义为 static,必须加锁,或者使用 DateUtils 工具类。
正例:注意线程安全,使用 DateUtils。亦推荐如下处理:

// ThreadLocal使得泛型类实例成为当前线程独有的一份拷贝, 从而做到共享资源在线程间隔离
// 当线程销毁时, ThreadLocal随之销毁
private static final ThreadLocal dateStyle = new ThreadLocal() {
    @Override
    protected DateFormat initialValue() {
    	return new SimpleDateFormat(“yyyy-MM-dd”);
    }
}

说明:如果是 JDK8 的应用,可以

原先(线程不安全)替代(线程安全)
DateInstant
CalendarLocalDateTime
SimpleDateFormatDateTimeFormatter

7.6 必须回收自定义的 ThreadLocal 变量记录的当前线程的值,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代码中使用 try-finally 块进行回收。

说明:ThreadLocal底层使用了ThreadLocalMap, 每一个泛型类实例都是一份拷贝并存进value中, 如果不主动释放Entry, 会导致内存泄漏。这是因为线程池恰好是为了减少 创建/销毁 线程的开销而复用线程, 那么如果线程池内某线程用完后不释放, ThreadLocalMap的Entry会越堆越多

正例:

objectThreadLocal.set(userInfo);
try {
    // ...
} finally {
    objectThreadLocal.remove();
}
Code
package com.bilibili.juc.threadlocal;

import java.util.Random;
import java.util.concurrent.TimeUnit;

public class ThreadLocalDemo2 {

    public static void main(String[] args) {
        House2 house2 = new House2();
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5) + 1;
                try {
                    for (int j = 1; j <= size; j++) {
                        house2.saleHouse();
                        house2.saleVolumeByThreadLocal();
                    }
                    System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house2.saleVolume.get());
                } finally {
                    house2.saleVolume.remove();
                }
            }, String.valueOf(i)).start();
        }
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出多少套: " + house2.saleCount);
    }

}


class House2 {

    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(1 + saleVolume.get());
    }

}

输出结果:
1	号销售卖出:2
3	号销售卖出:3
2	号销售卖出:4
4	号销售卖出:2
5	号销售卖出:3
main	共计卖出多少套: 14
不回收ThreadLocal变量产生内存泄露问题
package com.bilibili.juc.threadlocal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @ClassName: ThreadLocalDemo3
 * @Description:
 * @Author: zhangjin
 * @Date: 2023/12/22
 */
public class ThreadLocalDemo3 {

    public static void main(String[] args) {

        MyData myData = new MyData();

        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        try {
            for (int i = 0; i < 10; i++) {
                threadPool.submit(() -> {
                    Integer beforeInt = myData.threadLocalField.get();
                    myData.add();
                    Integer afterInt = myData.threadLocalField.get();
                    System.out.println(Thread.currentThread().getName() + "\t" + "beforeInt:" + beforeInt + "\tafterInt:" + afterInt);
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }

}

class MyData {

    ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);

    public void add() {
        threadLocalField.set(1 + threadLocalField.get());
    }

}

输出结果:
pool-1-thread-1	beforeInt:0	afterInt:1
pool-1-thread-2	beforeInt:0	afterInt:1
pool-1-thread-3	beforeInt:0	afterInt:1
pool-1-thread-1	beforeInt:1	afterInt:2
pool-1-thread-1	beforeInt:2	afterInt:3
pool-1-thread-1	beforeInt:3	afterInt:4
pool-1-thread-1	beforeInt:4	afterInt:5
pool-1-thread-2	beforeInt:1	afterInt:2
pool-1-thread-3	beforeInt:1	afterInt:2
pool-1-thread-1	beforeInt:5	afterInt:6
解决内存泄露问题
package com.bilibili.juc.threadlocal;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @ClassName: ThreadLocalDemo3
 * @Description:
 * @Author: zhangjin
 * @Date: 2023/12/22
 */
public class ThreadLocalDemo3 {

    public static void main(String[] args) {

        MyData myData = new MyData();

        ExecutorService threadPool = Executors.newFixedThreadPool(3);

        try {
            for (int i = 0; i < 10; i++) {
                threadPool.submit(() -> {
                    try {
                        Integer beforeInt = myData.threadLocalField.get();
                        myData.add();
                        Integer afterInt = myData.threadLocalField.get();
                        System.out.println(Thread.currentThread().getName() + "\t" + "beforeInt:" + beforeInt + "\tafterInt:" + afterInt);
                    } finally {
                        myData.threadLocalField.remove();
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            threadPool.shutdown();
        }

    }

}

class MyData {

    ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);

    public void add() {
        threadLocalField.set(1 + threadLocalField.get());
    }

}

输出结果:
pool-1-thread-1	beforeInt:0	afterInt:1
pool-1-thread-3	beforeInt:0	afterInt:1
pool-1-thread-2	beforeInt:0	afterInt:1
pool-1-thread-3	beforeInt:0	afterInt:1
pool-1-thread-2	beforeInt:0	afterInt:1
pool-1-thread-1	beforeInt:0	afterInt:1
pool-1-thread-2	beforeInt:0	afterInt:1
pool-1-thread-3	beforeInt:0	afterInt:1
pool-1-thread-2	beforeInt:0	afterInt:1
pool-1-thread-1	beforeInt:0	afterInt:1

小总结

  • 因为每个Thread内有自己的实例副本且该副本只有当前线程自己使用

  • 既然其他Thread不可访问,那就不存在多线程间共享问题

  • 统一设置初始值,但是每个线程对这个值得修改都是各自线程互相独立得

  • 如何才能不争抢

    • 加入synchronized或者Lock控制资源的访问顺序
    • 人手一份,大家各自安好,没有必要争抢

ThreadLocal源码分析

Thread、ThreadLocal、ThreadLocalMap关系

Thread和ThreadLocal

各自线程,人手一份

在这里插入图片描述

ThreadLocal和ThreadLocalMap

在这里插入图片描述

三者总概括

在这里插入图片描述

ThreadLocalMap实际上就是一个以ThreadLocal实例为Key,任意对象为value的Entry对象。当我们为ThreadLocal变量赋值,实际上就是以当前ThreadLocal实例为Key,值为value的Entry往这个ThreadLocalMap中存放

ThreadLocal类的set和get方法
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // ThreadLocalMap对象设置key-value:当前ThreadLocal对象-参数值
        map.set(this, value);
    else
        // 创建一个ThreadLocalMap对象,将当前线程的threadLocals属性指向ThreadLocalMap对象的引用
        // ThreadLocalMap对象设置key-value:当前ThreadLocal对象-参数值
        createMap(t, value);
}

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 通过当前ThreadLocal对象获取ThreadLocalMap的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            // 获取Entry的value
            T result = (T)e.value;
            return result;
        }
    }
    // 如果ThreadLocalMap对象为null,设置初始值
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
  	return t.threadLocals;
}

// Thread类的属性
ThreadLocal.ThreadLocalMap threadLocals = null;

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

private T setInitialValue() {
    // 初始化Entry的value方法,不重写默认返回null
    T value = initialValue();
    Thread t = Thread.currentThread();
    // 再次获取当前线程的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 二次校验,ThreadLocalMap对象是否为null
    if (map != null)
      // ThreadLocalMap对象设置key-value:当前ThreadLocal对象-initialValue()方法初始化的value值
      map.set(this, value);
    else
      // 创建一个ThreadLocalMap对象,将当前线程的threadLocals属性指向ThreadLocalMap对象的引用
      // ThreadLocalMap对象设置key-value:当前ThreadLocal对象-initialValue()方法初始化的value值
      createMap(t, value);
    // 返回initialValue()方法初始化的value值
    return value;
}

// 默认的初始化Entry的value方法,默认返回null
protected T initialValue() {
    return null;
}

小总结

近似的可以理解为:

ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象:

在这里插入图片描述

JVM内部维护了一个线程版的Map<ThreadLocal, Value>**(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当作Key,放进了ThreadLocalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。

ThreadLocal内存泄漏问题

从阿里开发手册和面试题开始讲起

  1. 如果不清理自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题,什么是内存泄露?为什么会导致内存泄露?
  2. static class Entry extends WeakReference<ThreadLocal<?>>为什么ThreadLocalMap类的静态内部类Entry要继承弱引用类?不继承弱引用类会怎么样?

什么是内存泄漏

不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏

谁惹的祸?

再回首ThreadLocalMap

在这里插入图片描述

强软弱虚引用分别是什么?
整体架构

在这里插入图片描述

Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作

在这里插入图片描述

finalize()方法在jdk9已经被标志过时

该方法一般不需要Java程序员重写,是给写Java虚拟机的人使用的

作用是当垃圾收集器确定没有对该对象的更多引用时,由对象上的垃圾收集器调用。子类重写finalize()方法以处置系统资源或执行其它清理

目的是在对象被不可撤销地丢弃之前执行清理操作

新建一个带finalize()方法的对象MyObject
class MyObject {

    // 这个方法一般不用重写,这里只是为了测试案例做说明
    // finalize()方法在对象被不可撤销地丢弃之前执行清理操作
    @Override
    protected void finalize() throws Throwable {
        System.out.println("--------invoke finalize method~!!!--------");
    }

}
强引用(默认支持模式)
  • 当内存不足时,JVM开始进行垃圾回收。对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收
  • 强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明这个对象还"活着",垃圾收集器不会碰这种对象
  • 在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用
  • 当一个对象被强引用变量引用时,它处于可达状态,是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收,因此强引用是造成Java内存泄露的主要原因之一
  • 对于一个普通的对象,如果没有其它引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)
Code
public class ReferenceDemo {

    public static void main(String[] args) {
        MyObject myObject = new MyObject();
        System.out.println("gc before:" + myObject);

        myObject = null;
        System.gc(); // 人工开启GC,一般不用

        // 等待finalize()方法打印完成
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("gc after:" + myObject);
    }

}

输出结果:
gc before:com.bilibili.juc.threadlocal.MyObject@2437c6dc
--------invoke finalize method~!!!--------
gc after:null
软引用

软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集

对于只有软引用的对象而言,当系统内存充足时,不会被回收,当系统内存不足时,他会被回收

软引用通常用在对内存敏感的程序中,比如高速缓存,内存够用就保留,不够用就回收

Code
public class ReferenceDemo {

    public static void main(String[] args) {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.out.println("gc before:" + softReference.get());

        System.gc();
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("gc after:" + softReference.get());
    }
    
}

输出结果:
gc before:com.bilibili.juc.threadlocal.MyObject@2437c6dc
gc after:com.bilibili.juc.threadlocal.MyObject@2437c6dc
修改内存大小

在这里插入图片描述

Code2
public class ReferenceDemo {

    public static void main(String[] args) {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.out.println("gc before:" + softReference.get());

        System.gc();
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("gc after内存够用:" + softReference.get());

        try {
            byte[] bytes = new byte[20 * 1024 * 1024];
        } finally {
            System.out.println("gc after内存不够用:" + softReference.get());
        }
    }
    
}

输出结果:
gc before:com.bilibili.juc.threadlocal.MyObject@2437c6dc
gc after内存够用:com.bilibili.juc.threadlocal.MyObject@2437c6dc
gc after内存不够用:null
--------invoke finalize method~!!!--------
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
	at com.bilibili.juc.threadlocal.ReferenceDemo.main(ReferenceDemo.java:28)
软引用使用场景

假如有一个应用需要读取大量的本地图片:

  • 如果每次读取图片都从硬盘读取则会严重影响性能
  • 如果一次性全部加载到内存中又可能会造成内存溢出

此时使用软引用来解决这个问题

设计思路是:用一个HashMap来保存图片的路径和与相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,有效避免了OOM的问题

用法:Map<String, SoftReference<BitMap>> imageCache = new HashMap<>();Map的key是文件夹地址,value是图片信息对象关联的软引用

弱引用

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生命周期更短

对于只有弱引用的对象而言,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存

Code
public class ReferenceDemo {

    public static void main(String[] args) {
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.out.println("gc before:" + weakReference.get());

        System.gc();
        try {
            TimeUnit.MILLISECONDS.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("gc after:" + weakReference.get());
    }
}

输出结果:
gc before:com.bilibili.juc.threadlocal.MyObject@2437c6dc
--------invoke finalize method~!!!--------
gc after:null
虚引用
  1. 虚引用必须和引用队列(ReferenceQueue)联合使用

    虚引用需要java.lang.ref.PhantomReference类来实现,顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都有可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用

  2. 虚引用(PhantomReference)的get方法总是返回null

    虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被finalize后,做某些事情的通知机制
    PhantomReference的的get方法总是返回null,因此无法访问对应的引用对象

  3. 处理监控通知使用

    换句话说,设置虚引用关联对象的唯一目的,就是在对象被GC的时候会收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作

PhantomReference类介绍

幻像引用对象,在收集器之后排队,确定它们的对象可以被回收。幻影参考通常用于安排事后清理操作

假设垃圾收集器在某个时间点确定对象是phantom reachable 。那时它将原子地清除对该对象的所有幻像引用以及对该对象可从其访问的任何其他可进入幻像的对象的所有幻像引用。在同一时间或稍后,它将使用参考队列注册的新清除的幻像引用入队

为了确保可回收对象保持如此,可能无法检索幻像引用的引用: 幻像引用的get方法始终返回null

构造方法

// 创建一个新的幻像引用,该引用引用给定对象并在给定队列中注册
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
    super(referent, q);
}
ReferenceQueue类介绍

引用队列,PhantomReference对象被回收前需要被引用队列保存下

Code

先修改内存大小

在这里插入图片描述

public class ReferenceDemo {
    
  public static void main(String[] args) {
      ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();
      PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(), referenceQueue);

      List<byte[]> list = new ArrayList<>();

      new Thread(() -> {
        while (true) {
          list.add(new byte[1 * 1024 * 1024]);
          try {
            TimeUnit.MILLISECONDS.sleep(500);
          } catch (InterruptedException e) {
            e.printStackTrace();
          }
          System.out.println(phantomReference.get() + "\t" + "list add ok");
        }
      }, "t1").start();

      new Thread(() -> {
        while (true) {
          Reference<? extends MyObject> reference = referenceQueue.poll();
          if (reference != null) {
            System.out.println("--------有虚对象回收,加入到队列了--------");
            break;
          }
        }
      }, "t2").start();
  }
  
}

输出结果:
--------invoke finalize method~!!!--------
null	list add ok
null	list add ok
null	list add ok
null	list add ok
null	list add ok
null	list add ok
null	list add ok
Exception in thread "t1" java.lang.OutOfMemoryError: Java heap space
	at com.bilibili.juc.threadlocal.ReferenceDemo.lambda$phantomReference$0(ReferenceDemo.java:33)
	at com.bilibili.juc.threadlocal.ReferenceDemo$$Lambda$1/2030562336.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)
--------有虚对象回收,加入到队列了--------
GCRoots和四大引用小总结

在这里插入图片描述

Thread、ThreadLocal、ThreadLocalMap关系大总结

在这里插入图片描述在这里插入图片描述

为什么要用弱引用?不用如何?

Demo

在这里插入图片描述

为什么源码要用弱引用?

当function01方法执行完毕后,栈帧销毁,强引用tl也就没有了,但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象(因为即使执行function01方法的线程销毁了,ThreadLocal是另一个类,是两个不同的类)

若这个Key是强引用,就会导致Key指向的ThreadLocal对象即V指向的对象不能被gc回收,造成内存泄露

若这个引用时弱引用大概率会减少内存泄漏的问题(当然,还得考虑key为null这个坑),使用弱引用就可以使ThreadLocal对象在方法执行完毕后顺利被回收且entry的key引用指向为null

弱引用就万事大吉了吗?

使用弱引用就可以使ThreadLocal对象在方法执行完毕后顺利被回收且entry的key引用指向为null此后我们调用get、set或remove方法时,就会尝试删除key为null的entry,可以释放value对象所占用的内存

key为null这个坑介绍
  1. 当我们为threadLocal变量赋值,实际上就是当前Entry(threadLocal实例为key,值为value)往ThreadLocalMap中存放,Entry中的key是弱引用,当threadLocal外部强引用被置为null,那么系统GC的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能引用到它,这个threadLocal必定会被回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(尤其是在线程池场景下,线程经常会被复用),这些key为null的Entry的value就会一直存在一条强引用链,Thread Ref->Thread->ThreadLocalMap->Entry->value永远无法回收,造成内存泄漏
  2. 当然,如果当前Thread运行结束,ThreadLocalMap、Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收
  3. 但是在实际使用中我们基本都是用线程池去维护我们的线程,比如用Executors.newFixedThreadPool()方法创建线程池,为了复用,线程是不会结束的,所以ThreadLocal内存泄漏就值得我们注意
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对象后,手动调用remove方法来删除它,尤其是在线程池中,不仅仅是内存泄漏的问题,因为线程池中的线程是重复使用的,意味着这个线程的ThreadLocalMap对象也是重复使用的,如果我们不手动调用remove方法,那么后面的线程就有可能获取到上个线程遗留下来的value值,造成bug

ThreadLocalMap类的set、get方法会去检查所有键为null的Entry对象

expungeStaleEntry()方法——>清除脏Entry——>清除key为null的entry

set()方法

在这里插入图片描述

getEntry()方法

在这里插入图片描述

remove()方法

在这里插入图片描述

小总结

从前面ThreadLocalMap类的set()、getEntry()、remove()方法看出,在ThreadLocal的生命周期中,针对ThreadLocal存在的内存泄漏问题,都会通过expungeStaleEntry()方法清理掉key为null的脏Entry

最佳实践

  • 使用ThreadLocal一定要初始化,避免空指针异

    • 使用ThreadLocal.withInitial(() -> 初始化值);方式初始化即可
  • 建议把ThreadLocal修饰为static

    • 阿里开发手册:

      19.【参考】ThreadLocal 对象使用 static 修饰,ThreadLocal 无法解决共享对象的更新问题。

      说明:这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享此静态变量也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只要是这个线程内定义的)都可以操控这个变量。

    • 个人解释:ThreadLocal实现了线程的数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap,所以ThreadLocal可以只初始化一次,只分配一块存储空间就足以,没必要作为成员变量多次被初始化

  • 用完一定记得手动remove

    • remove()方法不仅会清理当前ThreadLocal对象,还会通过expungeStaleEntry()方法清理掉key为null的脏Entry

小总结

  • ThreadLocal并不解决线程间共享数据的问题
  • ThreadLocal适用于变量在线程间隔离且在方法间共享的场景
  • ThreadLocal通过隐式的在不同线程内创建独立实例副本避免了实例线程安全的问题
  • 每个线程持有一个只属于它自己的专属map并维护了ThreadLocal对象与具体实例的映射,该Map由于只被持有他的线程访问,故不存在线程安全以及锁的问题
  • ThreadLocalMap的Entry对ThreadLocal的引用为弱引用。避免了ThreadLocal对象无法被回收的问题
  • 都会通过expungeStaleEntry,cleanSomeSlots,replaceStaleEntry这三个方法回收键为null的Entry对象的值(即为具体实例)以及entry对象本身从而防止内存泄漏,属于安全加固的方法
  • 群雄逐鹿起纷争,人各一份天下安
  • 23
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值