目录
1. 概述
1.1 线程封闭
在了解ThreadLocal之前,我们先了解下什么是线程封闭
把对象封闭在一个线程里,即使这个对象不是线程安全的,也不会出现并发安全问题。
线程封闭的3种实现方式:
- Ad-hoc线程封闭:维护线程封闭性的职责完全由程序来承担,不推荐使用
- 栈封闭:就是用栈(stack)来保证线程安全
public void testThread() {
StringBuilder sb = new StringBuilder();
sb.append("Hello");
}
StringBuilder是线程不安全的,但是它只是个局部变量,局部变量存储在虚拟机栈,虚拟机栈是线程隔离的,所以不会有线程安全问题
- ThreadLocal线程封闭:简单易用
第三种方式就是通过ThreadLocal来实现线程封闭,线程封闭的指导思想是封闭,而不是共享。所以说ThreadLocal是用来解决变量共享的并发安全问题。
https://zhuanlan.zhihu.com/p/192997550
1.2 简单的案例介绍
public class ThreadLocalDemo {
public static void main(String[] args) throws InterruptedException {
final ThreadLocal<String> threadLocal = new ThreadLocal<>();
threadLocal.set("main-thread : Hello");
Thread thread = new Thread(() -> {
// 获取不到主线程设置的值,所以为null
System.out.println(threadLocal.get());
threadLocal.set("sub-thread : World");
System.out.println(threadLocal.get());
});
// 启动子线程
thread.start();
// 让子线程先执行完成,再继续执行主线
thread.join();
// 获取到的是主线程设置的值,而不是子线程设置的
System.out.println(threadLocal.get());
threadLocal.remove();
System.out.println(threadLocal.get());
}
}
运行结果:
null
sub-thread : World
main-thread : Hello
null
运行结果说明了ThreadLocal只能获取本线程设置的值,也就是线程封闭。基本上,ThreadLocal对外提供的方法只有三个get()、set(T)、remove()。
https://zhuanlan.zhihu.com/p/192997550
1.3 ThreadLocal基本介绍
ThreadLocal相当于提供了一种线程隔离,
ThreadLocal提供了线程本地变量,保证了访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量的值都不同。
使用于多线程场景下,实现传递数据,实现线程隔离。
ThreadLocal的作用:ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。
ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
1.4 ThreadLocal在虚拟机中的存储结构
如下,程序中可能会定义1-N个ThreadLocal
1.5 内存泄漏问题
会产生内存泄漏问题。
ThreadLocalMap的生命周期由持有它的线程来决定,线程如果不进入terminated状态,ThreadLocalMap就不会被GC回收,这才是ThreadLocal内存泄露的原因。
通过上图来仔细看下ThreadLocal内存结构就会发现,Entry数组对象通过ThreadLocalMap最终被Thread持有,并且是强引用。也就是说Entry数组对象的生命周期和当前线程一样。即使ThreadLocal对象被回收了,Entry数组对象也不一定被回收,这样就有可能发生内存泄漏。
内存泄漏的补救措施:
- Entry的key是弱引用的ThreadLocal对象,很容易被回收,导致key为null(但是value不为null)。所以在调用get()、set(T)、remove()等方法的时候,会自动清理key为null的Entity。
- remove()方法就是用来清理无用对象,防止内存泄漏的。所以每次用完ThreadLocal后需要手动remove()。
https://zhuanlan.zhihu.com/p/192997550
2. 应用场景
- 维护JDBC的java.sql.Connection对象,因为每个线程都需要保持特定的Connection对象。
- Web开发时,有些信息需要从controller传到service传到dao,甚至传到util类。看起来非常不优雅,这时便可以使用ThreadLocal来优雅的实现。
- 包括线程不安全的工具类,比如Random、SimpleDateFormat等
(1)
数据库连接:
/**
* 存储当前线程的连接,同一个线程的connection是线程安全的。
*/
private ThreadLocal<Connection> threadLocal = new ThreadLocal<>();
(2)session管理
当前线程有一个ThreadLocalMap<ThreadLocal, T>,当前线程可以有多个ThreadLocal。
3. ThreadLocal与synchronized的关系
- synchronized是同一时间最多只有一个线程执行,所以变量只需要存一份,算是一种时间换空间的思想
- ThreadLocal是多个线程互不影响,所以每个线程存一份变量,算是一种空间换时间的思想
4. 总结
ThreadLocal是一种隔离的思想,当一个变量需要进行线程隔离时,就可以考虑使用ThreadLocal来优雅的实现。
在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
(1)实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
(2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
(3)在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。
5. 参考资料
https://blog.csdn.net/Baisitao_/article/details/100063561
https://www.jianshu.com/p/6fc3bba12f38
https://zhuanlan.zhihu.com/p/192997550
https://blog.csdn.net/liuxiao723846/article/details/81234424