首页 > 系统相关 >多线程篇(ThreadLocal & 内存模型 & 伪共享(ThreadLocal ))(持续更新迭代)

多线程篇(ThreadLocal & 内存模型 & 伪共享(ThreadLocal ))(持续更新迭代)

时间:2024-09-02 09:23:56浏览次数:13  
标签:变量 迭代 get Thread ThreadLocal 线程 多线程 public

目录

一、ThreadLocal

1. 前言

2. 简介

3. 与Synchronized的区别

4. 简单使用

5. 原理

5.1. set

5.2. get

5.3. remove

5.4. 与Thread,ThreadLocalMap之间的关系

5. 常见使用场景

场景一:存储用户Session

场景二、数据库连接,处理数据库事务

场景三、数据跨层传递(controller,service, dao)

场景四、Spring使用ThreadLocal解决线程安全问题

6. 内存泄漏问题

6.1. 是否会有内存泄漏?

6.2. 怎么解决这个内存泄漏问题

二、InheritableThreadLocal

1. 可以做什么

2. 使用实例

3. 原理

4. 和线程池搭配使用的问题

三、TransmittableThreadLocal

1. 前言

2. 简介

3. 使用

4. 源码

4.1. 重要属性 holder

4.2. 重要方法

4.3. 线程池相关重要方法

5. 应用场景

6. ThreadLocal,InheritableThreaLocal与TransmittableThreadLocal的比较


一、ThreadLocal

1. 前言

多线程访问同一个共享变量时特别容易出现并发问题,特别是在多个线程需要对一个共享变量进行写入时。

为了保证线程安全,一般使用者在访问共享变量时需要进行适当的同步,如图。

同步的措施一般是加锁,这就需要使用者对锁有一定的了解,这显然加重了使用者的负担。

那么有没有一种方式可以做到,当创建一个变量后,每个线程对其进行访问的时候,访问的是自己线程的变量呢?

其实 ThreadLocal 就可以做这件事情,虽然 ThreadLocal 并不是为了解决这个问题而出现的。

ThreadLocal 是 JDK 包提供的,它提供了线程本地变量,也就是如果你创建了一个 ThreadLocal 变量,

那么访问这个变量的每个线程都会有这个变量的一个本地副本。

当多个线程操作这个变量时,实际操作的是自己本地内存里面的变量,从而避免了线程安全问题。

创建一个 ThreadLocal 变量后,每个线程都会复制一个变量到自己的本地内存,如图所示。

2. 简介

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离

的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程

可以访问自己内部的副本变量。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这

里有几点需要注意:

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这也是 ThreadLocal 命名的由来。
  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独

立的实例副本。

ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本

都可被回收。

总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线

程间隔离而在方法或类间共享的场景

下图可以增强理解:

3. 与Synchronized的区别

CCAThreadLocal其实是与线程绑定的一个变量。

ThreadLocal和Synchonized都用于解决多线程并发访问。

但是ThreadLocal与synchronized有本质的区别:

  1. Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
  2. Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个

线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数

据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。

一句话理解ThreadLocal,threadlocl是作为当前线程中属性ThreadLocalMap集合中的某一个Entry的key值

Entry(threadlocl,value),虽然不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的

ThreadLocalMap是独一无二的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,

从而到达了线程间变量隔离的目的,但是在同一个线程中这个 value 变量地址是一样的。

4. 简单使用

直接上代码:

public class ThreadLocaDemo {
 
    private static ThreadLocal<String> localVar = new ThreadLocal<String>();
 
    static void print(String str) {
        //打印当前线程中本地内存中本地变量的值
        System.out.println(str + " :" + localVar.get());
        //清除本地内存中的本地变量
        localVar.remove();
    }
    public static void main(String[] args) throws InterruptedException {
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaDemo.localVar.set("local_A");
                print("A");
                //打印本地变量
                System.out.println("after remove : " + localVar.get());
               
            }
        },"A").start();
 
        Thread.sleep(1000);
 
        new Thread(new Runnable() {
            public void run() {
                ThreadLocaDemo.localVar.set("local_B");
                print("B");
                System.out.println("after remove : " + localVar.get());
              
            }
        },"B").start();
    }
}
A :local_A
after remove : null
B :local_B
after remove : null

从这个示例中我们可以看到,两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。

这个的理解也可以结合图1-1,相信会有一个更深刻的理解。

5. 原理

要看原理那么就得从源码看起。

5.1. set

 public void set(T value) {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
        //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 初始化thradLocalMap 并赋值
            createMap(t, value);
    }
}

从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread,并获取thread线程中的

ThreadLocalMap属性。

如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。

那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。

大家最后自己再idea上跟下源码,会有更深的认识。

  static class ThreadLocalMap {
 
        /
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 
        
    }

可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的

弱引用。

在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。详细内容要大家自己去跟。

//这个是threadlocal 的内部方法
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
 
 
    //ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

5.2. get

    public T get() {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //3、如果map数据不为空,
        if (map != null) {
            //3.1、获取threalLocalMap中存储的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
        return setInitialValue();
    }
 
 
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;
    }

5.3. remove

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
 }

remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。

为什么要删除,这涉及到内存泄露的问题。

实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,

如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来

ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一

来就会出现 key 为 null 的 value。

ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:

如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。

通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意

味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。

举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象

后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

5.4. 与Thread,ThreadLocalMap之间的关系

图4-1 Thread、THreadLocal、ThreadLocalMap之间啊的数据关系图

从这个图中我们可以非常直观的看出,ThreadLocalMap其实是Thread线程的一个属性值,而ThreadLocal是维

护ThreadLocalMap这个属性指的一个工具类。

Thread线程可以拥有多个ThreadLocal维护的自己线程独享的共享变量

(这个共享变量只是针对自己线程里面共享)

5. 常见使用场景

如上文所述,ThreadLocal 适用于如下两种场景

  • 1、每个线程需要有自己单独的实例
  • 2、实例需要在多个方法中共享,但不希望被多线程共享

对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。

ThreadLocal 可以以非常方便的形式满足该需求。

对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。

ThreadLocal 使得代码耦合度更低,且实现更优雅。

场景一:存储用户Session

一个简单的用ThreadLocal来存储Session的例子:

private static final ThreadLocal threadSession = new ThreadLocal();
 
    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }
}

场景二、数据库连接,处理数据库事务

场景三、数据跨层传递(controller,service, dao)

每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使

用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些

信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将

这个对象(如 user 对象)作为参数传递的麻烦。

比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就

会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。这个例

子和存储session有些像。

package com.kong.threadlocal;
 
 
public class ThreadLocalDemo05 {
    public static void main(String[] args) {
        User user = new User("jack");
        new Service1().service1(user);
    }
 
}
 
class Service1 {
    public void service1(User user){
        //给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
        UserContextHolder.holder.set(user);
        new Service2().service2();
    }
}
 
class Service2 {
    public void service2(){
        User user = UserContextHolder.holder.get();
        System.out.println("service2拿到的用户:"+user.name);
        new Service3().service3();
    }
}
 
class Service3 {
    public void service3(){
        User user = UserContextHolder.holder.get();
        System.out.println("service3拿到的用户:"+user.name);
        //在整个流程执行完毕后,一定要执行remove
        UserContextHolder.holder.remove();
    }
}
 
class UserContextHolder {
    //创建ThreadLocal保存User对象
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}
 
class User {
    String name;
    public User(String name){
        this.name = name;
    }
}

执行的结果:

service2拿到的用户:jack
service3拿到的用户:jack

场景四、Spring使用ThreadLocal解决线程安全问题

我们知道在一般情况下,只有无状态的 Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声

明为 singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、

TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用

ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式

在多线程中正常工作了。

一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层

开放功能调用。

在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9-2所示。

这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,

在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。

下面的实例能够体现Spring对有状态Bean的改造思路:

代码清单9-5 TopicDao:非线程安全

 
public class TopicDao {
   //①一个非线程安全的变量
   private Connection conn; 
   public void addTopic(){
        //②引用非线程安全变量
       Statement stat = conn.createStatement();
       …
   }
}

由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例

(非singleton)。

下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造:

代码清单9-6 TopicDao:线程安全

 
import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {
 
  //①使用ThreadLocal保存Connection变量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection(){
         
        //②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
        //并将其保存到线程本地变量中。
if (connThreadLocal.get() == null) {
            Connection conn = ConnectionManager.getConnection();
            connThreadLocal.set(conn);
              return conn;
        }else{
              //③直接返回线程本地变量
            return connThreadLocal.get();
        }
    }
    public void addTopic() {
 
        //④从ThreadLocal中获取线程对应的
         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。但这个实例基本上说明了

Spring对有状态类线程安全化的解决思路。

在本章后面的内容中,我们将详细说明Spring如何通过ThreadLocal解决事务管理的问题。

6. 内存泄漏问题

6.1. 是否会有内存泄漏?

ThreadLocal本身并不会导致内存泄漏,但不正确地使用ThreadLocal可能会导致内存泄漏。

在Java中,ThreadLocal提供了一种在多线程环境下保持变量的线程隔离的机制。

每个线程都会持有一个ThreadLocal实例的副本,线程之间的变量互不干扰。

如果使用不当,ThreadLocal可能会导致内存泄漏。

一种常见的情况是在使用完ThreadLocal后没有进行及时的清理操作。

例如,在Web应用程序中,如果将一个ThreadLocal实例放入了一个长寿命周期的容器中(如

ServletContext),但在容器关闭时没有清理ThreadLocal实例,就会导致内存泄漏。

另外,如果ThreadLocal中持有的对象是一个较大的对象,并且在使用完后没有手动释放,也可能导致内存泄漏。

因为ThreadLocal中的对象只有在线程结束后才会被垃圾回收器回收,

如果ThreadLocal没有被正确清理,对象就无法被释放,从而导致内存泄漏。

为了避免内存泄漏,需要注意以下几点:

  1. 在使用完ThreadLocal后,及时调用其remove()方法,清理ThreadLocal中的对象。可以使用finally块来确保清理操作的执行。
  2. 避免将ThreadLocal实例放入长生命周期的容器中,尽量将其放在短生命周期的地方,如方法内部。
  3. 注意ThreadLocal持有的对象的生命周期,避免持有过大的对象或过长时间的对象。

6.2. 怎么解决这个内存泄漏问题

  1. 及时清理:在使用完ThreadLocal后,确保调用其remove()方法,将ThreadLocal中的对象清理掉。可以使用finally块来确保清理操作的执行,以免遗漏。
  2. 使用弱引用:可以使用java.lang.ref.WeakReference来引用ThreadLocal对象。这样,当ThreadLocal对象没有强引用时,它就可以被垃圾回收器回收,从而避免内存泄漏。需要注意的是,在使用弱引用时,获取ThreadLocal中的值时需要判断弱引用是否已被回收。
  3. 注意对象生命周期:确保ThreadLocal中持有的对象的生命周期合理。避免持有过大的对象或过长时间的对象。可以在不需要使用ThreadLocal时,主动将对象设置为null,从而帮助垃圾回收器更早地回收对象。
  4. 合理管理ThreadLocal的生命周期:尽量将ThreadLocal的作用范围控制在需要的线程范围内,避免将其放入长生命周期的容器中。当不再需要使用ThreadLocal时,及时清理。
  5. 定期检查和清理:可以定期检查ThreadLocal使用情况,查看是否有未清理的ThreadLocal实例。可以通过一个定时任务或者监控工具来进行检查,并在需要的时候手动清理。

二、InheritableThreadLocal

本文内容:

  1. InheritableThreadLocal可以做什么
  2. InheritableThreadLocal使用实例
  3. InheritableThreadLocal原理
  4. InheritableThreadLocal和线程池搭配使用的问题。

1. 可以做什么

我们知道 ThreadLocal 解决的是让每个线程读取的 ThreadLocal 变量是相互独立的。

通俗的讲就是,比如我在线程1中set了ThreadLocal的值,那我在线程2中是get不到线程1设置的值的,只能读到

线程2自己set的值。

ThreadLocal有一个需求不能满足:就是子线程无法直接复用父线程的ThreadLocal变量里的内容。

demo如下:

package com.mt;
 
public class TestThreadLocal implements Runnable {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
 
    public static void main(String[] args) {
        System.out.println("----主线程设置值为\"主线程\"");
        threadLocal.set("主线程");
        System.out.println("----主线程设置后获取值:" + threadLocal.get());
        Thread tt = new Thread(new TestThreadLocal());
        tt.start();
        System.out.println("----主线程结束");
 
    }
 
    @Override
    public void run() {
        System.out.println("----子线程设置值前获取:" + threadLocal.get());
        System.out.println("----新开线程设置值为\"子线程\"");
        threadLocal.set("子线程");
        System.out.println("----新开的线程设置值后获取:" + threadLocal.get());
    }
}

运行结果:

可以看到虽然在main线程中启动了一个新的子线程,但是threadlocal变量的内容并没有传递到新的子线程中。

于是乎,InheritableThreadLocal就出现了。他可以实现在子线程中使用父线程中的线程本地变量

(也即InheritableThreadLocal变量)。

2. 使用实例

demo,根据上面的 threadlocal 测试代码稍作修改,把 Threadlocal 换做 InheritableThreadLocal。

package com.mt;
 
public class TestInheritableThreadLocal implements Runnable {
    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
 
    public static void main(String[] args) {
        System.out.println("----主线程设置值为\"主线程\"");
        threadLocal.set("主线程");
        System.out.println("----主线程设置后获取值:" + threadLocal.get());
        Thread tt = new Thread(new TestInheritableThreadLocal());
        tt.start();
        System.out.println("----主线程结束");
 
    }
 
    @Override
    public void run() {
        System.out.println("----子线程设置值前获取:" + threadLocal.get());
        System.out.println("----新开线程设置值为\"子线程\"");
        threadLocal.set("子线程");
        System.out.println("----新开的线程设置值后获取:" + threadLocal.get());
    }
}

运行结果如下:

在子线程设置值之前,就已经能够get到主线程设置的值了,

说明在父子进制之间传递了InheritableThreadLocal变量。

3. 原理

通过观察InheritableThreadLocal代码Structure,看到只是重写了ThreadLocal的三个方法。

childValue,createMap,getMap。

我们进入到createMap方法中查看。

可以看到,InheritableThreadLocal其实也是用ThreadLocalMap去存放值,这点和ThreadLocal一样,只不过InheritableThreadLocal的

变量在Thread类里的名字叫inheritableThreadLocals。我们进到Thread类中看这个变量。

当我们在主线程start一个子线程时,会new 一个Thread。

所以我们要追到Thread类中,看看创建线程时发生了什么才让父子线程的InheritableThreadLocal可以传递。

首先我们调用的是Thread(Runnable target)这个方法。

这个方法会调用init方法,然后经过一系列init函数重载,最终来到下面这个init方法。

在这个init方法里 ,跟InheritableThreadLocal紧密相关的有下面这些代码:

重点就是if里面的逻辑。

if (inheritThreadLocals && parent.inheritableThreadLocals != null)

第一项inheritThreadLocals 是传进来的boolean值,重载时传的是true,所以满足条件。

第二项就是判断父线程中的inheritableThreadLocals 是不是空,如果不是空就满足条件。

当同时满足if的两个条件后,就执行

this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

新创建出来的子线程的inheritableThreadLocals 变量就和父线程的inheritableThreadLocals 的内容一样了。

以上就是从源码的角度分析InheritableThreadLocal的原理。

4. 和线程池搭配使用的问题

首先给出结论:

InheritableThreadLocal和线程池搭配使用时,可能得不到想要的结果,因为线程池中的线程是复用的,并没有

重新初始化线程,

InheritableThreadLocal之所以起作用是因为在Thread类中最终会调用init()方法去把InheritableThreadLocal的

map复制到子线程中。

由于线程池复用了已有线程,所以没有调用init()方法这个过程,也就不能将父线程中的InheritableThreadLocal

值传给子线程。

下面是DEMO:

package com.mt;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
public class TestInheritableThreadLocalAndExecutor implements Runnable {
    private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
    private static ExecutorService executorService = Executors.newFixedThreadPool(1);
 
    public static void main(String[] args) throws Exception{
        System.out.println("----主线程启动");
        inheritableThreadLocal.set("主线程第一次赋值");
        System.out.println("----主线程设置后获取值:" + inheritableThreadLocal.get());
        executorService.submit(new TestInheritableThreadLocalAndExecutor());
        System.out.println("主线程休眠2秒");
        Thread.sleep(2000);
        inheritableThreadLocal.set("主线程第二次赋值");
        executorService.submit(new TestInheritableThreadLocalAndExecutor());
        executorService.shutdown();
    }
 
    @Override
    public void run() {
        System.out.println("----子线程获取值:" + inheritableThreadLocal.get());
    }
}

运行结果:

三、TransmittableThreadLocal

1. 前言

前面我看过了 ThreadLocal的实现机制和原理 以及 InheritableThreadLocal的实现机制和原理 两种类型的

ThreadLocal,前者是普通的,后者是在前者的基础上套了一层父子线程关系,当使用后者的时候,会在线程创建的时

候,浅拷贝一份父线程的变量值。那么今天空了,我来看看另外一种 ThreadLocal:TransmittableThreadLocal。

2. 简介

TransmittableThreadLocal(TTL)是阿里巴巴开源的一个 Java 库,用于解决 ThreadLocal 在多线程环境下的一些问

题,尤其是在使用线程池等场景下可能出现的问题。

与普通的 ThreadLocal 不同,TTL 具有以下特点:

(1)线程池透传性:在使用线程池执行任务时,TTL 可以透传 ThreadLocal 的值,确保后续线程能够正确访问前线程设

置的 TransmittableThreadLocal 变量值。

(2)线程池隔离性:TTL 在多线程环境下能够确保每个线程都有独立的 TransmittableThreadLocal值,避免了线程池重

用线程时可能出现的数据污染问题,比如线程池执行前从父线程继承的变量,不管是执行中变没变,下次执行任务的时

候,还是会和父线程保持一致。

(3)资源自动清理:TTL 支持自动清理 TransmittableThreadLocal 值,避免了可能导致内存泄漏的问

题。

(4)兼容性:TTL 兼容原生 ThreadLocal 的语法和用法,可以直接替换原生 ThreadLocal 使用,而无需修改现有代码。

3. 使用

TTL 通常用于需要在线程池中执行任务,并且需要在任务之间传递 ThreadLocal 值的场景。

例如,在 Web 应用中,可能需要在异步任务中访问当前用户的会话信息,而使用 TTL 可以确保子线程能够正确访问父线

程设置的会话信息。

TransmittableThreadLocal在使用线程池等会池化复用线程的执行组件情况下,提供ThreadLocal值的传递功能(把任务

提交给线程池时的ThreadLocal值传递到任务执行时),解决异步执行时上下文传递的问题。

我们来简单体验一下:

// TTL
private static final TransmittableThreadLocal<Integer> tl = new TransmittableThreadLocal<>();
public static void main(String[] args) {
    // 父线程设置变量 1
    tl.set(1);
    new Thread(() -> {
        System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
    }).start();
    new Thread(() -> {
        System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
    }).start();
}

这么一看貌似跟我们的 ITL 没什么区别是吧。我们来看一个线程池的例子:

public class Demo {

    private static final TransmittableThreadLocal<Integer> tl = new TransmittableThreadLocal<>();
    
    private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            2,
            10,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100)
    );

    public static void main(String[] args) {
        // 父线程设置变量 1
        tl.set(1);        poolExecutor.execute(() -> {
            // 更改当前线程中的值
            tl.set(2);
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        poolExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        poolExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        poolExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });

    }
}

我们看普通情况下,当某个线程改变了 TTL 的值后,下次该线程执行任务的时候,TTL 的值就是改变后的了。

这里需要引入一下 TTL 里的线程池,我们再看:

public class Demo {

    private static final TransmittableThreadLocal<Integer> tl = new TransmittableThreadLocal<>();

    private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
            2,
            2,
            10,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100)
    );
    private static final Executor ttlExecutor = TtlExecutors.getTtlExecutor(poolExecutor);

    public static void main(String[] args) {
        // 父线程设置变量 1
        tl.set(1);

        ttlExecutor.execute(() -> {
            // 更改当前线程中的值
            tl.set(2);
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        ttlExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        ttlExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });
        ttlExecutor.execute(() -> {
            System.out.println(String.format("子线程:%s,获取值=%s", Thread.currentThread().getName(), tl.get()));
        });

    }
}

看是不是,1号线程改变了 TTL 的值,下次执行的时候还是和父线程的值一致的。下面我们就来解析一下其内部原理,

看看TTL是怎么完成对ITL的优化的。

4. 源码

TTL 本质上是一个InheritableThreadLocal,意味着TTL具备ITL的功能:

// TTL继承关系
public class TransmittableThreadLocal<T> extends InheritableThreadLocal<T> implements TtlCopier<T> {

具体值是保存在线程内部的inheritableThreadLocals,不涉及线程池时可以当成 InheritableThreadLocal使用。

4.1. 重要属性 holder

TransmittableThreadLocal重要核心属性holder:

// holder 本质上是一个 InheritableThreadLocal
// 内部的值的类型是 WeakHashMap<TransmittableThreadLocal<Object>, ?>
// 这个 WeakHashMap 被当成一个 Set 使用,这个WeakHashMap的value永远是null, 不会被使用到,key 存的就是 TTL的对象
private static InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>> holder =
        new InheritableThreadLocal<WeakHashMap<TransmittableThreadLocal<Object>, ?>>() {
            @Override
            protected WeakHashMap<TransmittableThreadLocal<Object>, ?> initialValue() {
                return new WeakHashMap<TransmittableThreadLocal<Object>, Object>();
            }
            @Override
            protected WeakHashMap<TransmittableThreadLocal<Object>, ?> childValue(WeakHashMap<TransmittableThreadLocal<Object>, ?> parentValue) {
                return new WeakHashMap<TransmittableThreadLocal<Object>, Object>(parentValue);
            }
        };

holder的作用:存放的是所有和当前线程产生关联的 TransmittableThreadLocal,通过holder可以找到Java进程中所有

的TransmittableThreadLocal(即使用WeakHashMap收集线程中所有的 TransmittableThreadLocal)。

ThreadLocal.ThreadLocalMap和WeakHashMap的区别?

相同点:ThreadLocalMap.Entry和WeakHashMap都继承弱引用,弱引用的reference都指向key :

ThreadLocalMap.Entry:

不同点:ThreadLocalMap是一个用移位解决hash冲突的简易Map,而WeakHashMap是一个用链表解决hash冲突的

简易Map;

4.2. 重要方法

下面的方法均属于TTL类:

@Override
public final T get() {
    T value = super.get();
    if (disableIgnoreNullValueSemantics || null != value) addThisToHolder();
    return value;
}
@Override
public final void set(T value) {
    if (!disableIgnoreNullValueSemantics && null == value) {
        // may set null to remove value
        remove();
    } else {
        super.set(value);
        addThisToHolder();
    }
}
@Override
public final void remove() {
    // 从holder持有的map对象中移除该 ttl
    removeThisFromHolder();
    super.remove();
}
private void removeThisFromHolder() {
    holder.get().remove(this);
}
private void addThisToHolder() {
    if (!holder.get().containsKey(this)) {
        // 从 holder 中当前线程持有的map 中添加 ttl
        holder.get().put((TransmittableThreadLocal<Object>) this, null); // WeakHashMap supports null value.
    }
}

TTL里先了解上述的几个方法及对象,可以看出,单纯的使用TTL是达不到支持线程池本地变量的传递的,通过上边的例子

除了要启用TTL,还需要通过TtlExecutors.getTtlExecutorService包装一下线程池才可以。

4.3. 线程池相关重要方法

下面就来看看在程序即将通过线程池异步的时候,TTL帮我们做了哪些操作

(这一部分是TTL支持线程池传递的核心部分):

首先打开包装类,看下execute方法在执行时做了些什么。

// 此方法属于线程池包装类ExecutorTtlWrapper
@Override
public void execute(@NonNull Runnable command) {
    // 这里会把Rannable包装一层,这是关键,有些逻辑处理,需要在run之前执行
    executor.execute(TtlRunnable.get(command));
}
// 对应上面的get方法,返回一个TtlRunnable对象,属于TtLRannable包装类
@Nullable
public static TtlRunnable get(@Nullable Runnable runnable) {
    return get(runnable, false, false);
}
// 对应上面的get方法
@Nullable
public static TtlRunnable get(@Nullable Runnable runnable, boolean releaseTtlValueReferenceAfterRun, boolean idempotent) {
    if (null == runnable) return null;
    if (runnable instanceof TtlEnhanced) {
        // 若发现已经是目标类型了(说明已经被包装过了)直接返回
        // avoid redundant decoration, and ensure idempotency
        if (idempotent) return (TtlRunnable) runnable;
        else throw new IllegalStateException("Already TtlRunnable!");
    }
    // 最终的初始化
    return new TtlRunnable(runnable, releaseTtlValueReferenceAfterRun);
}
// 对应上面的TtlRunnable方法
private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
    // 这里就是重点了 这里将捕获后的父线程本地变量存储在当前对象的capturedRef里
    this.capturedRef = new AtomicReference<Object>(capture());
    this.runnable = runnable;
    this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
}
// 对应上面的capture方法,用于捕获当前线程(父线程)里的本地变量,此方法属于TTL的静态内部类Transmitter
@Nonnull
public static Object capture() {
    Map<TransmittableThreadLocal<?>, Object> captured = new
    HashMap<TransmittableThreadLocal<?>, Object>();
    for (TransmittableThreadLocal<?> threadLocal : holder.get().keySet()) {
        // holder里目前存放的k-v里的key,就是需要传给子线程的TTL对象
        captured.put(threadLocal, threadLocal.copyValue());
    }
    // 这里返回的这个对象,就是当前将要使用线程池异步出来的子线程,所继承的本地变量合集
    return captured; 
}
// 对应上面的copyValue,简单的将TTL对象里的值返回(结合之前的源码可以知道get方法其实就是获取当前线程(父线程)里的值,调用super.get方法)
private T copyValue() {
    return copy(get());
}
protected T copy(T parentValue) {
    return parentValue;
}

结合上述代码,大致知道了在线程池异步之前需要做的事情,其实就是把当前父线程里的本地变量取出来,然后赋值给

Rannable包装类里的capturedRef属性,那么在接下来线程执行的时候,也就是run方法里,大概率会将这些捕获到的值

赋给子线程的holder赋对应的TTL值,我们继续往下看Rannable包装类里的run方法是怎么实现的:

// run方法属于Rannable的包装类TtlRunnable
@Override
public void run() {
    // 获取由之前捕获到的父线程变量集
    Object captured = capturedRef.get();
    if (captured == null || releaseTtlValueReferenceAfterRun && !capturedRef.compareAndSet(captured, null)) {
        throw new IllegalStateException("TTL value reference is released after run!");
    }
    // 重点方法replay,此方法用来给当前子线程赋本地变量,返回的backup是此子线程原来就有的本地变量值
    // backup用于恢复数据(如果任务执行完毕,意味着该子线程会归还线程池,那么需要将其原生本地变量属性恢复)
    Object backup = replay(captured);
    try {
        // 执行异步逻辑
        runnable.run();
    } finally {
        // 这个方法就是用来恢复原有值的
        restore(backup);
    }
}

根据上述代码,我们看到了TTL在异步任务执行前,会先进行赋值操作(就是拿着异步发生时捕获到的父线程的本地变

量,赋给自己),当任务执行完,就恢复原生的自己本身的线程变量值。

下面来具体看这俩方法:

// 下面的方法均属于TTL的静态内部类Transmittable
@Nonnull
public static Object replay(@Nonnull Object captured) {
    @SuppressWarnings("unchecked")
    // 使用此线程异步时捕获到的父线程里的本地变量值
    Map<TransmittableThreadLocal<?>, Object> capturedMap =(Map<TransmittableThreadLocal<?>, Object>) captured; 
    //当前线程原生的本地变量,用于使用完线程后恢复用
    Map<TransmittableThreadLocal<?>, Object> backup = newHashMap<TransmittableThreadLocal<?>, Object>(); 

    //注意:这里循环的是当前子线程原生的本地变量集合,与本方法相反,restore方法里循环这个holder是指:该线程运行期间产生的变量+父线程继承来的变量
    for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();iterator.hasNext(); ) {
        Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
        TransmittableThreadLocal<?> threadLocal = next.getKey();
        // 所有原生的本地变量都暂时存储在backup里,用于之后恢复用
        backup.put(threadLocal, threadLocal.get()); 
        /**
         * 检查,如果捕获到的线程变量里,不包含当前原生变量值,则从当前原生变量里清除掉,对应的线程本地变量也清掉
         * 这就是为什么会将原生变量保存在backup里的原因,为了恢复原生值使用
         * 那么,为什么这里要清除掉呢?因为从使用这个子线程做异步那里,捕获到的本地变量并不包含原生的变量,当前线程
         * 在做任务时的首要目标,是将父线程里的变量完全传递给任务,如果不清除这个子线程原生的本地变量,
         * 意味着很可能会影响到任务里取值的准确性。
         *
         * 打个比方,有ttl对象tl,这个tl在线程池的某个子线程里存在对应的值2,当某个主线程使用该子线程做异步任务时
         * tl这个对象在当前主线程里没有值,那么如果不进行下面这一步的操作,那么在使用该子线程做的任务里就可以通过
         * 该tl对象取到值2,不符合预期
         */
        if (!capturedMap.containsKey(threadLocal)) {
            iterator.remove();
            threadLocal.superRemove();
        }
    }
    // 这一步就是直接把父线程本地变量赋值给当前线程了(这一步起就刷新了holder里的值了,具体往下看该方法,在异步线程运行期间,还可能产生别的本地变量,比如在真正的run方法内的业务代码,再用一个tl对象设置一个值)
    setTtlValuesTo(capturedMap);
    // 这个方法属于扩展方法,ttl本身支持重写异步任务执行前后的操作,这里不再具体赘述
    doExecuteCallback(true);
    return backup;
}
// 结合之前Rannable包装类的run方法来看,这个方法就是使用上面replay记录下的原生线程变量做恢复用的
public static void restore(@Nonnull Object backup) {
    @SuppressWarnings("unchecked")
    Map<TransmittableThreadLocal<?>, Object> backupMap =(Map<TransmittableThreadLocal<?>, Object>) backup;
    // call afterExecute callback
    doExecuteCallback(false);
    // 注意,这里的holder取出来的,实际上是replay方法设置进去的关于父线程里的所有变量(结合上面来看,就是:该线程运行期间产生的变量+父线程继承来的变量)
    for (Iterator<? extends Map.Entry<TransmittableThreadLocal<?>, ?>> iterator = holder.get().entrySet().iterator();iterator.hasNext(); ) {
        Map.Entry<TransmittableThreadLocal<?>, ?> next = iterator.next();
        TransmittableThreadLocal<?> threadLocal = next.getKey();
        /**
        * 同样的,如果子线程原生变量不包含某个父线程传来的对象,那么就删除,可以思考下,这里的清除跟上面replay里的有什么不同?
        * 这里会把不属于原生变量的对象给删除掉(这里被删除掉的可能是父线程继承下来的,也可能是异步任务在执行时产生的新值)
        */
        if (!backupMap.containsKey(threadLocal)) {
            iterator.remove();
            threadLocal.superRemove();
        }
    }
    // 同样调用这个方法,进行值的恢复
    setTtlValuesTo(backupMap);
}
// 真正给当前子线程赋值的方法,对应上面的setTtlValuesTo方法
private static void setTtlValuesTo(@Nonnull Map<TransmittableThreadLocal<?>, Object> ttlValues) {
    for (Map.Entry<TransmittableThreadLocal<?>, Object> entry : ttlValues.entrySet()) {
        @SuppressWarnings("unchecked")
        TransmittableThreadLocal<Object> threadLocal = (TransmittableThreadLocal<Object>) entry.getKey();
        threadLocal.set(entry.getValue()); //赋值,注意,从这里开始,子线程的holder里的值会被重新赋值刷新,可以参照上面ttl的set方法的实现
    }
}

好啦,到这里基本上把TTL比较核心的代码看完了,下面整理下整个流程,这是官方给出的时序图:

5. 应用场景

TransmittableThreadLocal用来实现线程间的参数传递,经典应用场景如下:

(1)分布式跟踪系统 或 全链路压测(即链路打标)

(2)日志收集记录系统上下文

(3)Session级 Cache

(4)应用容器或上层框架跨应用代码给下层 SDK传递信息

6. ThreadLocal,InheritableThreaLocal与TransmittableThreadLocal的比较

ThreadLocal、InheritableThreadLocal与TransmittableThreadLocal在Java中都是用于处理线程局部变量的工具,但它

们在使用场景和特性上有所不同。

(1)ThreadLocal

ThreadLocal是Java中一个非常重要的线程技术,它为每个线程提供了它自己的变量副本,使得线程间无法相互访问对方

的变量,从而避免了线程间的竞争和数据泄露问题。适用于需要在线程内部存储和获取数据,且不希望与其他线程共享数

据的场景。

(2)InheritableThreadLocal

InheritableThreadLocal是ThreadLocal的一个子类,它包含了ThreadLocal的所有功能,并扩展了ThreadLocal的功

能。 允许父线程中的InheritableThreadLocal变量的值被子线程继承。当创建一个新的线程时,这个新线程可以访问其父

线程中InheritableThreadLocal变量的值。

(3)TransmittableThreadLocal

TransmittableThreadLocal是阿里巴巴开源的一个框架,用于解决在使用线程池等场景下,ThreadLocal变量无法跨线程

传递的问题。能够在多线程传递中保持变量的传递性,确保在父线程和子线程之间正确传递ThreadLocal变量。

简单的来说就是:ThreadLocal适用于线程内部的数据存储和访问,确保数据在线程间的隔离。InheritableThreadLocal

适用于需要在父线程和子线程间传递数据的场景,实现数据的继承。TransmittableThreadLocal则是为了解决在使用线程

池等场景下,ThreadLocal变量无法跨线程传递的问题,实现数据的跨线程传递。

标签:变量,迭代,get,Thread,ThreadLocal,线程,多线程,public
From: https://blog.csdn.net/qq_51226710/article/details/141806104

相关文章