多线程之threadlocal
多线程之threadlocal
ThreadLocal
线程安全问题在于多个线程对共享资源的访问,通常会通过加锁,让同一时间只有一个线程能访问到共享资源。
ThreadLocal 提供另外一种解决思路,让每个线程拥有自己的私有内存空间,相互隔离。
数据结构
Threadlocal内部有一个threadLocalMap,是一个K-V结构,key是threadLocal对象,value为要保存的私有数据。
每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间, threadLocals是一个key-value的map结构,key是ThreadLocal对象(弱引用),value就是线程需要保存的私有数据。
ThreadLocal
并不维护ThreadLocalMap
,并不是一个存储数据的容器,它只是相当于一个工具包,提供了操作该容器的方法,如get、set、remove等。而ThreadLocal
内部类ThreadLocalMap
才是存储数据的容器,并且该容器由Thread
维护。
核心方法
- get/set
- remove
- initialValue : 返回初始值
源代码
/** |
get
获取当前线程 -> 获取当前线程的threadlocals -> 如果key是ThreadLocal类型, 则获取对应的value -> map为空则创建
public T get() { |
set
获取当前线程 -> 获取threadLocals -> 设置value
public void set(T value) { |
总结
(1)每个Thread维护着一个ThreadLocalMap的引用
(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储
(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。
(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中
(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。
(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
注意点
hash 冲突(entry数组)
根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置 (线性探测)
内存泄漏问题
key是弱引用,value是强引用:
发生GC时key可能会被回收,value不能被回收 - key为null,但是value不为空,entry无法被访问到;如果线程一直没有结束: Thread --引用--> ThreaLocalMap --引用--> Entry --引用--> value,这个value就无法被回收,导致内存泄露。
解决方法:
threadLocalMap set 方法会检查,如果key为null,value不为null,就清除该entry
调用get,set方法完成后再remove,将entry节点和Map的引用关系解除,整个entry GC roots 不可达,下次GC被回收
杂
1、为什么Key被设置成弱引用
创建ThreadLocal的对象t(强引用),线程中的threadLocals (类型ThreadLocal内部类ThreadLocalMap)中的entry key为强引用,那么当t被置为null时同时线程存活时ThreadLocal对象并不能被GC,造成内存泄漏
2、threadlocals在绑定线程结束时会变为不可达,被GC
3、ThreadLocal内存泄漏 (Entry key被GC,key为null,value无法被GC)
- jdk 层面check
- 在不使用ThreadLocal对象后,手动调用remove,删除key和value
4. ThreadLocal 使用场景
SimpleDateFormat 不是线程安全
使用SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
ublic class DateUtil {
private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String formatDate(Date date) {
return format1.get().format(date);
}
}Spring 事务
对于事务必须在同一个连接对象中操作,DataSourceTransactionManager 是spring的数据源事务管理器, 它会在你调用getConnection()的时候从数据库连接池中获取一个connection, 然后将其与ThreadLocal绑定, 事务完成后解除绑定。这样就保证了事务在同一连接下完成。
Session
web session的存储,一般每个请求一个线程(会有池化),使用threadlocal 存储session
Spring Security Context
spring SecurityContextHolder 设置SecurityContext 使用threadLocal
5、 threadlocal的实例和值是再堆还是栈上
在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
参考