一. ThreadLocal是什么
1.1、ThreadLocal简介
在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。 在JDK5.0以后,ThreadLocal已经支持泛型,ThreadLocal类的类名变为ThreadLocal<T>。从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。每个线程使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。
1.2、ThreadLocal接口
**ThreadLocal的接口方法:**ThreadLocal类接口很简单,只有4个方法,ThreadLocal 可以存储任何类型的变量对象, get返回的是一个Object对象,但是我们可以通过泛型来制定存储对象的类型。
1
2
3
4
5 1public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本
2public void set(T value) { } //set()用来设置当前线程中变量的副本
3public void remove() { } //remove()用来移除当前线程中变量的副本
4protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的
5
**remove():**将当前线程局部变量的值删除,目的是为了减少内存的占用。当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
** initialValue():**返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
1.3、线程维护ThreadLocal机制
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。
Thread 在内部是通过ThreadLocalMap来维护ThreadLocal变量表, 在Thread类中有一个threadLocals 变量,是ThreadLocalMap类型的,它就是为每一个线程来存储自身的ThreadLocal变量的, ThreadLocalMap是ThreadLocal类的一个内部类,这个Map里面的最小的存储单位是一个Entry, 它使用ThreadLocal作为key, 变量作为 value,这是因为在每一个线程里面,可能存在着多个ThreadLocal变量
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找
我们自己就可以提供一个简单的实现版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 1package com.test;
2
3public class MySession {
4 public static final ThreadLocal< MyDao > session = new InheritableThreadLocal< MyDao >();
5}
6public class MyDao {
7 public static Log2Context getInstance() {
8 MyDao myDao = null;
9 // 创建当前线程的myDao对象
10 myDao = MySession.session.get();
11
12 if (myDao == null) {
13 myDao = new MyDao();
14 MySession.session.set(myDao);
15 }
16 return myDao;
17 }
18}
19
二. ThreadLocal与同步机制
ThreadLocal和线程同步机制相比有什么优势呢?ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。
2 .1、同步机制
同步机制一般包括synchronized或者Object方法中的wait,notify。
在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
2.2、ThreadLocal机制
而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
2.3 两者的区别总结:
**1、两者采用的方式不同:**概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
**2、两者面向问题领域不同:**当然ThreadLocal并不能替代同步机制,两者面向的问题领域不同。同步机制是为了同步多个线程对相同资源的并发访问,是为了多个线程之间进行通信 的有效方式;而ThreadLocal是隔离多个线程的数据共享,从根本上就不在多个线程之间共享资源(变量),这样当然不需要对多个线程进行同步了。所 以,如果你需要进行多个线程之间进行通信,则使用同步机制;如果需要隔离多个线程之间的共享冲突,可以使用ThreadLocal,这将极大地简化你的程 序,使程序更加易读、简洁。
三. Spring Singleton Bean与线程安全
Spring使用ThreadLocal解决线程安全问题我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全状态采用ThreadLocal进行处理,让它们也成为线程安全的状态,因为有状态的Bean就可以在多线程中共享了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图所示:
同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有关联的对象引用到的都是同一个变量。
下面的实例能够体现Spring对有状态Bean的改造思路:
TestDao:非线程安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 1package com.test;
2
3import java.sql.Connection;
4import java.sql.SQLException;
5import java.sql.Statement;
6
7public class TestDao {
8 private Connection conn;// ①一个非线程安全的变量
9
10 public void addTopic() throws SQLException {
11 Statement stat = conn.createStatement();// ②引用非线程安全变量
12 // …
13 }
14}
15
由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:
代码清单4 TestDao:线程安全
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28 1ackage com.test;
2
3import java.sql.Connection;
4import java.sql.SQLException;
5import java.sql.Statement;
6
7public class TestDaoNew {
8 // ①使用ThreadLocal保存Connection变量
9 private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
10
11 public static Connection getConnection() {
12 // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
13 // 并将其保存到线程本地变量中。
14 if (connThreadLocal.get() == null) {
15 Connection conn = getConnection();
16 connThreadLocal.set(conn);
17 return conn;
18 } else {
19 return connThreadLocal.get();// ③直接返回线程本地变量
20 }
21 }
22
23 public void addTopic() throws SQLException {
24 // ④从ThreadLocal中获取线程对应的Connection
25 Statement stat = getConnection().createStatement();
26 }
27}
28
不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否是null,如果是null,则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同的线程使用线程相关的Connection,而不会使用其它线程的Connection。因此,这个TopicDao就可以做到singleton共享了。
当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在DAO只能做到本DAO的多个方法共享Connection时不发生线程安全问题,但无法和其它DAO共用同一个Connection,要做到同一事务多DAO共享同一Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。
四.ThreadLocal<T>的具体实现
那么到底ThreadLocal类是如何实现这种“为每个线程提供不同的变量拷贝”的呢?先来看一下ThreadLocal的set()方法的源码是如何实现的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 1/**
2 * Sets the current thread's copy of this thread-local variable
3 * to the specified value. Most subclasses will have no need to
4 * override this method, relying solely on the {@link #initialValue}
5 * method to set the values of thread-locals.
6 *
7 * @param value the value to be stored in the current thread's copy of
8 * this thread-local.
9 */
10 public void set(T value) {
11 Thread t = Thread.currentThread();
12 ThreadLocalMap map = getMap(t);
13 if (map != null)
14 map.set(this, value);
15 else
16 createMap(t, value);
17 }
18
在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。
线程隔离的秘密,就在于ThreadLocalMap这个类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程特有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。
为了加深理解,我们接着看上面代码中出现的getMap和createMap方法的实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 1/**
2 * Get the map associated with a ThreadLocal. Overridden in
3 * InheritableThreadLocal.
4 *
5 * @param t the current thread
6 * @return the map
7 */
8ThreadLocalMap getMap(Thread t) {
9 return t.threadLocals;
10}
11
12/**
13 * Create the map associated with a ThreadLocal. Overridden in
14 * InheritableThreadLocal.
15 *
16 * @param t the current thread
17 * @param firstValue value for the initial entry of the map
18 * @param map the map to store.
19 */
20void createMap(Thread t, T firstValue) {
21 t.threadLocals = new ThreadLocalMap(this, firstValue);
22}
23
接下来再看一下ThreadLocal类中的get()方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 1/**
2 * Returns the value in the current thread's copy of this
3 * thread-local variable. If the variable has no value for the
4 * current thread, it is first initialized to the value returned
5 * by an invocation of the {@link #initialValue} method.
6 *
7 * @return the current thread's value of this thread-local
8 */
9public T get() {
10 Thread t = Thread.currentThread();
11 ThreadLocalMap map = getMap(t);
12 if (map != null) {
13 ThreadLocalMap.Entry e = map.getEntry(this);
14 if (e != null)
15 return (T)e.value;
16 }
17 return setInitialValue();
18}
19
再来看setInitialValue()方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 1/**
2 * Variant of set() to establish initialValue. Used instead
3 * of set() in case user has overridden the set() method.
4 *
5 * @return the initial value
6 */
7 private T setInitialValue() {
8 T value = initialValue();
9 Thread t = Thread.currentThread();
10 ThreadLocalMap map = getMap(t);
11 if (map != null)
12 map.set(this, value);
13 else
14 createMap(t, value);
15 return value;
16 }
17
获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。
进一步地,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离,为什么可以这么做?因为不同的ThreadLocal对象作为不同键,当然也可以在线程的ThreadLocalMap对象中设置不同的值了。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。
五. ThreadLocal内存泄漏
每个Thread实例都具备一个ThreadLocal的map,以ThreadLocal Instance为key,以绑定的Object为Value。而这个map不是普通的map,它是在ThreadLocal中定义的,它和普通map的最大区别就是它的Entry是针对ThreadLocal弱引用的,即当外部ThreadLocal引用为空时,map就可以把ThreadLocal交给GC回收,从而得到一个null的key。
这个threadlocal内部的map在Thread实例内部维护了ThreadLocal Instance和bind value之间的关系,这个map有threshold,当超过threshold时,map会首先检查内部的ThreadLocal(前文说过,map是弱引用可以释放)是否为null,如果存在null,那么释放引用给gc,这样保留了位置给新的线程。如果不存在slate threadlocal,那么double threshold。除此之外,还有两个机会释放掉已经废弃的threadlocal占用的内存,一是当hash算法得到的table index刚好是一个null key的threadlocal时,直接用新的threadlocal替换掉已经废弃的。另外每次在map中新建一个entry时(即没有和用过的或未清理的entry命中时),会调用cleanSomeSlots来遍历清理空间。此外,当Thread本身销毁时,这个map也一定被销毁了(map在Thread之内),这样内部所有绑定到该线程的ThreadLocal的Object Value因为没有引用继续保持,所以被销毁。
从上可以看出Java已经充分考虑了时间和空间的权衡,但是因为置为null的threadlocal对应的Object Value无法及时回收。map只有到达threshold时或添加entry时才做检查,不似gc是定时检查,不过我们可以手工轮询检查,显式调用map的remove方法,及时的清理废弃的threadlocal内存。需要说明的是,只要不往不用的threadlocal中放入大量数据,问题不大,毕竟还有回收的机制。
综上,废弃threadlocal占用的内存会在3中情况下清理:
1 thread结束,那么与之相关的threadlocal value会被清理
2 GC后,thread.threadlocals(map) threshold超过最大值时,会清理
3 GC后,thread.threadlocals(map) 添加新的Entry时,hash算法没有命中既有Entry时,会清理
那么何时会“内存泄露”?当Thread长时间不结束,存在大量废弃的ThreadLocal,而又不再添加新的ThreadLocal(或新添加的ThreadLocal恰好和一个废弃ThreadLocal在map中命中)时。
六. ThreadLocal与线程池
ThreadLocal与线程对象紧密绑定的, 一般web容器(如tomcat)使用了线程池,线程池中的线程是可能存在复用的。
如果使用了线程池(如web容器,Executor),那么即使即使父线程已经结束,子线程依然存在并被池化。这样,线程池中的线程在下一次请求被执行的时候,ThreadLocal对象的get()方法返回的将不是当前线程中设定的变量,因为池中的“子线程”根本不是当前线程创建的,当前线程设定的ThreadLocal变量也就无法传递给线程池中的线程。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 1import java.util.concurrent.Executor;
2importjava.util.concurrent.Executors;
3
4public classThreadLocalTest {
5 private static ThreadLocal<String> vLocal = newThreadLocal<String>();
6 public static void main(String[] args) {
7 Executorexecutor = Executors.newFixedThreadPool(2);
8 // 模拟10个请求
9 for (int i =0; i < 10; i++) {
10 final int flag= i;
11 executor.execute(new Runnable() {
12 @Override
13 public voidrun() {
14// vLocal.set(null);
15 //模拟某一线程改变了ThreadLocal的值
16 if (flag == 1) {
17 vLocal.set("set:test");
18 }
19 System.out.println(Thread.currentThread().getName()+ ":" + vLocal.get());
20 }
21 });
22 }
23 }
24}
25
ThreadLocal的在线程池环境下要注意:
1)并非每次web请求时候程序运行的ThreadLocal都是唯一的。
2) ThreadLocal的生命周期不等于一次Request的生命周期.
3)ThreadLocal可以用于存放与请求无关对象,不能用来传递参数
4) ThreadLocal数据是在线程创建时绑定在线程上的, 所以解决方法是在使用数据之前调用remove() 移除掉之前的其他线程产生的数据
解决方法:重构remove方法 ,先remove, 然后再初始化一次, 这样就可以保证数据是干净的了.
1
2
3
4
5
6 1@Override
2public void remove() {
3 super.remove();
4 initialValue();
5}
6
当然你也可以在调用的finally里面使用remove, 也是可以的.
七. 父子线程传递InheritableThreadLocal
ThreadLocal可以为当前线程保存局部变量,但ThreadLocal不支持继承性,如果子线程想要拿到父线程的中的ThreadLocal值怎么办呢?
JDK提供了实现方案InheritableThreadLocal: 在创建子线程的时候将父线程的局部变量传递到子线程中。
1)在创建InheritableThreadLocal对象的时候赋值给线程的t.inheritableThreadLocals变量
2)在创建新线程的时候检查父线程中t.inheritableThreadLocals变量是否为null,如果不为null则copy一份ThradLocalMap到子线程的t.inheritableThreadLocals成员变量中去。
3)因为复写了getMap(Thread)和CreateMap()方法,所以get值得时候,就可以在getMap(t)的时候就会从t.inheritableThreadLocals中拿到map对象,从而实现了可以拿到父线程ThreadLocal中的值。
1)、Thread内部为InheritableThreadLocal开辟了一个单独的ThreadLocalMap。
2)、在父线程创建一个子线程的时候,会检查这个ThreadLocalMap是否为空,不为空则会浅拷贝给子线程的ThreadLocalMap。 Thread的init相关逻辑如下:
1
2
3
4 1if (parent.inheritableThreadLocals != null)
2this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
3
4
赋值拷贝代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 1 private ThreadLocalMap(ThreadLocalMap parentMap) {
2 Entry[] parentTable = parentMap.table;
3 int len = parentTable.length;
4 setThreshold(len);
5 table = new Entry[len];
6
7 for (int j = 0; j < len; j++) {
8 Entry e = parentTable[j];
9 if (e != null) {
10 @SuppressWarnings("unchecked")
11 ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
12 if (key != null) {
13 Object value = key.childValue(e.value);
14 Entry c = new Entry(key, value);
15 int h = key.threadLocalHashCode & (len - 1);
16 while (table[h] != null)
17 h = nextIndex(h, len);
18 table[h] = c;
19 size++;
20 }
21 }
22 }
23 }
24
25
需要注意的是,
拷贝为浅拷贝。父子线程的 ThreadLocalMap 内的 key 都指向同一个 InheritableThreadLocal 对象,Value 也指向同一个 Value。
子线程的Value更改可以覆盖父线程的Value。
注意:
创建子线程的时候,子线程会继承InheritableThreadLocal中父线程的值,
但是只会在创建(new Thrad对象)的时候继承一次。如果在子线程的生命周期内,父线程修改了自己的线程局部变量值,子线程再次读取,获取的仍然是第一次读取的值。即:子线程继承父线程的值,只是在线程创建的时候继承一次。之后子线程与后父线程便相互独
八. 父子线程传递InheritableThreadLocal
我们在使用线程的时候往往不会只是简单的new Thrad对象,而是使用线程池,线程池的特点:
1)为了减小创建线程的开销,线程池会缓存已经使用过的线程
2)生命周期统一管理,合理的分配系统资源
那么线程池会给InheritableThreadLocal带来什么问题呢?
如使用ThreadPoolExecutor、tomcat线程池的时候,某一线程中的数据和ThreadLocal等在没有删除或者解绑的情况下,会被下一个Runable类或者Http请求复用。
但我们可以手动实现,在使用完这个线程的时候清除所有的localMap,在submit新任务的时候在重新重父线程中copy所有的Entry。然后重新给当前线程的t.inhertableThreadLocal赋值。这样就能够解决在线程池中每一个新的任务都能够获得父线程中ThreadLocal中的值而不受其他任务的影响,因为在生命周期完成的时候会自动clear所有的数据。Alibaba的一个库解决了这个问题https://github.com/alibaba/transmittable-thread-local
Transmittable ThreadLocal是阿里开源的库,继承了InheritableThreadLocal,优化了在使用线程池等会池化复用线程的情况下传递ThreadLocal的使用。
如何使用
这个库最简单的方式是这样使用的,通过简单的修饰,使得提交的runable拥有了上一节所述的功能。具体的API文档详见github,这里不再赘述
1
2
3
4
5
6
7
8
9
10
11 1TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
2parent.set("value-set-in-parent");
3
4Runnable task = new Task("1");
5// 额外的处理,生成修饰了的对象ttlRunnable
6Runnable ttlRunnable = TtlRunnable.get(task);
7executorService.submit(ttlRunnable);
8
9// Task中可以读取, 值是"value-set-in-parent"
10String value = parent.get();
11
原理简述
TransmittableThreadLocal 原理是在任务提交给线程池时,将ThreadLocal数据一起提交,相当于重新set一次ThreadLocal。
简单来说,有个专门的TtlRunnable和TtlCallable包装类,用于读取原Thread的ThreadLocal对象及值并存于Runnable/Callable中,在执行run或者call方法的时候再将存于Runnable/Callable中的ThreadLocal对象和值读取出来,存入调用run或者call的线程中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31 1public final class TtlRunnable implements Runnable, TtlEnhanced, TtlAttachments {
2 private final AtomicReference<Object> capturedRef;
3 private final Runnable runnable;
4 private final boolean releaseTtlValueReferenceAfterRun;
5
6 private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
7 //从父类capture复制到本类
8 this.capturedRef = new AtomicReference<Object>(capture());
9 this.runnable = runnable;//提交的runnable对象
10 this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
11 }
12 /**
13 * wrap method {@link Runnable#run()}.
14 */
15 @Override
16 public void run() {
17 //取出保存在captured中的父线程ThreadLocal值
18 Object captured = capturedRef.get();
19 //重新set
20 if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
21 throw new IllegalStateException("TTL value reference is released after run!");
22 }
23
24 Object backup = replay(captured);
25 try {
26 runnable.run();
27 } finally {
28 restore(backup);
29 }
30 }
31
capture函数的复制过程如下:
1
2
3
4
5
6
7
8
9 1@Nonnull
2public static Object capture() {
3 Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
4 for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
5 captured.put(threadLocal, threadLocal.copyValue());
6 }
7 return captured;
8}
9
其中holder记录了当前 Thread 绑定了哪些 TransmittableThreadLocal 对象。captured保存了父线程ThreadLocal的值。
接着任务提交到线程池,线程开始run() 运行时:
1)、取出保存在captured中的父线程ThreadLocal值并重新set。即将父线程值传递到了任务执行时。
2)、执行后再恢复 backup 的数据到 holder中,将 backup 中的 TransmittableThreadLocal set到当前线程中。
如何更新父线程ThreadLocal值?
如果线程之间出了要能够得到父线程中的值,同时想更新值怎么办呢?在前面我们有提到,当子线程copy父线程的ThreadLocalMap的时候是浅拷贝的,代表子线程Entry里面的value都是指向的同一个引用,我们只要修改这个引用的同时就能够修改父线程当中的值了,比如这样:
1
2
3
4
5
6
7
8
9
10 1@Override
2public void run() {
3 System.out.println("========");
4 Span span= inheritableThreadLocal.get();
5 System.out.println(span);
6 span.name="child123";//修改父引用为child123
7 inheritableThreadLocal.set(new Span("word"));
8 System.out.println(inheritableThreadLocal.get());
9}
10
这样父线程中的值就会得到更新了。能够满足父线程ThreadLocal值的实时更新,同时子线程也能共享父线程的值。不过场景倒是不是很常见的样子。
应用场景和意义
TTL的运用主要是根据其能提供跨线程、跨线程池依靠ThreadLocal来传递消息的特性决定的。
和其他传递消息的方式来比较,主要的优势在于TTL的代码透明性更好,可以做到对代码侵入性尽可能小。
1、分布式调用跟踪系统
业界有两个比较有代表性的分布式调用跟踪系统,Google的Drapper和淘宝的鹰眼。
APM现在传递节点信息,如TranceId与SpanId,是直接通过http等协议进行消息包装进行信息交互的,利用TTL我们可以在比如A机器调用跟踪B机器这样的场景下,通过利用ThreadLocal来进行消息数据的传递,这样的好处是可以将消息数据和调用线程绑定在一起,不会过多地污染业务代码,也是TTL目前主要且能够发力的地方。
2、应用容器或上层框架跨应用代码给下层SDK传递信息
这种传递消息的方式,主要优势在于可以让容器层和应用层、基础服务工具层之间解耦合,并且传递的过程也是足够透明的,不会过多地侵入用户代码,其实如果能开发人员能够把控到部署平台,给JVM启动设置参数,那么利用Agent植入TTL的方法,对于代码非侵入性是最好的。
3
、日志收集记录系统上下文
通过TTL跨日志线程间的消息传递,达成日志上下文的串联,更好地对日志数据进行分析和操作。
修饰线程池:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 1package com.turing.log2.utils;
2/**
3 * Created by huangguisu on 2019/10/28.
4 */
5import com.alibaba.ttl.threadpool.TtlExecutors;
6import com.turing.log2.context.Log2Context;
7import com.turing.log2.utils.Log2Utils;
8import com.turing.log2.context.Log2ThreadLocalSession;
9import java.util.concurrent.ExecutorService;
10
11
12public class Log2TtlExecutors{
13
14 public static ExecutorService getTtlExecutorService(ExecutorService executorService) {
15 // 额外的处理,生成修饰了的对象executorService
16 executorService = TtlExecutors.getTtlExecutorService(executorService);
17 Log2Utils.getContextGlobalId();
18 Log2ThreadLocalSession.threadLocalContext.set(Log2Context.getInstance());
19 return executorService;
20 }
21
22}
23
24
25public class Log2ThreadLocalSession {
26 public static final ThreadLocal<Log2Context> logSession = new InheritableThreadLocal<Log2Context>();
27 public static TransmittableThreadLocal<Log2Context> threadLocalContext = new TransmittableThreadLocal <Log2Context>() ;
28}
29
30
31
32