多线程之threadlocal

多线程之threadlocal

ThreadLocal

线程安全问题在于多个线程对共享资源的访问,通常会通过加锁,让同一时间只有一个线程能访问到共享资源。

ThreadLocal 提供另外一种解决思路,让每个线程拥有自己的私有内存空间,相互隔离。

数据结构

Threadlocal内部有一个threadLocalMap,是一个K-V结构,key是threadLocal对象,value为要保存的私有数据。

每个Thread线程内部都有一个ThreadLocalMap变量threadLocals,这个threadLocals就是这个线程的私有空间, threadLocals是一个key-value的map结构,key是ThreadLocal对象(弱引用),value就是线程需要保存的私有数据。

Screen Shot 2020-08-18 at 5.12.05 PM

Screen Shot 2020-08-18 at 1.52.32 PM

ThreadLocal并不维护ThreadLocalMap,并不是一个存储数据的容器,它只是相当于一个工具包,提供了操作该容器的方法,如get、set、remove等。而ThreadLocal内部类ThreadLocalMap才是存储数据的容器,并且该容器由Thread维护。

Screen Shot 2020-08-18 at 10.40.39 PM

核心方法

  • get/set
  • remove
  • initialValue : 返回初始值

源代码

/**
* 线程局部变量threadLocals为ThreadLocal.ThreadLocalMap类型
*/
public class Thread implements Runnable {
ThreadLocal.ThreadLocalMap threadLocals = null;
}

/**
* ThreadLocal$ThreadLocalMap 散列表结构
* key=ThreadLocal value=Object
*/
static class ThreadLocalMap {
private Entry[] table;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}

get

获取当前线程 -> 获取当前线程的threadlocals -> 如果key是ThreadLocal类型, 则获取对应的value -> map为空则创建

public T get() {
// 获取当前线程私有的map thread.threadLocals
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 获取map的value值
if (map != null) {
// map的key是ThreadLocal类型
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果map=null,初始化map,下文有讲解
return setInitialValue();
}

/**
* 返回ThreadLocalMap类型的thread.threadLocals
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}


/**
* 初始化value,map=null时会初始化map
*/
private T setInitialValue() {
T value = initialValue();// 初始值
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);// 将初始值存入map
else
createMap(t, value);// map=null时初始化map
return value;
}

/**
* 返回map中value的初始值
* 默认为null,一般需要重写该方法以获得非null值
*/
protected T initialValue() {
return null;
}

set

获取当前线程 -> 获取threadLocals -> 设置value

public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(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,造成内存泄漏

Screen Shot 2020-10-05 at 9.50.57 PM

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>() {
    @Override
    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的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。


参考

https://mp.weixin.qq.com/s?__biz=MzAxMjEwMzQ5MA==&mid=2448892399&idx=2&sn=59c1cbe4fb3890a065c98071611d4a64&chksm=8fb57bc2b8c2f2d4b6e0b57ef065d5d178dce86fa9362ec0b40f88298b2bcf3da6467e46caff&scene=158#rd

https://mp.weixin.qq.com/s/5feVZ5zHOeGcI_yUAFZ5uw

https://mp.weixin.qq.com/s/vtLKqtHjAknbIV8RBojtbQ