1 ThreadLocal介绍
ThreadLocal提供了线程的局部变量。每个线程都有一份自己的、独立初始化的变量。这样的设计就能保证线程的安全(多线程修改共享变量不安全,修改自己的变量安全)。
2 ThreadLocal案例
现有需求如下:销售员销售房子,年终需要统计每个销售员分别销售了多少套房子。
该案例中每个销售员就相当于是一个线程,销售的房子数即为每个线程的局部变量,每个线程修改其自身的变量(即销售房子数)。
注意:在使用完ThreadLocal变量时,需要手动Remove,否在在多线程使用线程池的情况下,由于线程的复用,可能会导致内存泄露的问题。
class SalesMan {
//创建ThreadLocal线程局部变量,并赋初始值0
ThreadLocal<Integer> salesCount = ThreadLocal.withInitial(() -> 0);
//每调用一次数量加1
public void saleHouse() {
salesCount.set(salesCount.get() + 1);
}
}
public class ThreadLocalDemo1 {
public static void main(String[] args) {
SalesMan salesMan = new SalesMan();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
int random = new Random().nextInt(5) + 1;
try {
for (int j = 0; j < random; j++) {
salesMan.saleHouse();
}
} finally {
salesMan.salesCount.remove();
}
System.out.println(Thread.currentThread().getName() + " 号销售卖出 " + salesMan.salesCount.get());
}, String.valueOf(i)).start();
}
}
}
//输出
2 号销售卖出 5
3 号销售卖出 1
4 号销售卖出 2
0 号销售卖出 4
1 号销售卖出 5
2 ThreadLocal源码分析
前置知识,Thread、ThreadLocal与ThreadLocalMap。
一个线程是一个Thread;线程的一个局部变量就是一个ThreadLocal;一个线程可以有多个局部变量(ThreadLocal),这多个局部变量就存在ThreadLocalMap(每个线程独有一份)里。
在JUC源码中Thread类中引用了一个ThreadLocalMap,里面存储了这个线程的所有ThreadLocal字段。
从上图可以看出,ThreadLocalMap类通过ThreadLocal类引用,它属于ThreadLcoal类的一个静态内部类,其中map的key是ThreadLocal(线程局部变量字段),value对应的是该ThreadLoval的值。
2.1 get()方法源码分析
这是get()方法的源码。
上图可以看出,在调用ThreadLocal的get()方法时需要先获取到当前线程,并同通过getMap()方法获取当前线程的ThreadLoaclMap(里面存储了该线程的所有局部变量)。以下是getMap()方法的源码,可以看出只是通过线程(t)调用了其自身的ThreadLocalMap。
然后判断是否初始化过该ThreadLocalMap,未初始化则调用setInitialValue()方法初始化;若已初始化过,则通过map.getEntry(当前ThreadLocal)获取Map中key对应ThreadLocal的Entry,并返回Entry的value值。如下是setInitialValue()方法的源码。其中将ThreadLocal字段的值初始化为null,再获取当前线程的ThreadLocalMap,若map已经创建过则直接set对应ThreadLocal的值,若不存在,则调用createMap()方法为当前线程创建一个ThreadLocalMap,并进行赋值操作。
总结:
- 调用ThreadLocal的set()方法时,实际上就是向ThreadLocalMap中设置值,key是ThreadLocal对象,value是set传递进来的值。
- 调用ThreadLocal的get()方法时,实际上就是通过ThreadLocal作为key,从ThreadLocalMap中获取value;
ThreadLocal本身并不存储数据,它只是作为一个key让线程从ThreadLocalMap中获取value。
3 ThreadLocal的内存泄露问题
前置知识。弱引用,弱引用的对象在jvm发生垃圾回收时,若jvm对空间还充足,则不会回收该对象;在jvm内存空间不足时,会回收弱引用对象。以下是内存泄露的两种情况以及处理方案。
- 当一个方法执行完毕ThreadLocal需要被回收,但此时ThreadLocalMap中的key还指向这个对象,则会出现ThreadLocal无法被回收的情况,出现内存泄露。解决方法:将ThreadLocalMap中的key(ThreadLocal)设置为弱引用,gc内存不够时直接回收。
- 脏Entry。当ThreadLocal对象被回收,导致Entry中的key为null,但是value中的对象还存在,并且无法被访问回收,从而导致内存泄露。解决方法:ThreadLocal类在进行get、set等方法时,会将key为null的Entry的value直接置为null,从而防止了内存泄露。