ThreadLocal原理解析与应用场景及其避坑指南
ThreadLocal很容易让人望文生义,不知道的人想当然地认为是一个"本地线程"。其实,ThreadLocal并不是一个Thread,而是Thread的一个局部变量,我认为也许把它命名为ThreadLocalVariable更容易让人理解一些。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
从线程的角度看,目标变量就像是线程的本地变量,这也是类名中“Local”所要表达的意思。
ThreadLoal的应用场景我就不多了,web项目中我们可以使用ThreadLocal保存用户Session信息;也可以使用在网关使用ThreadLocal来做一些简单的性能统计(比如说接口耗时);当然了,很多开源框架里也有很多使用ThreadLocal的地方,比如说Spring的事务管理底层实现也是使用了ThreadLocal的。也可以说是面试的常客,每逢面试只要问到了并发基本都会问到,关于ThreadLocal的原理以及不正当的使用造成的OOM内存溢出的问题,值得花时间仔细研究一下其原理。
ThreadLocal在Spring的事务管理中的应用
Spring的事务管理的底层实现就用到了ThreadLocal来保存事务信息,废话不多说,直接上图吧:
既然提到了事务信息,那我就截图一下TransactionInfo的详细信息吧,有兴趣的自己下去研究一下。ThreadLocal原理
ThreadLocal的内部类
TheadLocal的结构比较简单,只有三个内部类,具体情况如截图所示:
ThreadLocalMap
用于保存当前线程的所有变量信息的,一个线程对应一个ThreadLocalMap。
从上图我们可以知道,ThreadLocalMap从字面上可以看出这是一个保存ThreadLocal对象的map,不过是经过了两层包装的ThreadLocal对象:(1)第一层包装是使用 WeakReference<ThreadLocal<?>> 把ThreadLocal对象变成一个弱引用的对象。
(2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference<ThreadLocal<?>>。
ThreadLocalMap的构造函数
从截图可以知道,ThreadLocalMap的内部默认的table大小是16;Key是ThredLocal,value是我们需要保存的信息。ThreadLocalMap自己实现了如何从key到value的映射:使用一个static的原子属性AtomicInteger nextHashCode,通过每次增加 HASH_INCREMENT = 0x61c88647 ,然后 & (INITIAL_CAPACITY - 1) 取得在数组 private Entry[] table 中的索引。
ThreadLocal、ThreadLocalMap、Entry三者之间的关系
ThreadLocal、ThreadLocalMap、Entry三者之间的关系如下图(可能有点小复杂):
简单点说就是:一个Thread中只有一个ThreadLocalMap,一个ThreadLocalMap中可以有多个ThreadLocal对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中的一个Entry(也就是说:一个Thread可以依附有多个ThreadLocal对象)。Entry
用于封装我们设置的变量信息的。
SuppliedThreadLocal
仅仅用于实现Java 8之后的函数式编程用的,这里不细说。
ThreadLocal的内存回收
ThreadLocal层面的回收:
当线程死亡时,那么所有的保存在的线程局部变量就会被回收,其实这里是指线程Thread对象中的 ThreadLocal.ThreadLocalMap threadLocals 会被回收,这是显然的。
ThreadLocalMap层面的回收
当ThreadLocalMap 的 private Entry[] table,已经被占用达到了三分之二时 threshold = 2/3(也就是线程拥有的局部变量超过了10个) ,就会尝试回收 Entry 对象,我们可以看到 ThreadLocalMap.set()方法中有下面的代码:
ThreadLocal踩坑实战
首先看一下代码,模拟了一个线程数为1000的线程池,所有线程共享一个ThreadLocal变量,每一个线程执行的时候插入一个大的List集合:
import com.example.model.Emp;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* ThreadLocal 线程局部变量Test
* */
public class ThreadLocalTest {
private static int BIG_LIST_SIZE = 50000;
private static int THREAD_SIZE = 1000;
private static ThreadLocal<List<Emp>> empLocal = new ThreadLocal<List<Emp>>();
public static void main(String[] args){
ExecutorService executors = Executors.newFixedThreadPool(THREAD_SIZE);
for(int i=0; i<THREAD_SIZE; i++){
executors.execute(new Runnable() {
public void run() {
empLocal.set(new ThreadLocalTest().addEmp());
System.out.println(Thread.currentThread().getName());
}
});
try {
Thread.sleep(1L);
}catch (Exception e){
e.printStackTrace();
}
}
}
private List<Emp> addEmp(){
List<Emp> empList = new ArrayList<Emp>();
for(int i=0; i<BIG_LIST_SIZE; i++){
empList.add(new Emp(i, "test" + i));
}
return empList;
}
}
复制代码
为了模拟出OutOfMemory异常,我们把Xmx的值设置为128M,如下图所示:
当运行一段时间后,在控制台我们很快就可以看到报错信息了:
现在对代码进行优化,每次set之后都remove掉,看是否还有相同的问题,优化之后的代码如下:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* ThreadLocal 线程局部变量Test
* */
public class ThreadLocalTest {
private static int BIG_LIST_SIZE = 10000;
private static int THREAD_SIZE = 1000;
private static ThreadLocal<List<Emp>> empLocal = new ThreadLocal<List<Emp>>();
public static void main(String[] args){
ExecutorService executors = Executors.newFixedThreadPool(THREAD_SIZE);
for(int i=0; i<THREAD_SIZE; i++){
executors.execute(new Runnable() {
public void run() {
empLocal.set(new ThreadLocalTest().addEmp());
System.out.println(Thread.currentThread().getName());
empLocal.remove();//优化后
}
});
try {
Thread.sleep(1L);
}catch (Exception e){
e.printStackTrace();
}
}
}
private List<Emp> addEmp(){
List<Emp> empList = new ArrayList<Emp>();
for(int i=0; i<BIG_LIST_SIZE; i++){
empList.add(new Emp(i, "test" + i));
}
return empList;
}
}
复制代码
优化之后的代码一切正常,再未出现过OutofMemory异常。所以用ThreadLocal的时候要记得在适当的时候remove。
关注“Java架构师养成记”,带你装逼带你飞。后台回复“面试突击”,获取BAT一线互联网高频面试题讲解视频。 欢迎大家关注我的微信公众号,不定期分享各类面试题。