1 ThreadLocal简介
1.1 面试题
- ThreadLocal中ThreadLocalMap的数据结构和关系
- ThreadLocal的key是弱引用,这是为什么?
- ThreadLocal内存泄漏问题你知道吗?
- ThreadLocal中最后为什么要加remove方法?
1.2 是什么?
ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事物ID)与线程关联起来。
1.3 能干吗?
实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不用麻烦别人,不和其他人共享,人人有份,人各一份)。主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其改为当前线程所存的副本的值从而避免了线程安全问题。比如8锁案例中,资源类是使用同一部手机,多个线程抢夺同一部手机,假如人手一份不是天下太平?
所以ThreadLocal中存放的是属于自己线程中的一些属性,不用写回主物理内存的内容。
1.4 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方法确定变量的初始值
1.5 样例说明
需求
5个销售卖房子,按照出单数各自统计。比如某房产中介销售都有自己的销售额指标,自己专属于自己的,不和别人掺和。
正好对应了前面的【每个线程都有自己专属的本地变量副本】
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
House house = new House();
for (int i = 1; i <= 5; i++) {
new Thread(() -> {
try {
//模拟当前线程从1-5中随机卖出的数量
int sale = new Random().nextInt(5) + 1;
for (int j = 0; j < sale; j++) {
house.saleVolumeByThreadLocal();
}
System.out.println(Thread.currentThread().getName() + "线程卖出了:" + house.saleVolume.get());
} finally {
house.saleVolume.remove();
countDownLatch.countDown();
}
}, String.valueOf(i)).start();
}
countDownLatch.await();
System.out.println("总共卖出:" + house.saleCount);
}
}
class House {
//所有人卖出的总数
AtomicInteger saleCount = new AtomicInteger();
//每个线程的ThreadLocal初始化为0
ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
public void saleVolumeByThreadLocal() {
//获取对应线程的saleVolume再加1
saleVolume.set(saleVolume.get() + 1);
saleCount.getAndIncrement();
}
}
5线程卖出了:2
1线程卖出了:5
2线程卖出了:2
3线程卖出了:2
4线程卖出了:2
总共卖出:13
注意,也要调用remove() 接口,不然容易导致内存泄漏
必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,如果不清理自定义的ThreadLocal变量,可能会影像后序业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally块进行回收。
样例:
//Demo2-主要演示线程池情况下,线程池中的线程会复用(不会自动清空),而上面的都是新建一个Thread
class MyData{
ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);
public void add(){
threadLocalField.set(1+ threadLocalField.get());
}
}
/**
* 根据阿里规范,需要对自定义的ThreadLocal进行回收,否则容易造成内存泄漏和业务逻辑问题(因为线程池中的线程会复用)
*/
public class ThreadLocalDemo2 {
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+"\t afterInt"+afterInt);