ThreadLocal、InheritableThreadLocal、TransmittableThreadLocal


多线程 访问同一个共享变量的时候容易出现并发问题,ThreadLocal是除了加锁这种同步方式之外的一种保证规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题。

一、ThreadLocal

1、使用案例

//1、定义静态方法
public class TestThreadLocal {
    public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();
    public static String get(){
        return RESOURCE.get();
    }
    public static void set(String x){
        RESOURCE.set(x);
    }
}

//2、当前线程下,任何方法里面都可以调用THreadLocal来获取数据。
//3、要正常拿到数据保证A线程设置数据,A线程才能获取设置的数据,
//4、也就是获取和设置可以在任何方法里面,但是必须是相同的线程对象。
private void A(){
    TestThreadLocal.set("1");
    B();
}
private void B(){
    String s = TestThreadLocal.get();
}

使用ThreadLocal的最终目的还是为了得到安全的数据,下面分析下ThreadLocal的结构

2、Thread

在提到ThreadLocal前我们先看下Thread类以及属性

class Thread implements Runnable{
	//每一个线程都有一个属于自己的Map,里面存的就是线程私有的数据。
	ThreadLocal.ThreadLocalMap threadLocals = null;

	//子类可以拿到的Map数据。下面有单独介绍inheritableThreadLocals
	ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

为什么要先提Thread,因为每个Thread都会绑定一个ThreadLocal,通过Thread的属性threadLocals,就可以拿到ThreadLocal的私有数据

3、ThreadLocal

public class ThreadLocal<T> {
	//Map的Key的类型是ThreadLocal,value是Object类型
	static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
			
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
		
		//属性,是个数组,所以ThreadLocal的内容都保存在了这里
		private Entry[] table;
    }
}

注意Entry[] table是ThreadLocalMap的属性

public void set(T value) {
    //获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    else//如果map为null,说明首次添加,需要首先创建出对应的map
        createMap(t, value);
}

ThreadLocalMap getMap(Thread t) {
	//获取线程对象里面的属性threadLocals,并绑定到当前调用线程的成员变量threadLocals上
    return t.threadLocals; 
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
    //创建给ThreadLocalMap的table属性赋值,并且将firstValue放在数组首位。
}


createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中

public T get() {
    //(1)获取当前线程
    Thread t = Thread.currentThread();
    //(2)获取当前线程的threadLocals变量
    ThreadLocalMap map = getMap(t);
    //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //(4)执行到此处,threadLocals为null,初始化当前线程的threadLocals变量
    return setInitialValue();
}


ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
private T setInitialValue() {
    //获取初始值,每个类对这个方法的实现是不一样的。
    T value = initialValue();
    //获取当前线程
    Thread t = Thread.currentThread();
   	//拿到threadLocals对象
    ThreadLocalMap map = getMap(t);
    //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
    if (map != null)
        map.set(this, value);
    else	//如果map为null,说明首次添加,需要首先创建出对应的map
        createMap(t, value);
    return value;
}
public void remove() {
    //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
 }

4、结构

ThreadLocal我们往往会在类中将其定义成静态常量,所以ThreadLocal的实例对象,从一开始就固定了,而且固定不变,变动的线程和线程里面的Map对象。

1、当2个线程分别往ThreadLocal设置了数据后,两个线程Thread对象和ThreadLcal对象关系图如下
在这里插入图片描述

https://img-blog.csdnimg.cn/537ae44afbd74a9ebe2189be86dfadf6.png,上图中的2个Map是不同的,因为他们都是通过ThreadA对象和ThreadB对象创建来的,但是这个Key是相同的,都是指向ThreadLocal的实例对象。

2、当一个线程,通过多个ThreadLocal实例对象设数据时,结构如下图,

在这里插入图片描述
https://img-blog.csdnimg.cn/84b8237c55224a6ea7011ee1dfa1cf3b.png

5、问题:

同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的

private void A(){
	//设置数据
    TestThreadLocal.set("1");
    B();
}
private void B(){
	//创建子线程
    Thread thread = new Thread(() -> {
        TestThreadLocal.get();//子线程是获取不到数据的。
    });
    thread.start();
}

好在InheritableThreadLocal可以解决这个问题

二、InheritableThreadLocal

如何解决子线程获取父线程的数据就要使用InheritableThreadLocal

class Thread implements Runnable{
	ThreadLocal.ThreadLocalMap threadLocals = null;
	//子线程用的
	ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
class InheritableThreadLocal<T> extends ThreadLocal<T>{
	//childValue方法在复制父类的parent.inheritableThreadLocals时候会用到
    //比如复制时代码如下:Object value = key.childValue(e.value);
	protected T childValue(T parentValue) {
	    return parentValue;
	}

	ThreadLocalMap getMap(Thread t) {
	   return t.inheritableThreadLocals;
	}

	void createMap(Thread t, T firstValue) {
	    t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
	}	
}

1、使用方式

public class InheritableThreadLocalTest {
    private static ThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    public static String get() {
        return threadLocal.get();
    }
    public static void set(String x) {
        threadLocal.set(x);
    }
}

//设置数据
private void A() {
    InheritableThreadLocalTest.set("1");
    B();
}
//子线程获取数据
private void B() {
    Thread thread = new Thread(() -> {
        String s = InheritableThreadLocalTest.get();
    });
    thread.start();
}

InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap方法。
其中createMap方法在被调用的时候,创建的是inheritableThreadLocal而不是threadLocals。
同理,getMap方法在当前调用者线程调用get方法的时候返回的也不是threadLocals而是inheritableThreadLocal。

下面我们看看重写的childValue方法在什么时候执行,怎样让子线程访问父线程的本地变量值。我们首先从Thread类开始说起

就是当我们创建一个线程的时候,由主线程来创建,那么主线程就一定会有threadLocals和inheritableThreadLocals,在创建子线程的时候,
主线程会把自己的inheritableThreadLocals复制到子线程的inheritableThreadLocals,注意是复制了父线程的inheritableThreadLocals

不单单复制inheritableThreadLocals还会复制下面这些对象

this.priority = parent.getPriority();//线程的优先级
this.contextClassLoader = parent.getContextClassLoader();//类加载器
this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

下面是显示创建线程会调用的方法。会吧当前执行这个方法的线程变量复制到子线程变量中去。

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize) {
    init(g, target, name, stackSize, null, true);
}
private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    
    //(1)获取当前线程(父线程)
    Thread parent = currentThread();
    //(2)父线程的inheritableThreadLocal和inheritThreadLocals=true
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
    //(3)父线程的inheritableThreadLocals赋值给子线程的inheritableThreadLocals
    this.inheritableThreadLocals = 
         ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
}

在init方法中,首先(1)处获取了当前线程(父线程),然后(2)处判断当前父线程的inheritableThreadLocals是否为null,然后调用createInheritedMap将父线程的inheritableThreadLocals作为构造函数参数创建了一个新的ThreadLocalMap变量,然后赋值给子线程。下面是createInheritedMap方法和ThreadLocalMap的构造方法

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

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);
                //通过Key和value生成一个新的Entry
                Entry c = new Entry(key, value);
                int h = key.threadLocalHashCode & (len - 1);
                while (table[h] != null)
                    h = nextIndex(h, len);
                table[h] = c;
                size++;
            }
        }
    }
}

遍历父类的Entry,然后挨个复制,但其实就一个元素有值

在构造函数中将父线程的inheritableThreadLocals成员变量的值赋值到新的ThreadLocalMap对象中。返回之后赋值给子线程的inheritableThreadLocals。
总之,InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。当子线销毁的时候,会调用inheritableThreadLocals = null 将子线程的inheritableThreadLocal置空。

结构

当一个线程里面既用到了ThreadLocal又用到了inheritableThreadLocal时结构如下
在这里插入图片描述
Map是存ThreadLocal类型的数据,inheritableMap是存inheritableThreadLocal类型的数据,只有设置了对应的Map才会有数据,如果inheritMap如果不是本线程设置的值,那就是从父类的inheritMap里面copy过来的。

如果是父线程里面创建了一个子线程,那么数据结构关系如下图
在这里插入图片描述
inheritableMap就从父类里面copy过来,前提是在父类线程里面手动设置了inheritableMap数据。

当项目中定义了多个inheritableThreadLocal实例对象,在一个线程给不同的inheritableThreadLocal对象设置了不同的数据时,结构如下图

在这里插入图片描述

从inheritableThreadLocals存在的问题

子线程的InheritableThreadLocal是从父线程里面深度copy过来的,当我们使用线程池,那么子线程是核心线程的话,是会被复用的。既然子线程是同一个对象,那么拿到的InheritableThreadLocal也必然是一个。

举个例子,2个不同的请求过来,把2个请求的用户信息放到InheritableThreadLocal,然后使用线程池里面的线程处理任务。

1、假设A先请求过来,把A的用户数据塞到InheritableThreadLocal里面,在线程池里面获取InheritableThreadLocal里面的数据,拿到的是A的用户数据,处理结束后。
2、当B请求过来,把B的用户数据塞到InheritableThreadLocal,在线程池里面获取InheritableThreadLocal的数据,拿到的可能还是A的用户数据。

为什么?虽然A和B用的是不同的线程设置了InheritableThreadLocal,但是使用线程池的时候,子线程是一样的,这个子线程还是在A线程哪里创建的,创建的时候,子线程复制的InheritableThreadLocal还是A线程的。因为核心线程是会复用的,B使用线程池的时候,用的还是原来的核心线程,子线程一样,必然InheritableThreadLocal里面的内容也是一样。

public class InheritableThreadLocalTest {
    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();

    public static String get() {
        return threadLocal.get();
    }

    public static void set(String x) {
        threadLocal.set(x);
    }
}

public class TestA {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    public static void main(String[] args) {
        TestA testA = new TestA();
        //模拟请求A
        testA.taskA();
        //模拟请求B
        testA.taskB();
    }

    private void taskA() {
        //请求A进来,设置A的用户信息,到父线程
        InheritableThreadLocalTest.set("A用户信息");
        //子线程获取父线程数据
        executorService.submit(() -> {
            System.out.println(InheritableThreadLocalTest.get());
        });
    }
    private void taskB() {
        //请求B进来,设置B的用户信息,到父线程
        InheritableThreadLocalTest.set("B户信息");
        //子线程获取父线程数据
        executorService.submit(() -> {
            System.out.println(InheritableThreadLocalTest.get());
        });
    }
}
-----------输出:
A用户信息
A用户信息

疑问1:核心线程数什么时候会销毁

使用线程池

executor.execute(()->{})
public void execute(Runnable command) {
	//当线程池中的核心线程小于配置的核心线程数量时
	if (workerCountOf(c) < corePoolSize) {
    	if (addWorker(command, true)) return;
	}
	//执行任务入队,入队的任务是复用核心线程执行还是创建新的非核心线程?
	if (isRunning(c) && workQueue.offer(command)) {
		......
	}	
}

看下addWorker做了什么

private final HashSet<Worker> workers = new HashSet<Worker>();

private boolean addWorker(Runnable firstTask, boolean core) {
	Worker w = new Worker(firstTask);
	workers.add(w);//将核心线程放入队列
}

Worker(Runnable firstTask) {
    setState(-1);
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);//这里是创建新的线程
}

当核心线程数还没用完的时候,会创建新的线程,那么InheritableThreadLocal的值就会从父线程里面copy,自然是没有问题,当我们在子线程中操作InheritableThreadLocal是可以拿到数据。

public boolean offer(E e) {
    return offerLast(e);
}

public boolean offerLast(E e) {
    if (e == null) throw new NullPointerException();
    Node<E> node = new Node<E>(e);
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        return linkLast(node);
    } finally {
        lock.unlock();
    }
}

private boolean linkLast(Node<E> node) {
    // assert lock.isHeldByCurrentThread();
    if (count >= capacity)
        return false;
    Node<E> l = last;
    node.prev = l;
    last = node;
    if (first == null)
        first = node;
    else
        l.next = node;
    ++count;
    notEmpty.signal();
    return true;
}
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    
    while (task != null || (task = getTask()) != null) {
        task.run();
    }
}
private Runnable getTask() {
    for (;;) {
        workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS)
    }
}

但是如果有新的任务进来,只要核心线程有空闲,就会复用原先创建好的核心线程,这个时候,如果上一个使用过这个线程的子线程修改了InheritableThreadLocal,那么当前的子线程在使用InheritableThreadLocal就会有问题了。因为这次是没有重行创建新线程,那么InheritableThreadLocal还是之前的InheritableThreadLocal。

如何解决

三、TransmittableThreadLocal

1、使用案例:

public class TestA {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    public static void main(String[] args) {
        TestA testA = new TestA();

        //模拟请求A
        testA.taskA();
        //模拟请求B
        testA.taskB();

    }

    private void taskA() {
        //请求A进来,设置A的用户信息,到父线程
        TransmittableThreadLocalTest.set("A用户信息");

        //创建任务A
        RunnableTask runnableTask = new RunnableTask();
        TtlRunnable ttlRunnable = TtlRunnable.get(runnableTask);
        executorService.submit(ttlRunnable);//把任务丢到线程池里面去
    }

    private void taskB() {
        //请求B进来,设置B的用户信息,到父线程
        TransmittableThreadLocalTest.set("B用户信息");

        //创建任务B
        RunnableTask runnableTask = new RunnableTask();
        TtlRunnable ttlRunnable = TtlRunnable.get(runnableTask);
        executorService.submit(ttlRunnable);
    }
}

class TransmittableThreadLocalTest {
    private static TransmittableThreadLocal<String> USER_CONTEXT = new TransmittableThreadLocal<>();
    public static String get() {
        return USER_CONTEXT.get();
    }
    public static void set(String x) {
        USER_CONTEXT.set(x);
    }
}

class RunnableTask implements Runnable{
    @Override
    public void run() {
        //获取TransmittableThreadLocal变量
        System.out.println(TransmittableThreadLocalTest.get());
    }
}

这里有个TtlRunnable类,这个东西很重要,当线程池调用任务的run方法时,TtlRunnable会对run方法的前后做一个特殊处理,因为任务的创建的创建是通过父类线程来创建的,线程是通过线程池来创建,那么在创建的任务的时候,TtlRunnable里面有个设置了一个属性,这个属性就是存的父类里面的ThreadLocal的数据,在执行任务的时候,这个任务是子线程执行的,也就是执行TtlRunnable对象的run方法时,拿到TtlRunnable对象事前存好的属性,替换到子线程里面的ThreadLocal数据,在任务执行完之后,在把子线程原有的ThreadLocal设置回去。

public final class TtlRunnable implements Runnable, TtlWrapper<Runnable>, TtlEnhanced, TtlAttachments {
	//这两个属性是配套用的,作用是什么?
	//这个表示是否使用锁的方式来执行任务,为什么要加锁,因为一个TtlRunnable对象,可以被线程池执行多次,也就是多个线程执行这个TtlRunnable对象,而一个对象里面的属性,比如会有线程安全的问题,有就引入了下面的Atomic类,这个类有CAS方法,足够保障访问属性线程安全。
	private final boolean releaseTtlValueReferenceAfterRun;
    private final AtomicReference<Object> capturedRef = new AtomicReference(Transmitter.capture());

    private final Runnable runnable;//真正的任务
  
    //构造方法
    private TtlRunnable(@NonNull Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        this.runnable = runnable;
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }

    public void run() {
        Object captured = this.capturedRef.get();//这里就是拿到Atomic对象,里面包含了父类ThreadLocal的数据。
        if (captured != null && (!this.releaseTtlValueReferenceAfterRun || this.capturedRef.compareAndSet(captured, (Object)null))) {

        	//把当前线程里面的ITL数据清空,再把创建TtlRunnable对象时,从父类的ThreadLocal拿到的对象,塞到当前执行者线程,也就是子线程类里面去。
        	//返回清空的数据,因为在最后finally里面把数据在存回去。
            Object backup = Transmitter.replay(captured);

            try {
                this.runnable.run();
            } finally {
                Transmitter.restore(backup);//把子线程原有的数据设置回来。
            }

        } else {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
    }
}

结构图

在这里插入图片描述
当第二个线程也使用TransmittableThreadLocal设置数据时,结构如下图

在这里插入图片描述

注意2个线程的inheritableMap不是同一个东西,因为这个2个Map是来自2个不同Thread实例,创建的Map实例,所以2个Map里面都存了2个key,一个是我们自定义的TransmittableThreadLocal实例,一个是TransmittableThreadLocal框架自定义的Holder实例(Holder是静态常量所以实例对象固定不变),所以2个Map的第二个key,也就是Holder是同一个实例对象,也即是2个Map的第二个key是一样的,key所对应的value也就是WeakHashMap对象,也是同一个Map,并且这个Map里面key,其实就是我们自己定义的TransmittableThreadLocal实例对象,所以这里key也是一样的。

  • 6
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

信仰_273993243

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

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

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

打赏作者

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

抵扣说明:

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

余额充值