原文地址: Java 并发编程 ③ - ThreadLocal 和 InheritableThreadLocal 详解转载请注明出处!java
往期文章:git
继上一篇结尾讲的,这一篇文章主要是讲ThreadLocal 和 InheritableThreadLocal。主要内容有:github
ThreadLocal 反作用编程
ThreadLocal 适用于每一个线程须要本身独立的实例且该实例须要在多个方法中被使用,即变量在线程间隔离而在方法或类间共享的场景。 确切的来讲,ThreadLocal 并非专门为了解决多线程共享变量产生的并发问题而出来的,而是给提供了一个新的思路,曲线救国。数组
经过实例代码来简单演示下ThreadLocal的使用。安全
public class ThreadLocalExample { private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { ExecutorService service = Executors.newCachedThreadPool(); service.execute(() -> { System.out.println(Thread.currentThread().getName() + " set 1"); threadLocal.set(1); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } // 不会收到线程2的影响,由于ThreadLocal 线程本地存储 System.out.println(Thread.currentThread().getName() + " get " + threadLocal.get()); threadLocal.remove(); }); service.execute(() -> { System.out.println(Thread.currentThread().getName() + " set 2"); threadLocal.set(2); threadLocal.remove(); }); ThreadPoolUtil.tryReleasePool(service); } }
能够看到,线程1不会受到线程2的影响,由于ThreadLocal 建立的是线程私有的变量。session
咱们先看下ThreadLocal 与 Thread 的类图,了解他们的主要方法和相互之间的关系。多线程
图中几个类咱们标注一下:并发
接下去,咱们首先先开始了解这几个类的相互关系:异步
Thread 类中有一个 threadLocals 成员变量(实际上还有一个inheritableThreadLocals,后面讲),它的类型是ThreadLocal 的内部静态类ThreadLocalMap
public class Thread implements Runnable { // ...... 省略 /* ThreadLocal values pertaining to this thread. This map is maintained
ThreadLocal.ThreadLocalMap threadLocals = null; ```
ThreadLocalMap 是一个定制化的Hashmap,为何是个HashMap?很好理解,每一个线程能够关联多个ThreadLocal变量。
/** * ThreadLocalMap is a customized hash map suitable only for * maintaining thread local values. No operations are exported * outside of the ThreadLocal class. The class is package private to * allow declaration of fields in class Thread. To help deal with * very large and long-lived usages, the hash table entries use * WeakReferences for keys. However, since reference queues are not * used, stale entries are guaranteed to be removed only when
*/ static class ThreadLocalMap { // ... } ```
ThreadLocalMap 初始化时会建立一个大小为16的Entry 数组,Entry 对象也是用来保存 key- value 键值对(这个Key固定是ThreadLocal 类型)。值得注意的是,这个Entry 继承了 WeakReference
(这个设计是为了防止内存泄漏,后面会讲)
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
public void set(T value) { // ① 获取当前线程 Thread t = Thread.currentThread(); // ② 去查找对应线程的ThreadLocalMap变量 ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else // ③ 第一次调用就建立当前线程的对应的ThreadLocalMap // 而且会将值保存进去,key是当前的threadLocal,value就是传进来的值 createMap(t, value); } ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
public T get() { // ① 获取当前线程 Thread t = Thread.currentThread(); // ② 去查找对应线程的ThreadLocalMap变量 ThreadLocalMap map = getMap(t); if (map != null) { // ③ 不为null,返回当前threadLocal 对应的value值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } // ④ 当前线程的threadLocalMap为空,初始化 return setInitialValue(); } private T setInitialValue() { // ⑤ 初始化的值为null T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else // 初始化当前线程的threadLocalMap createMap(t, value); return value; } protected T initialValue() { return null; }
若是当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例对应的本地变量。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
从源码中能够看出来,自始至终,这些本地变量不是存放在ThreadLocal实例里面,而是存放在调用线程的threadLocals变量,那个线程私有的threadLocalMap 里面。
ThreadLocal就是一个工具壳和一个key,它经过set方法把value值放入调用线程的threadLocals里面并存放起来,当调用线程调用它的get方法时,再从当前线程的threadLocals变量里面将其拿出来使用。
讲到这里,实现原理就算讲完了,实际上ThreadLocal 的源码算是很是简单易懂。关于ThreadLocal 真正的重点和难点,是咱们后面的内容。
ThreadLocal 是为了线程可以安全的共享/传递某个变量设计的,可是有必定的反作用。
ThreadLocal 的主要问题是会产生脏数据和内存泄露。
先说一个结论,这两个问题一般是在线程池的线程中使用 ThreadLocal 引起的,由于线程池有线程复用和内存常驻两个特色。
脏数据应该是你们比较好理解的,因此这里呢,先拿出来说。线程复用会产生脏数据。因为线程池会重用 Thread 对象 ,那么与 Thread 绑定的类的静态属性 ThreadLocal 变量也会被重用。若是在实现的线程 run() 方法体中不显式地调用 remove() 清理与线程相关的 ThreadLocal 信息,那么假若下一个线程不调用 set() 设置初始值,就可能 get() 到重用的线程信息,包括 ThreadLocal 所关联的线程对象的 value 值。
为了便于理解,这里提供一个demo:
public class ThreadLocalDirtyDataDemo { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(1); for (int i = 0; i < 2; i++) { MyThread thread = new MyThread(); pool.execute(thread); } ThreadPoolUtil.tryReleasePool(pool); } private static class MyThread extends Thread { private static boolean flag = true; @Override public void run() { if (flag) { // 第一个线程set以后,并无进行remove // 而第二个线程因为某种缘由(这里是flag=false) 没有进行set操做 String sessionInfo = this.getName(); threadLocal.set(sessionInfo); flag = false; } System.out.println(this.getName() + " 线程 是 " + threadLocal.get()); // 线程使用完threadLocal,要及时remove,这里是为了演示错误状况 } } }
执行结果:
Thread-0 线程 是 Thread-0 Thread-1 线程 是 Thread-0
在讲这个以前,有必要看一张图,从栈与堆的角度看看ThreadLocal 使用过程中几个类的引用关系。
看到红色的虚线箭头没?这个就是理解ThreadLocal的一个重点和难点。
咱们再看一遍Entry的源码:
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
ThreadLocalMap 的每一个 Entry 都是一个对键的弱引用 - WeakReference<ThreadLocal<?>>
,这一点从super(k)
可看出。另外,每一个 Entry都包含了一个对 值 的强引用。
在前面的叙述中,我有提到Entry extends WeakReference<ThreadLocal<?>>
是为了防止内存泄露。实际上,这里说的防止内存泄露是针对ThreadLocal 对象的。
怎么说呢?继续往下看。
若是你有学习过Java 中的引用的话,这个WeakReference
应该不会陌生,当 JVM 进行垃圾回收时,不管内存是否充足,都会回收<font color='#ff6600' >只被弱引用关联</font>的对象。
更详细的相关内容能够阅读笔者的这篇文章 【万字精美图文带你掌握JVM垃圾回收#Java 中的引用】 )
经过这种设计,即便线程正在执行中, 只要 ThreadLocal 对象引用被置成 null,Entry 的 Key 就会自动在下一次 YGC 时被垃圾回收(由于只剩下ThreadLocalMap 对其的弱引用,没有强引用了)。
若是这里Entry 的key 值是对 ThreadLocal 对象的强引用的话,那么 即便ThreadLocal的对象引用被声明成null 时,这些 ThreadLocal 不能被回收,由于还有来自 ThreadLocalMap 的强引用,这样子就会形成内存泄漏。
这类key被回收( key == null
)的Entry 在 ThreadLocalMap 源码中被称为 stale entry (翻译过来就是 “过期的条目”),会在下一次执行 ThreadLocalMap 的 getEntry 和 set 方法中,将 这些 stale entry 的value 置为 null,使得原来value 指向的变量能够被垃圾回收。
“会在下一次执行 ThreadLocalMap 的 getEntry 和 set 方法中,将 这些 stale entry 的value 置为 null,使得 原来value 指向的变量能够被垃圾回收”这一部分描述,能够查阅
ThreadLocalMap#expungeStaleEntry()
方法源码及调用这个方法的地方。
这样子来看,ThreadLocalMap 是经过这种设计,解决了 ThreadLocal 对象可能会存在的内存泄漏的问题,而且对应的value 也会由于上述的 stale entry 机制被垃圾回收。
可是咱们为何还会说使用ThreadLocal 可能存在内存泄露问题呢,在这里呢,指的是还存在那个Value(图中的紫色块)实例没法被回收的状况。
请注意哦,上述机制的前提是ThreadLocal 的引用被置为null,才会触发弱引用机制,继而回收Entry 的 Value对象实例。咱们来看下ThreadLocal 源码中的注释
instances are typically private static fields in classesThreadLocal 对象一般做为私有静态变量使用
-- 若是说一个 ThreadLocal 是非静态的,属于某个线程实例类,那就失去了线程内共享的本质属性。
做为静态变量使用的话, 那么其生命周期至少不会随着线程结束而结束。也就是说,绝大多数的静态threadLocal对象都不会被置为null。这样子的话,经过 stale entry 这种机制来清除Value 对象实例这条路是走不通的。必需要手动remove() 才能保证。
这里仍是用上面的例子来作示例。
public class ThreadLocalDirtyDataDemo { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(1); for (int i = 0; i < 2; i++) { MyThread thread = new MyThread(); pool.execute(thread); } ThreadPoolUtil.tryReleasePool(pool); } private static class MyThread extends Thread { private static boolean flag = true; @Override public void run() { if (flag) { // 第一个线程set以后,并无进行remove // 而第二个线程因为某种缘由(这里是flag=false) 没有进行set操做 String sessionInfo = this.getName(); threadLocal.set(sessionInfo); flag = false; } System.out.println(this.getName() + " 线程 是 " + threadLocal.get()); // 线程使用完threadLocal,要及时remove,这里是为了演示错误状况 } } }
在这个例子当中,若是不进行 remove() 操做, 那么这个线程执行完成后,经过 ThreadLocal 对象持有的 String 对象是不会被释放的。
为何说只有线程复用的时候,会出现这个问题呢?固然啦,由于这些本地变量都是存储在线程的内部变量中的,当线程销毁时,threadLocalMap的对象引用会被置为null,value实例对象 随着线程的销毁,在内存中成为了避免可达对象,而后被垃圾回收。
// Thread#exit() private void exit() { if (group != null) { group.threadTerminated(this); group = null; } /* Aggressively null out all reference fields: see bug 4006245 */ target = null; /* Speed the release of some of these resources */ threadLocals = null; inheritableThreadLocals = null; inheritedAccessControlContext = null; blocker = null; uncaughtExceptionHandler = null; }
总结一下
在一些场景中,子线程须要能够获取父线程的本地变量,好比用一个统一的ID来追踪记录调用链路。可是ThreadLocal 是不支持继承性的,同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到对应的对象的。
为了解决这个问题,InheritableThreadLocal 也就应运而生。
public class InheritableThreadLocalDemo { private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>(); public static void main(String[] args) { // 主线程 threadLocal.set("hello world"); // 启动子线程 Thread thread = new Thread(() -> { // 子线程输出父线程的threadLocal 变量值 System.out.println("子线程: " + threadLocal.get()); }); thread.start(); System.out.println("main: " +threadLocal.get()); } }
输出:
main: hello world 子线程: hello world
要了解原理,咱们先来看一下 InheritableThreadLocal 的源码。
public class InheritableThreadLocal<T> extends ThreadLocal<T> { // ① protected T childValue(T parentValue) { return parentValue; } // ② ThreadLocalMap getMap(Thread t) { return t.inheritableThreadLocals; } // ③ void createMap(Thread t, T firstValue) { t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue); } } public class Thread implements Runnable { /* * InheritableThreadLocal values pertaining to this thread. This map is * maintained by the InheritableThreadLocal class. */ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
能够看到,InheritableThreadLocal 继承了ThreadLocal,而且重写了三个方法,看来实现的门道就在这三个方法里面。
先看代码③,InheritableThreadLocal 重写了createMap方法,那么如今当第一次调用set方法时,建立的是当前线程的inheritableThreadLocals 变量的实例而再也不是threadLocals。由代码②可知,当调用get方法获取当前线程内部的map变量时,获取的是inheritableThreadLocals而再也不是threadLocals。
能够这么说,在InheritableThreadLocal的世界里,变量inheritableThreadLocals替代了threadLocals。
代码②③都讲了,再来看看代码①,以及如何让子线程能够访问父线程的本地变量。
这要从建立Thread的代码提及,打开Thread类的默认构造函数,代码以下。
public Thread(Runnable target) { init(null, target, "Thread-" + nextThreadNum(), 0); } private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { // ... 省略无关部分 // 获取父线程 - 当前线程 Thread parent = currentThread(); // ... 省略无关部分 // 若是父线程的inheritThreadLocals不为null 且 inheritThreadLocals=true if (inheritThreadLocals && parent.inheritableThreadLocals != null) // 设置子线程中的inheritableThreadLocals变量 this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); // ... 省略无关部分 } static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) { return new ThreadLocalMap(parentMap); }
再来看看里面是如何执行createInheritedMap 的。
private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (int j = 0; j < len; j++) { Entry e = parentTable[j]; if (e != null) { @SuppressWarnings("unchecked") ThreadLocal<Object> key = (ThreadLocal<Object>) e.get(); if (key != null) { // 这里调用了重写的代码① childValue Object value = key.childValue(e.value); Entry c = new Entry(key, value); int h = key.threadLocalHashCode & (len - 1); while (table[h] != null) h = nextIndex(h, len); table[h] = c; size++; } } } }
在该构造函数内部把父线程的inheritableThreadLocals成员变量的值复制到新的ThreadLocalMap 对象中。
## 小结
本章讲了ThreadLocal 和 InheritableThreadLocal 的相关知识点。
ThreadLocal 实现线程内部变量共享,InheritableThreadLocal 实现了父线程与子线程的变量继承。可是还有一种场景,InheritableThreadLocal 没法解决,也就是在使用线程池等会池化复用线程的执行组件状况下,异步执行执行任务,须要传递上下文的状况。
针对上述状况,阿里开源了一个TTL
库,即Transmittable ThreadLocal来解决这个问题,有兴趣的朋友们能够去看看。
以后有时间的话我会单独写一篇文章介绍一下。
若是本文有帮助到你,但愿能点个赞,这是对个人最大动力🤝🤝🤗🤗。