java–线程ThreadLocal和TransmittableThreadLocal

释放双眼,带上耳机,听听看~!

一. 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&lt; MyDao &gt; session = new InheritableThreadLocal&lt; MyDao &gt;();
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应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图所示:

 

java--线程ThreadLocal和TransmittableThreadLocal

同一线程贯通三层这样你就可以根据需要,将一些非线程安全的变量以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&lt;Connection&gt; connThreadLocal = new ThreadLocal&lt;Connection&gt;();
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&#x27;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&#x27;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&#x27;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&#x27;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&lt;String&gt; vLocal = newThreadLocal&lt;String&gt;();  
6    public static void main(String[] args) {  
7        Executorexecutor = Executors.newFixedThreadPool(2);  
8        // 模拟10个请求  
9        for (int i =0; i &lt; 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(&quot;set:test&quot;);  
18                   }  
19                   System.out.println(Thread.currentThread().getName()+ &quot;:&quot; + 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 &lt; len; j++) {
8                Entry e = parentTable[j];
9                if (e != null) {
10                    @SuppressWarnings(&quot;unchecked&quot;)
11                    ThreadLocal&lt;Object&gt; key = (ThreadLocal&lt;Object&gt;) 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 &amp; (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带来什么问题呢?

如使用ThreadPoolExecutortomcat线程池的时候,某一线程中的数据和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&lt;String&gt; parent = new TransmittableThreadLocal&lt;String&gt;();
2parent.set(&quot;value-set-in-parent&quot;);
3
4Runnable task = new Task(&quot;1&quot;);
5// 额外的处理,生成修饰了的对象ttlRunnable
6Runnable ttlRunnable = TtlRunnable.get(task);
7executorService.submit(ttlRunnable);
8
9// Task中可以读取, 值是&quot;value-set-in-parent&quot;
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&lt;Object&gt; 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&lt;Object&gt;(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 &amp;&amp; !capturedRef.compareAndSet(captured, null)) {
21            throw new IllegalStateException(&quot;TTL value reference is released after run!&quot;);
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&lt;TransmittableThreadLocal&lt;?&gt;, Object&gt; captured = new HashMap&lt;TransmittableThreadLocal&lt;?&gt;, Object&gt;();
4    for (TransmittableThreadLocal&lt;?&gt; 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(&quot;========&quot;);
4         Span span=  inheritableThreadLocal.get();
5         System.out.println(span);
6         span.name=&quot;child123&quot;;//修改父引用为child123
7         inheritableThreadLocal.set(new Span(&quot;word&quot;));
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&lt;Log2Context&gt; logSession = new InheritableThreadLocal&lt;Log2Context&gt;();
27  public static TransmittableThreadLocal&lt;Log2Context&gt; threadLocalContext = new TransmittableThreadLocal &lt;Log2Context&gt;() ;
28}
29
30
31
32

 

给TA打赏
共{{data.count}}人
人已打赏
安全技术

详解Node.js API系列 Crypto加密模块(2) Hmac

2021-12-21 16:36:11

安全技术

从零搭建自己的SpringBoot后台框架(二十三)

2022-1-12 12:36:11

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索