目录
1、ThreadLocal是什么?
2、ThreadLocal实现原理
3、设置线程变量的2种方式
4、关于ThreadLocal的内存泄漏问题
5、使用过程中的注意事项和误区
1、ThreadLocal是什么?
比较书面的回答:
类如其名,线程本地变量。当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。这句话没问题,但容易被人误解,会被误以为:任意变量用ThreadLocal维护都是线程隔离的。后面会解答这个问题。
2、ThreadLocal实现原理
每个线程(Thread)中都有一个 ThreadLocalMap容器,通过ThreadLocal可以存放和读取Thread中的ThreadLocalMap。每个Thread对象之间是隔离的,Thread对象中的ThreadLocalMap容器自然也是隔离的。
通俗点来说:可以把ThreadLocal看着是一个工具类,通过这个工具类的get、set、remove等方法可以操作各自线程对象中的ThreadLocalMap,实现互不干扰。
要探究原里,就离不开两个类:Thread和ThreadLocal,先分别看一下这两个类,为了方便理解,这里就简要介绍核心的概念和源码,更细节的东西查看源码就行,源码不多也很简单。
1)关于Thread类
如下代码判断,代码中的任意位置我们都可以通过 Thread.currentThread() 来获取当前线程对象,即可以获得当前线程的名称、id等等属性;但是无法直接获取到Thread中的ThreadLocalMap。
public static void main(String[] args) {
// 获取当前线程-主线程
Thread mainThread = Thread.currentThread();
System.out.println("main thread id: " + mainThread.getId());
System.out.println("main thread name: " + mainThread.getName());
System.out.println("------------------------------");
// 自定义线程 1
Thread thread1 = new Thread(()-> {
// 获取当前线程
Thread th = Thread.currentThread();
System.out.println("thread id: " + th.getId());
System.out.println("thread name: " + th.getName());
});
// 设置线程名称
thread1.setName("MyThread-1");
thread1.start();
}
执行结果:
看Thread类的源码,里面有一个属性ThreadLocalMap,该Map就是用来存储各线程独立变量的。
2) 关于ThreadLocal类
前面说了,可以把它看着是一个工具类,通过这个工具类的get、set、remove等方法可以操作各自线程对象中的ThreadLocalMap。
简单看一下ThreadLocal类的源码
Thread类中的ThreadLocalMap属性是ThreadLocal类中的内部类
从源码可以看出,ThreadLocal类中有内部类ThreadLocalMap,ThreadLocalMap中有内部类Entry,Entry类有两个属性,k和v。ThreadLocalMap是用的Entry数组来存储数据(Entry对象)。
使用ThreadLocal的set方法添加一个变量,下面通过代码来看一下这个流程
public static void main(String[] args) {
ThreadLocal<User> threadLocal = new ThreadLocal<>();
// 创建线程1
Thread thread1 = new Thread(()->{
User user = new User("user1");
// 添加当前线程的变量,和其他线程隔离
threadLocal.set(user);
});
// 设置名称、启动
thread1.setName("thread1");
thread1.start();
}
ThreadLocal.set()方法的源码:
public void set(T value) {
// 得到当前线程对象
Thread t = Thread.currentThread();
// 得到当前线程对象中的Map
ThreadLocalMap map = getMap(t);
// Map不为空就把值添加进去,this就是ThreadLocal对象,如果为空就创建一个Map
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocal.get()也是类似的道理,先拿到当前线程对象,再拿到当前线程对象中的ThreadLocalMap,再从中取值。
最终还是应了开头那句话,可以把ThreadLocal看着一个工具类,可以用他来往当前线程中存储和获取值。
3、设置线程变量的2种方式
1)创建ThreadLocal对象时设置变量
创建ThreadLocal对象时设置初始化值,通过执行结果可以看出,每个线程在第一次调用get方法获取值的时候都会执行该段代码初始化变量,也就是每个线程得到的是一个新的对象,最终都存储到自己线程Thread的ThreadLocalMap容器中,不是同一个对象,也不是同一个存储容器,当然是隔离的。
@Data
class User {
private String userName;
public User() {}
public User(String userName) {
System.out.println("init user...");
this.userName = userName;
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
// 创建ThreadLocal,并设置初始化值
ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> {
// 每个线程在不执行set方法设置变量的情况下,第一次调用get方法获取值的时候执行该段代码,初始化变量,也就是每个线程得到的是一个新的对象
User user = new User("user1");
return user;
});
// 创建线程1
Thread thread1 = new Thread(()->{
System.out.println("thread1 .......");
threadLocal.get().setUserName("T1-user");
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
// 用完后清除,避免内存泄漏
threadLocal.remove();
});
// 设置名称、启动
thread1.setName("thread1");
thread1.start();
Thread.sleep(1000);
// 创建线程2
Thread thread2 = new Thread(()->{
System.out.println("thread2 .......");
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
// 用完后清除,避免内存泄漏
threadLocal.remove();
});
thread2.setName("thread2");
thread2.start();
}
}
2)通过set()方法设置变量
创建threadLocal对象,不设置初始化值,在各自的线程中通过set方法设置变量。
@Data
class User {
private String userName;
public User() {}
public User(String userName) {
System.out.println("init user...");
this.userName = userName;
}
}
public class Test {
public static void main(String[] args) throws InterruptedException {
// 创建threadLocal对象,不设置初始化值
ThreadLocal<User> threadLocal = new ThreadLocal<>();
// 创建线程1
Thread thread1 = new Thread(()->{
User user = new User("user1");
// 添加当前线程的变量,和其他线程隔离
threadLocal.set(user);
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
// 用完后清除,避免内存泄漏
threadLocal.remove();
});
// 创建线程2
Thread thread2 = new Thread(()->{
User user = new User("user2");
// 添加当前线程的变量,和其他线程隔离
threadLocal.set(user);
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
// 用完后清除,避免内存泄漏
threadLocal.remove();
});
// 设置名称、启动
thread1.setName("thread1");
thread1.start();
Thread.sleep(1000);
thread2.setName("thread2");
thread2.start();
}
}
执行结果:
4、关于ThreadLocal的内存泄漏问题
提到ThreadLocal,肯定都会想到内存泄漏,当ThreadLocalMap的Entry中的key为null,而value不为null时,该value就永远不能被访问到,就是一个无用的对象,按理来说应该被回收,而根据可达性分析导致在垃圾回收的时候进行可达性分析的时候,如果当前线程没有结束,当前线程持有ThreadLocalMap,ThreadLocalMap持有Entry对象,Entry对象包含value,value可达从而不会被回收掉,这样就存在了内存泄漏。
1)话接上面,为什么ThreadLocalMap的Entry中的key会为null呢?
因为Entry中的key是弱引用,在垃圾回收的时候,如果key没有被其他对象引用,也就是说后续代码中不会再被用到,他就会被回收,最终Entry中的key为null。原来ThreadLocal对象在这里被引用,现在key为空,ThreadLocal在这里就没有被引用,如果其他地方也没有引用ThreadLocal对象,ThreadLocal对象就可以被回收,释放内存。
在使用完ThreadLocal后调用其remove方法,就可以清除不被使用的变量,避免内存泄漏。
Thread thread2 = new Thread(()->{
User user = new User("user2");
// 添加当前线程的变量,和其他线程隔离
threadLocal.set(user);
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
// 用完后清除,避免内存泄漏
threadLocal.remove();
});
在ThreadLocal中,调用get、set、remove方法都会清除key为空的value,避免内存泄漏。
通过源码,可以追踪到 expungeStaleEntry 方法,该方法会清空key为空的value。
2)既然key是弱引用,GC回收会影响ThreadLocal的正常工作吗?
不会,因为有ThreadLocal变量引用着它,也就是说后面还会用到他,是不会被GC回收的,执行一段代码一探究竟。
Thread thread1 = new Thread(()->{
// 设置变量
threadLocal.set(new User("thread1-user"));
// 输出变量
System.out.println(threadLocal.get().getUserName());
System.gc(); //垃圾回收
System.out.println("gc...gc");
// 输出变量
System.out.println(threadLocal.get().getUserName());
});
执行结果:
可以看到,如果后续还会用到,是不会被回收的,不然问题就大了:“上一秒刚设置的变量,下一秒获取的时候就没了?”。
5、使用过程中的注意事项和误区
1)ThreadLocal与线程池
一般web容器,如tomcat就使用了线程池,或者我们自定义的线程池,线程池中的线程是存在复用情况的。如果我们在当前线程中使用ThreadLocal设置了一个变量,】并且没有执行remove方法,当前线程执行结束后,线程还在线程池中存在,线程并没有被销毁,下一个请求过来就会使用线程池中的线程,就会拿到上一个请求在线程中设置的变量。所以使用玩后一定要调用ThreadLocal的remove方法。
2)错误的理解导致使用方法
很多人看见这句话:“用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程”,就会理解为变量或者内存的完全隔离,就会出现错误的用法,例如:
错误方式一
@Data
class User {
private String userName;
public User() {}
public User(String userName) {
System.out.println("init user...");
this.userName = userName;
}
}
public class Test {
// 这是一个公共的变量
public static User user = new User("user1");
public static void main(String[] args) throws InterruptedException {
ThreadLocal<User> threadLocal = new ThreadLocal<>();
// 创建线程1
Thread thread1 = new Thread(()->{
// 设置变量
threadLocal.set(user);
// 线程1改变了user对象的值
threadLocal.get().setUserName("thread1-user");
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
});
// 创建线程2
Thread thread2 = new Thread(()->{
// 设置变量
threadLocal.set(user);
// 可拿到线程1中改变的user对象的值
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
});
// 设置名称、启动
thread1.setName("thread1");
thread1.start();
Thread.sleep(1000);
thread2.setName("thread2");
thread2.start();
}
}
错误方式二
@Data
class User {
private String userName;
public User() {}
public User(String userName) {
System.out.println("init user...");
this.userName = userName;
}
}
public class Test {
// 这是一个公共的变量
public static User user = new User("user1");
public static void main(String[] args) throws InterruptedException {
ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> user);
// 创建线程1
Thread thread1 = new Thread(()->{
// 线程1改变了user对象的值
threadLocal.get().setUserName("thread1-user");
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
});
// 创建线程2
Thread thread2 = new Thread(()->{
// 可拿到线程1中改变的user对象的值
System.out.println(Thread.currentThread().getName() + " " + threadLocal.get());
});
// 设置名称、启动
thread1.setName("thread1");
thread1.start();
Thread.sleep(1000);
thread2.setName("thread2");
thread2.start();
}
}
执行结果:
是不是意外,不是说变量在线程之间是隔离的吗?怎么线程1改了user对象的值,线程二中也随之改变了呢?
因为ThreadLocal设置变量(对象)的时候,并不是拷贝一份新的变量(对象),而是直接赋值对象的引用,如果这个变量(对象)是一个公共变量(对象),那么各线程的ThreadLocalMap中的key所指向的其实还是同一个对象,并没有隔离。
代码说明:
public class Test {
// 这是一个公共的变量
public static User user = new User("user1");
// ***** 线程不隔离
// 不能到达user对象在各线程中互相隔离的效果, 因为user本身就是公共变量
public static ThreadLocal<User> threadLocal = ThreadLocal.withInitial(()-> user);
// ***** 线程隔离
public static ThreadLocal<User> threadLocal2 = ThreadLocal.withInitial(()-> {
// 每个线程在不执行set方法设置变量的情况下,第一次执行get方法的时候都会执行本段代码,创建新的对象,现场之间使用的就不是同一个user对象
User user = new User("user1");
return user;
});
}