ThreadLocal详解-笔记
什么是ThreadLocal
ThreadLocal在《Java核心技术 卷一》中被称作线程局部变量,我们可以利用ThreadLocal创建只能由同一线程读和写的变量。因此就算两个线程正在执行同一段代码,并且这段代码具有对ThreadLocal变量的引用,这两个线程也无法看到彼此的ThreadLocal变量。
ThreadLocal两大使用场景
1、每个线程需要一个独享的线程变量(通常是在工具类中使用,典型的比如线程非安全的SimpleDateFormat和Random类)
2、 每个线程内需要保存全局变量,可以让不同方法使用,避免参数不断传递的麻烦(列如在拦截器中获取用户信息,用于当前线程中使用)。
ThreadLocal在场景一中的使用
/*** @author: Durian* @date: 2020/9/3 16:59* @description: 场景一 SimpleDateFormat*/public class ThreadLocalDemo1{public static void main(String[] args){new Thread(() -> new ThreadLocalDemo1().date(10)).start();new Thread(() -> new ThreadLocalDemo1().date(120)).start();}private void date(int seconds){Date date = new Date(seconds * 1000); // 从1970-1-1开始SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd hh:mm:ss");String s = sdf.format(date);System.out.println(Thread.currentThread().getName() + " = " + s);}}运行结果:Thread-0 = 1970-01-01 08:00:10Thread-1 = 1970-01-01 08:02:00
上面的代码展示了当两个线程同时使用SimpleDateFormat去格式化时间,可以看出并没有任何问题,因为在该代码中,SimpleDateFormat在每个线程中都会被实例化,每个线程都拥有自己的SimpleDateFormat类,所以并不会有线程安全问题。但是这里仅仅是两个线程使用,如果是很多线程使用呢,那么就会创建许多SimpleDateFormat对象,但是使用完马上又被销毁,这是很消耗资源的操作,于是将SimpleDateFormat提取出来作为一个static全局变量,降低资源 消耗。
看下面的代码:
/*** @author: Durian* @date: 2020/9/3 17:09* @description: 许多线程使用SimpleDateFormat*/public class ThreadLocalDemo2{private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");private static Set<String> dates = Collections.synchronizedSet(new HashSet<>());public static void main(String[] args) throws InterruptedException{ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 100; i++){int finalI = i;executorService.execute(() -> new ThreadLocalDemo2().date(finalI * 10));}executorService.shutdown();Thread.sleep(1000);System.out.println(dates.size());}private void date(int seconds){Date date = new Date(seconds * 1000); // 从1970-1-1开始String s = sdf.format(date);System.out.println(Thread.currentThread().getName() + " = " + s);dates.add(s);}}部分运行结果:pool-1-thread-9 = 1970-01-01 08:01:20 // 1970-01-01 08:01:20pool-1-thread-3 = 1970-01-01 08:00:00pool-1-thread-8 = 1970-01-01 08:01:20 // 1970-01-01 08:01:20pool-1-thread-4 = 1970-01-01 08:00:40pool-1-thread-10 = 1970-01-01 08:02:00pool-1-thread-10 = 1970-01-01 08:02:10pool-1-thread-3 = 1970-01-01 08:02:30set总数:81
运行结果出现了重复的结果,并且set集合大小不等于100,可以表明在多个线程同时使用一个SimpleDateFormat变量时,将出现线程安全问题。
在SimpleDateFormat的源码中,可以看到format的方法中直接对calendar进行了直接的值操作,在多线程环境中将出现安全问题。

从上面代码可以看出,多线程中使用SimpleDateFormat出现线程安全问题的源头就是,SimpleDateFormat对象被线程共享了,而SimpleDateFormat类的方法并不是线程安全的,所以解决的办法就是使得SimpleDateFormat不被线程所共享。而ThreadLocal的本意就是“线程本地”,意思就是将一个全局变量为每一个线程做一份拷贝,每个线程的变量是隔离的,互不影响。
通过代码演示:
/*** @author: Durian* @date: 2020/9/3 17:23* @description:*/public class ThreadLocalDemo3{private static ThreadLocal<SimpleDateFormat> sdfThreadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")); // 使用lambda表达式初始化ThreadLocal// 用来判断结果是否都是唯一private static Set<String> dates = Collections.synchronizedSet(new HashSet<>());public static void main(String[] args) throws InterruptedException{ExecutorService executorService = Executors.newFixedThreadPool(10);for (int i = 0; i < 100; i++){int finalI = i;executorService.execute(() -> new ThreadLocalDemo3().date(finalI * 10));}executorService.shutdown();Thread.sleep(3000); // 确保线程全部执行完System.out.println("set总数: " + dates.size());}private void date(int seconds){Date date = new Date(seconds * 1000); // 从1970-1-1开始SimpleDateFormat sdf = sdfThreadLocal.get();String s = sdf.format(date);System.out.println(Thread.currentThread().getName() + " = " + s);dates.add(s);sdfThreadLocal.remove();}}运行结果pool-1-thread-10 = 1970-01-01 08:14:20pool-1-thread-6 = 1970-01-01 08:16:10pool-1-thread-3 = 1970-01-01 08:14:30pool-1-thread-4 = 1970-01-01 08:14:10pool-1-thread-7 = 1970-01-01 08:14:00pool-1-thread-2 = 1970-01-01 08:13:50pool-1-thread-6 = 1970-01-01 08:16:30pool-1-thread-10 = 1970-01-01 08:16:20pool-1-thread-9 = 1970-01-01 08:16:00pool-1-thread-1 = 1970-01-01 08:15:50pool-1-thread-8 = 1970-01-01 08:15:30pool-1-thread-5 = 1970-01-01 08:15:20set总数: 100
从运行结果可以发现,使用了ThreadLocal后,SimpleDateFormat线程安全的问题被解决了。
建议使用Java8的日期时间类和对应的线程安全的DateTimeFormat类
ThreadLocal在场景二中的使用
比如我们有这么一个类,它需要获取一个用户信息,然后执行一系列的业务逻辑操作,这些逻辑操作分散在各个方法中。
如果不使用一个全局变量,就得对每一个方法进行参数传递,而使用全局变量后的代码如下:
/*** @author: Durian* @date: 2020/9/3 17:41* @description: 场景二的使用,每个线程内需要保存全局变量*/public class ThreadLocalDemo4{private static User user;public static void main(String[] args){for (int i = 0; i < 10; i++){new Thread(() ->{new Intercept().intercept();new Service1().intercept();}).start();}}// 模拟拦截器static class Intercept{public void intercept(){// 拦截器获取用户信息放入全局变量user中,方便操作user = new User(Math.random() * 10);}}// 业务类1static class Service1{public void intercept(){new Service2().process();new Service3().process();}}// 业务类2static class Service2{public void process(){System.out.println(user.getMoney());user.setMoney(Math.random() * 10);}}// 业务类3static class Service3{public void process(){System.out.println(user.getMoney());}}}@Data@AllArgsConstructorclass User{private Double money;}部分运行结果:2.2954366413594722.2954366413594722.2954366413594722.2954366413594722.2954366413594722.2954366413594720.1597985241974753
可以看到运行结果有很多money是重复的,这是不符合预期的,因为user是全局变量,在多线程环境中对user的值进行操作会出现线程安全问题。这也就是ThreadLocal使用的第二种场景,每个线程内需要保存全局变量,可以让不同方法使用,避免参数不断传递的麻烦。
使用ThreadLocal后:
/*** @author: Durian* @date: 2020/9/3 17:41* @description: 场景二的使用,每个线程内需要保存全局变量*/public class ThreadLocalDemo5{public static void main(String[] args){for (int i = 0; i < 10; i++){new Thread(() ->{new Intercept().intercept();new Service1().intercept();}).start();}}// 模拟拦截器static class Intercept{public void intercept(){User user = new User(Math.random() * 10);// 将拦截器获取到的用户信息放入ThreadLocal中UserContextHolder.holder.set(user);}}// 业务类1static class Service1{public void intercept(){new Service2().process();new Service3().process();}}// 业务类2static class Service2{public void process(){System.out.println(UserContextHolder.holder.get().getMoney());// 使用ThreadLocal的get方法拿到UserUserContextHolder.holder.get().setMoney(Math.random() * 10);}}// 业务类3static class Service3{public void process(){System.out.println(UserContextHolder.holder.get().getMoney());// 使用完成后remove掉userUserContextHolder.holder.remove();}}}// 表示持有User对象class UserContextHolder{// 在第二种场景下使用ThreadLocal是不需要先初始化的public static ThreadLocal<User> holder = new ThreadLocal<>();}
在引入ThreadLocal后,每个业务类的方法可以方便的通过UserContextHolder的静态属性holder拿到拦截器放入的User信息,不用再进行参数传递,也避免了使用static变量造成的线程安全问题。
ThreadLocal的原理
在ThreadLocal中有一个重要内部类ThreadLocalMap;而在每个Thread类中,拥有一个ThreadLocal.ThreadLocalMap类型的属性threadLocals,也就是说,每个线程都有属于自己的threadLocals,而ThreadLocalMap中有一个核心类Entry,Entry为key-value对,key中存储的是当前线程的ThreadLocal,value为要保持的线程的私有变量,也就是想要放入ThreadLocal的变量。它们的关系如下图所示:

每个线程的ThreadLocalMap拥有多个Entry,Entry的key是本线程的ThreadLocal,而Value可以有多个不同的值,这是为了保证在一个线程中可能会使用多个ThreadLocal来保持变量。
上面的分析可知,ThreadLocal的核心就时ThreadLocalMap,接下来看一看ThreadLocalMap的分析。
ThreadLocalMap解析
static class ThreadLocalMap {/*** The initial capacity -- MUST be a power of two.*/private static final int INITIAL_CAPACITY = 16;/*** The table, resized as necessary.* table.length MUST always be a power of two.*/private Entry[] table;/*** The number of entries in the table.*/private int size = 0;/*** The next size value at which to resize.*/private int threshold; // Default to 0}
由上面可见在ThreadLocalMap中维护着table,size以及threshold三个属性。table是一个Entry数组主要用来保存具体的数据,size是table的大小,而threshold这表示当table中元素数量超过该值时,table就会扩容。了解了ThreadLocalMap的结构之后,我们就来看下其set方法吧。
private void set(ThreadLocal<?> key, Object value) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {ThreadLocal<?> k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}
通过上面的代码分析得出,整个的设值过程如下:
- 通过ThreadLocal的threadLocalHashCode值定位到table中的位置i。
- 如果table中i这个位置是空的,那么就新创建一个Entry对象放置在i这个位置。
- 如果table中i这个位置不为空,则取出来i这个位置的key。
- 如果这个key刚好就是当前ThreadLocal对象,则直接修改该位置上Entry对象的value。
- 如果这个key不是当前TreadLocal对象,则寻找下一个位置的Entry对象,然后重复上述步骤进行判断(ThreadLocalMap处理hash冲突的策略时线性探测)。
对于get方法也是同样的原理从ThreadLocalMap中获取值。那么ThreadLocal是如何生成threadLocalHashCode值的呢?
public class ThreadLocal<T> {private final int threadLocalHashCode = nextHashCode();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);}}复制代码
可见我们在初始化一个ThreadLocal对象的时候都为其会生成一个threadLocalHashCode值,每初始化一个ThreadLocal该值就增加0x61c88647。这样就可以做到每个ThreadLocal在ThreadLocalMap中找到一个存储值的位置了。
ThreadLocal重要方法介绍
protected T initialValue()
ThreadLocal的初始化方法
protected T initialValue() {return null;}
该方法会放回当前线程对应的初始值,这是一个延迟加载的方法,只有在调用get()方法时,才会触发。
当线程第一次使用get方法访问变量时,将触发此方法,如果线程之前调用了set方法,则不会为再为该线程调用initiaValue方法。
通常,每个线程只会调用一次initiaValue方法,除非再调用remove()后再次调用get方法,则会再次调用该方法。
如果在创建ThreadLocal时就想要初始化,需要使用内部类的方式实现initiaValue方法。在上文的第一种使用场景中就是这样做的,但是使用的是withInitial方法,该方法作用与initiaValue一致,但可用lambda简化初始化代码。
void set(T value)
为这个线程设置一个值,会覆盖已有的值
public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t); // 获取该线程的ThreadLocalMapif (map != null)map.set(this, value); // 如果map != null 将值设置到对应ThreadLocal的valueelsecreateMap(t, value); // 如果map == null,创建一个ThreadLocalMap再将值设置进去}
T get()
获取该线程中的值
public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this); // 获取当前ThreadLocal对应的Entryif (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result; // 返回获得的对应ThreadLocal的value}}return setInitialValue(); // 如果map == null 调用初始化方法,返回重写方法的返回值}
void remve()
删除对应线程中的值
public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this); // 移除对应ThreadLocal的值,不会删除线程中其他ThreadLocal的}
可以看到,不管是get、set和remove最终调用的方法都是map的get、set和remove方法。
ThreadLocal的使用注意点
内存泄露问题
每个thread中都存在一个map, map的类型是ThreadLocal.ThreadLocalMap. Map中的key为一个threadlocal实例. 这个Map的确使用了弱引用,不过弱引用只是针对key. 每个key都弱引用指向threadlocal. 当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收. 但是,我们的value却不能回收,因为存在一条从current thread连接过来的强引用. 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。所以得出一个结论就是只要这个线程对象被gc回收,就不会出现内存泄露,但在threadLocal设为null和线程结束这段时间不会被回收的,就发生了我们认为的内存泄露。其实这是一个对概念理解的不一致,也没什么好争论的。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的就可能出现内存泄露 。(在web应用中,每次http请求都是一个线程,tomcat容器配置使用线程池时会出现内存泄漏问题)

ThreadLocalMap的每个Entry都是一个对key的弱引用(弱引用将会在发生GC的时候被回收),同时,每个Entry都包含了一个强引用的value,。
正常情况下,当线程终止,保存在ThreadLocal中的value就是会垃圾回收,因为没有任何强引用了
但是如果线程不被终止(线程持续很久)那么key对应的value就不会被回收,因为调用链还存在,如下图

因为Thread和value之间的强引用还存在,导致value无法被回收,可能出现OOM。
JDK中的处理是,在set、remove、rehash方法中会扫描key为null的Entry,并把对应的value设置为nul,这样value的对象就可以被回收。

但是如果一个ThreadLocal不被使用,那么实际上set、remove、rehash方法也不会被调用,同时线程又不停止,那么调用链会一直存在,将导致value的内存泄露。
ThreadLocal只是操作Thread中的ThreadLocalMap,每个Thread都有一个map,ThreadLocalMap是线程内部属性,ThreadLocalMap生命周期是和Thread一样的,不依赖于ThreadLocal。
ThreadLocal通过Entry保存在map中,key为Thread的弱引用(GC时会自动回收),value为存入的变量副本,一个线程不管有多少个ThreadLocal,都是通过一个ThreadLocalMap来存放局部变量的,可以再源码中看到,set值时先获取map对象,如果不存在则创建,threadLocalMap初始大小为16,当容量超过2/3时会自动扩容。
避免线value内存露方法
- 调用remove方法,就会删除对应的Entry对象,可以避免内存泄露,所以应该在每次使用完ThreadLocal后,调用remove方法。
