目录
5、ThreadLocal与Thread、ThreadLocalMap
1、概述
ThreadLocal叫做“线程变量”,其中填充的变量属于当前线程,对其他线程是隔离的,不会被别的线程读取或修改。这种思路叫线程封闭。
原理是,每个线程内部都有一个ThreadLocalMap,它用ThreadLocal作为键,能够存储值。
ThreadLocal适合,每个线程需要有自己的实例,且该实例需要在多个方法间传递,但不希望线程间共享。
- 实例需要在多个方法间传递,就可以保存在当前线程的ThreadLocal中,就不需要通过参数传递了。需要时直接get()取出。
2、ThreadLocal与sync的区别
它们都用于解决多线程的并发访问,区别是:
-
synchronized用于线程间的数据共享,而ThreadLocal用于线程内的数据共享,线程间的数据隔离
-
synchronized利用锁机制,让变量或代码块在同一时刻只能被一个线程访问。
而ThreadLocal在每个线程内都提供了变量的副本,每个线程只能操作自己内部的副本,避免了共享问题
3、使用方法
ThreadLocal的变量通常用private static修饰
public class ThreadLocalDemo {
private static ThreadLocal<String> localVar = new ThreadLocal<>();
//打印出本线程内的localVar值
public static void printLocalVar(String str){
System.out.println(str + " " + localVar.get());
//清除本地内存中的本地变量
localVar.remove();
}
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
ThreadLocalDemo.localVar.set("t1的localVar");
printLocalVar("t1");
}, "t1");
t1.start();
Thread.sleep(1000);
Thread t2 = new Thread(() -> {
ThreadLocalDemo.localVar.set("t2的localVar");
printLocalVar("t2");
}, "t2");
t2.start();
}
}
注意:
- ThreadLocal的泛型是Object的,可以定义成map、set、list等等
- 一个类中可以定义多个ThreadLocal属性
4、原理分析
1、set方法
赋值过程:
- 首先获取当前线程对象,并获取当前线程的ThreadLocalMap
- 如果这个map为null,就调用createMap()方法初始化map,初始化需要传入泛型和要存储的值
- 如果map不为null,就调用set的重载方法,它会将当前线程对象作为键,存储值。
2、ThreadLocalMap
createMap()
其实线程对象的threadLocals属性,就是用来存储ThreadLocalMap的。
ThreadLocalMap是ThreadLocal的静态内部类。它用Entry保存数据,而且继承了弱引用。
Entry内部使用ThreadLocal类型的变量作为键,保存传入的值。
ThreadLocalMap如何工作
这个Map使用哈希确定下标,将值保存在数组中,类似于HashMap。但没有实现Map接口,也没有链表结构。
一个线程只有一个ThreadLocalMap,但是可以创建多个ThreadLocal字段,所以需要使用数组存储每个Entry。
不使用链表,它解决哈希冲突的方式是,找空隙:
- 要插入一组数据,根据ThreadLocal对象的哈希值,计算出一个下标
- 如果该下标对应的位置是空的,就初始化一个Entry,存入数据
- 如果不为空,就检查它的key,如果正好和要存入的key一样,此次是覆盖操作,直接替换Value
- 如果不为空且key不符,说明出现了哈希冲突,就找下一个空的位置,继续判断,直到成功插入。
- 在get的时候也是,如果下标中的key不符,说明插入时有哈希冲突,就找下一个位置,直到找到key
3、get方法
获取流程:
- 获取当前线程对象
- 如果map不为null,就通过ThreadLocal对象,取出对应的Entry
- 如果entry不为空,就获取Entry中的Value,返回。
- 如果前一步中map为空,就调用setInitialValue()方法
setInitialValue()
这个方法是给ThreadLocal设置初始值
4、remove方法
将ThreadLocal的值,从当前线程的ThreadLocalMap中删除。
5、总结
ThreadLocal的值,存储在当前线程对象的threadLocals属性中,这个属性对应一个ThreadLocalMap对象,在第一次调用ThreadLocal的set方法时被初始化。
ThreadLocalMap保存对象的策略是,以ThreadLocal为键,映射存储值。
这个ThreadLocal是多线程共享的,而ThreadLocalMap是线程私有的,所以每个线程都可以根据ThreadLocal存储不同的值,别的线程也无法获取到。
5、使用场景
1、Spring实现事务隔离级别
Spring使用ThreadLocal的方式,保证一个线程中的数据库操作都是使用的同一个连接对象
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
……
2、解决日期的线程安全
项目中有部分用户的时间出错,发现是多个线程共享一个SimpleDataFormat的问题。
使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add()。
如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
但是每个线程内部都new一个SimpleDataFormat对象也不太好,所以使用ThreadLocal包装SimpleDataFormat,解决了线程安全的问题。
3、多个方法调用
一个线程经常需要横跨多个方法调用,那么它的参数就必须层层传递,给每个方法都加上相同的参数不太优雅。
而且,如果中间遇到第三方类库,参数就无法传递了。可以使用ThreadLocal,开始时把参数存进去,需要时直接get取出即可。
4、JDBC的数据库连接
从数据库连接池里获取的连接 Connection,在 JDBC 规范里并没有要求这个 Connection 必须是线程安全的。
数据库连接池通过线程封闭技术,保证一个 Connection 一旦被一个线程获取之后,在这个线程关闭 Connection 之前的这段时间里,不会再分配给其他线程,从而保证了 Connection 不会有并发问题
6、常见问题
1、ThreadLocal对象存放在哪里
Java中,栈内存是线程私有的,堆内存是线程共享的。
ThreadLocal对象存放在堆上。
2、如何共享ThreadLocal数据
使用InheritableThreadLocal,可以实现主线程和子线程共享ThreadLocal数据。
在主线程new一个InheritableThreadLocal的实例,子线程就可以获取到它的值。它也是ThreadLocal类型。
final ThreadLocal threadLocal = new InheritableThreadLocal();
3、为什么把ThreadLocalMap的键弄成弱引用
减少了内存泄漏。
存储了一个对象到ThreadLocalMap中,如果后来把该ThreadLocal类型的对象设置为了null,就只有ThreadLocalMap的key引用了这个对象。
- 如果定义成弱引用,当ThreadLocal失去所有外部强引用,下次GC它就能被垃圾回收,对应的Entry的Key将置为null。
- 如果使用强引用,那么在已经失去该对象的所有外部引用的情况下,它仍将一直存在,无法被垃圾回收。
- ThreadLocalMap的生命周期和线程实例一致,只要线程一直运行,ThreadLocalMap就不会销毁,所以Key也不会销毁,可能导致内存泄露。
但是,把Key做成弱引用也无法完全避免内存泄漏,因为Value是强引用的,所以需要及时清理才行。
4、ThreadLocal的内存泄漏问题
在线程池中使用ThreadLocal,需要考虑内存泄露问题。
ThreadLocalMap中,Entry的Key是ThreadLocal对象的弱引用。如果一个对象只存在弱引用,那它在下一次GC中一定会被清理。
所以,如果ThreadLocal没有外部的强引用,在垃圾回收时,key会被清理掉,但是Entry的Value是强引用,只有线程结束后才能被回收。
-
因为线程对象通过强引用指向ThreadLocalMap,而ThreadLocalMap也是通过强引用指向Entry,所以Entry的value是强引用。
key不再使用,被清理掉了,就没有任何途径能访问到这个value,所以value属于垃圾。
ThreadLocalMap在实现时考虑了这种情况,因此调用set()、get()时,会清理掉键为null的Entry对象。
保险起见,使用完ThreadLocal的值后,就手动调用remove()方法,把值全部清理掉,这样value就能被垃圾回收了。
调用set、get清理对象具体的流程是:
- 调用ThreadLocal的get(),它会先获取当前线程对象的ThreadLocalMap
- 调用ThreadLocalMap的getEntry(),它会调用哈希函数,计算出一个数组下标。
- 如果发生了哈希冲突(下标的key不等于所需的key),就调用getEntryAfterMiss()
- ThreadLocalMap使用开放定址法解决哈希冲突,即向后寻找所需的key值。
- 期间如果遇到key为null的Entry,就会调用expungeStaleEntry(),将key为null的Entry的value也设置为null
5、ThreadLocal与Thread、ThreadLocalMap
Thread有一个threadLocals属性,存放一个线程私有的ThreadLocalMap类型变量。
ThreadLocalMap是ThreadLocal的静态内部类。它类似Map,使用Entry存放数据,key为ThreadLocal对象。
ThreadLocal相当于ThreadLocalMap的工具类。调用ThreadLocal对象的get、set方法,底层是在调用ThreadLocalMap的get、set方法
ThreadLocal帮助ThreadLocalMap初始化。