java(8)--线程ThreadLocal详解

一. ThreadLocal是什么


1.1、ThreadLocal简介:维护当前线程中变量的副本。

      在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。 在JDK5.0以后,ThreadLocal已经支持泛型,ThreadLocal类的类名变为ThreadLocal<T>。从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

     ThreadLocal 是一个本地线程副本变量工具类,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。通过这种方式,避免资源在多线程间共享。

     当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。每个线程使用的都是自己从内存中拷贝过来的变量的副本, 这样就不存在线程安全问题,也不会影响程序的执行性能。

1.2、ThreadLocal接口简介

ThreadLocal的接口方法:ThreadLocal类接口很简单,只有4个方法,ThreadLocal 可以存储任何类型的变量对象, get返回的是一个Object对象,但是我们可以通过泛型来制定存储对象的类型。

public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) { } //set()用来设置当前线程中变量的副本
public void remove() { } //remove()用来移除当前线程中变量的副本
protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的

remove():将当前线程局部变量的值删除,目的是为了减少内存的占用。当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

 initialValue():返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

1.3、ThreadLocal简单案例

package com.test;
 
public class MySession {
    public static final ThreadLocal< MyDao > session = new InheritableThreadLocal< MyDao >();
}
public class MyDao {  
    public static Log2Context getInstance() {
        MyDao myDao = null;
        // 创建当前线程的myDao对象
        myDao = MySession.session.get();
 
        if (myDao == null) {
            myDao = new MyDao();
            MySession.session.set(myDao);
        }
        return myDao;
    }
} 

二.线程维护ThreadLocal<T>的具体实现机制


ThreadLocal类是实现这种“为每个线程提供不同的变量拷贝" 机制:
1、每个Thread对象都有一个ThreadLocalMap:Thread类中有一个ThreadLocalMap类型的threadLocals 变量,用来存储线程自身的ThreadLocal变量。
2、ThreadLocalMap是ThreadLocal类的一个内部类:这个Map里面的最小的存储单位是一个Entry, 它使用ThreadLocal实例作为key, 待存储的变量作为 value,这样线程可以存储多个ThreadLocal变量.
2)变量存储到ThreadLocalMap:存储到当创建一个ThreadLocal对象的时候,就会将该ThreadLocal对象添加到该ThreadLocalMap的map中,其中键就是ThreadLocal实例对象,值可以是存放的value (范型)。

初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找

ThreadLocal类提供set/get方法存储和获取value值,但实际上ThreadLocal类并不存储value值,真正存储是靠ThreadLocalMap这个类,ThreadLocalMap是ThreadLocal的一个静态内部类,它的key是ThreadLocal实例对象,value是任意Object对象。

1、ThreadLocal类set方法

先来看一下ThreadLocal的set()方法的源码是如何实现的:  

/**
    * Sets the current thread's copy of this thread-local variable
    * to the specified value.  Most subclasses will have no need to
    * override this method, relying solely on the {@link #initialValue}
    * method to set the values of thread-locals.
    *
    * @param value the value to be stored in the current thread's copy of
    *        this thread-local.
    */ 
   public void set(T value) { 
       Thread t = Thread.currentThread(); 
       ThreadLocalMap map = getMap(t); 
       if (map != null) 
           map.set(this, value); 
       else 
           createMap(t, value); 
   } 

在这个方法内部我们看到,首先通过getMap(Thread t)方法获取一个和当前线程相关的ThreadLocalMap,然后将变量的值设置到这个ThreadLocalMap对象中,当然如果获取到的ThreadLocalMap对象为空,就通过createMap方法创建。然后往map(ThreadLocalMap对象)里添加K-V,K是当前ThreadLocal对象实例(即源码的this),V是我们传入的value。

2、ThreadLocalMap类的定义

ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取(对比Map对象来理解),每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。

3、Thread类维护了一个ThreadLocalMap的变量引用

ThreadLocalMap   map的获取是需要从Thread类对象里面取,看一下Thread类的定义。

class Thread implements Runnable {
    //.....//
    
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    //.....//
}

我们接着看上面代码中出现的getMap和createMap方法的实现:

/** 
 * Get the map associated with a ThreadLocal. Overridden in 
 * InheritableThreadLocal. 
 * 
 * @param  t the current thread 
 * @return the map 
 */  
ThreadLocalMap getMap(Thread t) {  
    return t.threadLocals;  
}  
  
/** 
 * Create the map associated with a ThreadLocal. Overridden in 
 * InheritableThreadLocal. 
 * 
 * @param t the current thread 
 * @param firstValue value for the initial entry of the map 
 * @param map the map to store. 
 */  
void createMap(Thread t, T firstValue) {  
    t.threadLocals = new ThreadLocalMap(this, firstValue);  
}  

t.threadLocals 设置thread实例的threadLocals变量。

4、ThreadLocal类中的get()方法

接下来再看ThreadLocal类中的get()方法:

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */ 
public T get() { 
    Thread t = Thread.currentThread(); 
    ThreadLocalMap map = getMap(t); 
    if (map != null) { 
        ThreadLocalMap.Entry e = map.getEntry(this); 
        if (e != null) 
            return (T)e.value; 
    } 
    return setInitialValue(); 
} 

再来看setInitialValue()方法

/**
    * Variant of set() to establish initialValue. Used instead
    * of set() in case user has overridden the set() method.
    *
    * @return the initial value
    */ 
   private T setInitialValue() { 
       T value = initialValue(); 
       Thread t = Thread.currentThread(); 
       ThreadLocalMap map = getMap(t); 
       if (map != null) 
           map.set(this, value); 
       else 
           createMap(t, value); 
       return value; 
   } 

 获取和当前线程绑定的值时,ThreadLocalMap对象是以this指向的ThreadLocal对象为键进行查找的,这当然和前面set()方法的代码是相呼应的。

由于不同的ThreadLocal对象实例作为不同key键,因此我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,就像你在一个HashMap对象中存储一个键值对和多个键值对一样,仅此而已。

5、Thread,ThreadLocal,ThreadLocalMap,Entry的UML关系图

1)、每个线程是一个Thread实例,其内部维护一个threadLocals的实例成员,其类型是ThreadLocal.ThreadLocalMap。它就是为每一个线程来存储自身的ThreadLocal变量的

2)、通过实例化ThreadLocal实例,我们可以对当前运行的线程设置一些线程私有的变量,通过调用ThreadLocal的set和get方法存取。ThreadLocal本身并不是一个容器,我们存取的value实际上存储在ThreadLocalMap中,ThreadLocal只是作为TheadLocalMap的key。

3)、每个线程实例都对应一个TheadLocalMap实例,我们可以在同一个线程里实例化很多个ThreadLocal来存储很多种类型的值,这些ThreadLocal实例分别作为key,对应各自的value,最终存储在Entry table数组中。

4)、当调用ThreadLocal的set/get进行赋值/取值操作时,首先获取当前线程的ThreadLocalMap实例,然后就像操作一个普通的map一样,进行put和get。

三. 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:非线程安全

package com.test; 
   
import java.sql.Connection; 
import java.sql.SQLException; 
import java.sql.Statement; 
   
public class TestDao { 
    private Connection conn;// ①一个非线程安全的变量 
   
    public void addTopic() throws SQLException { 
        Statement stat = conn.createStatement();// ②引用非线程安全变量 
        // … 
    } 
} 

由于处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

代码清单4 TestDao:线程安全

ackage com.test; 
   
import java.sql.Connection; 
import java.sql.SQLException; 
import java.sql.Statement; 
   
public class TestDaoNew { 
    // ①使用ThreadLocal保存Connection变量 
    private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>(); 
   
    public static Connection getConnection() { 
        // ②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection, 
        // 并将其保存到线程本地变量中。 
        if (connThreadLocal.get() == null) { 
            Connection conn = getConnection(); 
            connThreadLocal.set(conn); 
            return conn; 
        } else { 
            return connThreadLocal.get();// ③直接返回线程本地变量 
        } 
    } 
   
    public void addTopic() throws SQLException { 
        // ④从ThreadLocal中获取线程对应的Connection 
        Statement stat = getConnection().createStatement(); 
    } 
}

不同的线程在使用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内存泄漏问题


1、ThreadLocal内存模型

我们先看看ThreadLocal内存模型:

1)线程的一些局部变量和引用使用的内存属于Stack(栈)区,而普通的对象是存储在Heap(堆)区。

2)线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef。

3)当ThreadLocal的set/get被调用时,JVM会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化。

Map实例化之后,也就拿到了该ThreadLocalMap的句柄,那么就可以将当前ThreadLocal对象作为key,进行存取操作。

2、ThreadLocal实例的引用是个弱引用

上图中的虚线,表示key对应ThreadLocal实例的引用是个弱引用。ThreadLocalMap的key是一个弱引用类型,源代码如下:

   

super(k)通过父类WeakReference类来实现弱引用

强引用:一直活着:类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。

弱引用:回收就会死亡:被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。

软引用:有一次活的机会:软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。

虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。

       每个Thread实例都具备一个ThreadLocal的map,以ThreadLocal实例为key,以绑定的Object为Value。而这个map不是普通的map,它是在ThreadLocal中定义的,它和普通map的最大区别就是它的Entry是针对ThreadLocal弱引用的,即当外部ThreadLocal引用为空时,map就可以把ThreadLocal交给GC回收,从而得到一个null的key。


       这个threadlocal内部的map在Thread实例内部维护了ThreadLocal 实例和bind value之间的关系,这个map的大小是有threshold(阀值),当超过threshold时,map会首先检查内部的ThreadLocal(前文说过,map是弱引用可以释放)是否为null,如果存在null,那么释放引用给gc,这样保留了位置给新的线程。如果不存在slate threadlocal(预定的threadlocal),那么resize threadLocalMap 即double threshold(Double the capacity of the table 扩容threadLocalMap的容量为两倍,具体可以看源码),

 看看ThreadLocalMap就知道,sz >= threshold

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            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占用的内存:
1)一是当hash算法得到的table index刚好是一个null key的threadlocal时,直接用新的threadlocal替换掉已经废弃的。
2)另外每次在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中命中)时。

key 使用强引用:引用ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key 使用弱引:引用ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set、get、remove的时候会被清除。

比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal被清理后key为null,对应的value在下一次ThreadLocalMap调用set、get、remove的时候可能会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

六. ThreadLocal与线程池


     ThreadLocal与线程对象紧密绑定的, 一般web容器(如tomcat)使用了线程池,线程池中的线程是可能存在复用的。   

      如果使用了线程池(如web容器,Executor),那么即使即使父线程已经结束,子线程依然存在并被池化。这样,线程池中的线程在下一次请求被执行的时候,ThreadLocal对象的get()方法返回的将不是当前线程中设定的变量,因为池中的“子线程”根本不是当前线程创建的,当前线程设定的ThreadLocal变量也就无法传递给线程池中的线程。

import java.util.concurrent.Executor;  
importjava.util.concurrent.Executors;  
   
public classThreadLocalTest {  
    private static ThreadLocal<String> vLocal = new ThreadLocal<String>();  
    public static void main(String[] args) {  
        Executorexecutor = Executors.newFixedThreadPool(2);  
        // 模拟10个请求  
        for (int i =0; i < 10; i++) {  
           final int flag= i;  
           executor.execute(new Runnable() {  
                @Override  
               public voidrun() {  
//                   vLocal.set(null);  
                 //模拟某一线程改变了ThreadLocal的值  
                   if (flag == 1) {  
                       vLocal.set("set:test");  
                   }  
                   System.out.println(Thread.currentThread().getName()+ ":" + vLocal.get());  
               }  
           });  
       }  
    }  
}  

ThreadLocal的在线程池环境下要注意:

    1)并非每次web请求时候程序运行的ThreadLocal都是唯一的。

    2) ThreadLocal的生命周期不等于一次Request的生命周期.

    3)ThreadLocal可以用于存放与请求无关对象,不能用来传递参数

    4) ThreadLocal数据是在线程创建时绑定在线程上的, 所以解决方法是在使用数据之前调用remove() 移除掉之前的其他线程产生的数据

解决方法:重构remove方法 ,先remove, 然后再初始化一次, 这样就可以保证数据是干净的了.

@Override 
public void remove() { 
    super.remove(); 
     initialValue(); 
} 

当然你也可以在调用的finally里面使用remove也是可以。或者使用AutoCloseable方法自动清除:

class UserContext implements AutoCloseable{
    static final ThreadLocal<User> context = new ThreadLocal<>(); //全局唯一静态变量
    public static User getCurrentUser(){    //获取当前线程的ThreadLocal User
        return context.get();
    }
    public UserContext(User user){ //初始化ThreadLocal的User
        context.set(user);
    }
    @Override
    public void close(){ //移除ThreadLocal关联的User
        context.remove();
    }
}

业务逻辑:

try(UserContext ctx = new UserContext(user)){ 
   //业务逻辑
} 

可以在全局过滤器来实现,这样每次请求结束都会自动remove threadlocal:

public class GlobalFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        try (ContextLocal ignored = new ContextLocal()) {
            filterChain.doFilter(request, response);
        }
    }
}

七. 父子线程传递InheritableThreadLocal


ThreadLocal可以为当前线程保存局部变量,但ThreadLocal不支持继承性,如果子线程想要拿到父线程的中的ThreadLocal值怎么办呢?

public class InheritableThreadLocalTest {

    private static ThreadLocal<String> tl = new ThreadLocal<>();

    public static void main(String[] args) {
        tl.set("main thread ThreadLocal value.");
        // 开启一个子线程
        new Thread(() -> {
            System.out.println("从父线程获取的值:" + tl.get());
        }).start();
    }

}

这个输出结果:从父线程获取的值:null

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中的值。

看看在线程new Thread的时候线程都做了些什么,Thread的init相关逻辑如下:

1)、Thread内部为InheritableThreadLocal开辟了一个单独的ThreadLocalMap。在父线程创建一个子线程的时候,会检查这个ThreadLocalMap是否为空,不为空则会浅拷贝给子线程的ThreadLocalMap

private void init(ThreadGroup g, Runnable target, String name,
                     long stackSize, AccessControlContext acc) {
       //省略上面部分代码
       if (parent.inheritableThreadLocals != null)
       //这句话的意思大致不就是,copy父线程parent的map,创建一个新的map赋值给当前线程的inheritableThreadLocals。
           this.inheritableThreadLocals =
               ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
      //ignore
   }

2、在copy过程中是浅拷贝,key和value都是原来的引用地址。赋值拷贝代码如下:

 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) {
                        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++;
                    }
                }
            }
        }

应用例子

package com.javademo.demo.threadlocal;

public class InheritableThreadlocalTest {
    private static final InheritableThreadLocal<String> itl = new InheritableThreadLocal<String>();

    public static void main(String[] args) {
        itl.set("main thread InheritableThreadLocal value.");
        new Thread(() -> {
            System.out.println("从父线程获取的值:" + itl.get());
        }).start();
    }
}

下面输出结果是:

需要注意的是,拷贝为浅拷贝。父子线程的 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的一个库解决了这个问题GitHub - alibaba/transmittable-thread-local: 📌 TransmittableThreadLocal (TTL), the missing Java™ std lib(simple & 0-dependency) for framework/middleware, provide an enhanced InheritableThreadLocal that transmits values between threads even using thread pooling components.

Transmittable ThreadLocal是阿里开源的库,继承了InheritableThreadLocal,优化了在使用线程池等会池化复用线程的情况下传递ThreadLocal的使用。


如何使用

这个库最简单的方式是这样使用的,通过简单的修饰,使得提交的runable拥有了上一节所述的功能。具体的API文档详见github,这里不再赘述

TransmittableThreadLocal<String> parent = new TransmittableThreadLocal<String>();
parent.set("value-set-in-parent");

Runnable task = new Task("1");
// 额外的处理,生成修饰了的对象ttlRunnable
Runnable ttlRunnable = TtlRunnable.get(task);
executorService.submit(ttlRunnable);

// Task中可以读取, 值是"value-set-in-parent"
String value = parent.get();


原理简述

TransmittableThreadLocal 原理是在任务提交给线程池时,将ThreadLocal数据一起提交,相当于重新set一次ThreadLocal。

简单来说,有个专门的TtlRunnable和TtlCallable包装类,用于读取原Thread的ThreadLocal对象及值并存于Runnable/Callable中,在执行run或者call方法的时候再将存于Runnable/Callable中的ThreadLocal对象和值读取出来,存入调用run或者call的线程中。

public final class TtlRunnable implements Runnable, TtlEnhanced, TtlAttachments {
    private final AtomicReference<Object> capturedRef;
    private final Runnable runnable;
    private final boolean releaseTtlValueReferenceAfterRun;

    private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        //从父类capture复制到本类
        this.capturedRef = new AtomicReference<Object>(capture());
        this.runnable = runnable;//提交的runnable对象
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }
    /**
     * wrap method {@link Runnable#run()}.
     */
    @Override
    public void run() {
        //取出保存在captured中的父线程ThreadLocal值
        Object captured = capturedRef.get();
        //重新set
        if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }

        Object backup = replay(captured);
        try {
            runnable.run();
        } finally {
            restore(backup);
        }
    }

capture函数的复制过程如下:

@Nonnull
public static Object capture() {
    Map<TransmittableThreadLocal<?>, Object> captured = new HashMap<TransmittableThreadLocal<?>, Object>();
    for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
        captured.put(threadLocal, threadLocal.copyValue());
    }
    return captured;
}		

其中holder记录了当前 Thread 绑定了哪些 TransmittableThreadLocal 对象。captured保存了父线程ThreadLocal的值。

接着任务提交到线程池,线程开始run() 运行时:

1)、取出保存在captured中的父线程ThreadLocal值并重新set。即将父线程值传递到了任务执行时。

2)、执行后再恢复 backup 的数据到 holder中,将 backup 中的 TransmittableThreadLocal set到当前线程中。

如何更新父线程ThreadLocal值?
如果线程之间出了要能够得到父线程中的值,同时想更新值怎么办呢?在前面我们有提到,当子线程copy父线程的ThreadLocalMap的时候是浅拷贝的,代表子线程Entry里面的value都是指向的同一个引用,我们只要修改这个引用的同时就能够修改父线程当中的值了,比如这样:

@Override
public void run() {
    System.out.println("========");
         Span span=  inheritableThreadLocal.get();
         System.out.println(span);
         span.name="child123";//修改父引用为child123
         inheritableThreadLocal.set(new Span("word"));
         System.out.println(inheritableThreadLocal.get());
}

这样父线程中的值就会得到更新了。能够满足父线程ThreadLocal值的实时更新,同时子线程也能共享父线程的值。不过场景倒是不是很常见的样子。

应用场景和意义

TTL的运用主要是根据其能提供跨线程、跨线程池依靠ThreadLocal来传递消息的特性决定的。

和其他传递消息的方式来比较,主要的优势在于TTL的代码透明性更好,可以做到对代码侵入性尽可能小

1、分布式调用跟踪系统

业界有两个比较有代表性的分布式调用跟踪系统,Google的Drapper和淘宝的鹰眼。

APM现在传递节点信息,如TranceId与SpanId,是直接通过http等协议进行消息包装进行信息交互的,利用TTL我们可以在比如A机器调用跟踪B机器这样的场景下,通过利用ThreadLocal来进行消息数据的传递,这样的好处是可以将消息数据和调用线程绑定在一起,不会过多地污染业务代码,也是TTL目前主要且能够发力的地方。

2、应用容器或上层框架跨应用代码给下层SDK传递信息

 这种传递消息的方式,主要优势在于可以让容器层和应用层、基础服务工具层之间解耦合,并且传递的过程也是足够透明的,不会过多地侵入用户代码,其实如果能开发人员能够把控到部署平台,给JVM启动设置参数,那么利用Agent植入TTL的方法,对于代码非侵入性是最好的。

3、日志收集记录系统上下文

通过TTL跨日志线程间的消息传递,达成日志上下文的串联,更好地对日志数据进行分析和操作。

修饰线程池:

package com.turing.log2.utils;
/**
 * Created by huangguisu on 2019/10/28.
 */
import com.alibaba.ttl.threadpool.TtlExecutors;
import com.turing.log2.context.Log2Context;
import com.turing.log2.utils.Log2Utils;
import com.turing.log2.context.Log2ThreadLocalSession;
import java.util.concurrent.ExecutorService;


public class Log2TtlExecutors{

    public static ExecutorService  getTtlExecutorService(ExecutorService executorService) {
        // 额外的处理,生成修饰了的对象executorService
        executorService = TtlExecutors.getTtlExecutorService(executorService);
        Log2Utils.getContextGlobalId();
        Log2ThreadLocalSession.threadLocalContext.set(Log2Context.getInstance());
        return executorService;
    }

}


public class Log2ThreadLocalSession {
	public static final ThreadLocal<Log2Context> logSession = new InheritableThreadLocal<Log2Context>();
	public static TransmittableThreadLocal<Log2Context> threadLocalContext = new TransmittableThreadLocal <Log2Context>() ;
}


  • 3
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

hguisu

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值