线程上下文ThreadLocal

1,434 阅读8分钟

ThreadLocal

作用?在同一个线程内,可以传递数据。

什么意思呢?就是一个请求进来的时候,系统内是要经过很多个类和很多个方法的,那怎么在多个类和多个方法之间传递数据呢?靠ThreadLocal。

说白了,就是创建一个对象:

ThreadLocal threadLocal = new ThreadLocal();

然后这个对象threadLocal<线程对象,数据>,存储了所有请求线程的数据。也就是说,你在不同地方读数据的时候,只要是同一个线程,那么读出来的数据就是同一个数据。这样就实现了在同一个线程内部的不同处理环节传递数据的作用。

说白了,就是map<线程对象,数据>。只不过键值对特殊一点,key是某个线程对象,value是要传递的数据。


如果同一个线程内部,要传递多个数据咋办?

创建多个threadLocal对象。

说白了,就是每个threadLocal对象,如果是同一个线程,就只能传递一个数据。也就是说,threadLocal对象存储的是不同线程的数据,但是呢,同一个线程只能传递一个数据。

如果同一个线程,要传递多个数据,解决方法就是创建多个threadLocal对象。


那threadLocal对象本身是怎么被传递的呢?

其实也不是传递,其实就是怎么才能在不同的地方都能访问到?

基于工具类 + 静态数据:

工具类{
private static ThreadLocal threadLocal = new ThreadLocal();

//写数据
set(){
  threadLocal.set(数据); //写数据
}

//读数据
get(){
  threadLocal.get(); //读数据
}

}

即在不同的地方要访问一个数据,就使用工具类 + 静态数据。


缺点?缺点是什么呢?不能把父线程的数据传递给子数据。

举个例子,父线程写了数据,然后子线程去读,读不到,即读出来为null,因为本来子线程就没有数据,所以肯定是null。

看代码

/**
 * @author javaself
 */
public class InheritableThreadLocalTest2 {
  public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
  public static ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

  public static void main(String[] args) {
    threadLocal.set("threadLocal 的值: hello");
    inheritableThreadLocal.set("threadLocal 的值: hello inheritableThreadLocal");

    new Thread(() -> {
      System.out.println("子线程获取到的值是:");
      //读不到父线程的数据
      System.out.println(threadLocal.get()); //null
      //可以读到父线程的数据
      System.out.println(inheritableThreadLocal.get()); //threadLocal 的值: hello inheritableThreadLocal
    }).start();

  }

}

那怎么解决这个问题呢?只能才能把父线程的数据传递给子线程呢?这个时候就要用到jdk自带的InheritableThreadLocal类。类的名字,就是可传递的意思。

父子线程传递数据-InheritableThreadLocal

还是刚才的代码,看截图

这个是测试结果,InheritableThreadLocal是可以把父线程的数据传递给子线程的。

那实现原理是什么呢?

实现原理

核心原理有两点

1、  InheritableThreadLocal用了一个单独的map

2、  InheritableThreadLocal重写了getMap方法

接下来,讲细节。


首先,InheritableThreadLocal用了一个单独的map。作用是什么呢?也是存储数据,也是<线程对象,数据>,其实和上文最开始提到的map基本上完全一样,只不过这个map是一个单独的map对象,而且是专门用来解决传递数据的问题的。

来看下Thread源码:

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

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; //这个map专门用来解决传递数据的问题

说白了,就是任何一个Thread线程对象都包含了两个map,一个是不能传递数据,一个是专门传递的。就看你写数据的时候,是写到哪个map?如果写到不能传递数据的map,子线程读的时候也就不能传递;如果写到传递数据的map,子线程就可以读父线程的数据。


刚才最后这段话,其实就已经说出了重点,即写和读的时候,到底使用的是哪个map?

那这个问题,其实就是我们核心原理的第二点,即:InheritableThreadLocal重写了getMap方法。

具体是怎么重写的呢?看InheritableThreadLocal源码:

    public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    
    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals; //返回的是可传递数据的map
    }

Thread类的源码,包含了两个map。然后,InheritableThreadLocal类重写了getMap方法,返回数据就是可重写的map。

为什么重写getMap方法,就可以实现传递功能呢?

因为InheritableThreadLocal继承了ThreadLocal。

在写数据的时候,其实就是调用ThreadLocal的set方法,set方法里面会调用getMap方法,由于InheritableThreadLocal继承并且重写了ThreadLocal的getMap方法,所以调用的时候,就调用了InheritableThreadLocal的getMap方法,这个时候获取到的就是可传递map。

来看下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); //调用了父类InheritableThreadLocal重写的getMap方法
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

get方法,同理。


刚才讲的都是理论和源码,我们来实战调试一下源码,就知道为什么子线程可以读父线程的数据了。

测试代码:

public class InheritableThreadLocalTest4 {
  public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
  public static ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

  public static void main(String[] args) {
    inheritableThreadLocal.set("threadLocal 的值: hello inheritableThreadLocal"); //写数据到main线程的可传递map

    new Thread(() -> {
      //可以读到父线程的数据
      System.out.println(inheritableThreadLocal.get()); //threadLocal 的值: hello inheritableThreadLocal //子线程读的时候,也是从main线程的可传递map读数据
    }).start();
  }

}

先看写数据即set方法源码,下面的截图有两个关键点:第一个是父线程就是main线程,第二个是写数据到父线程的可传递map<main线程对象,数据>。

再来看读数据即get方法源码,下面截图是子线程读数据即调用get方法:

截图说明,重点是getMap方法:只要是同一个InheritableThreadLocal对象,那么getMap方法的返回数据就是同一个数据,即main线程的可传递map里的数据。也就是说,main方法如果创建了多个子线程,那么多个子线程都是从main线程的可传递map里读同一个数据。


上文提到,子线程读数据的时候,getMap方法读到的是父线程的数据,这个说法其实是不准确的,准确的说法是,子线程读到的是子线程的数据,即子线程对象.可传递map。只不过子线程对象.可传递map和main线程对象.可传递的值一样,所以相当于就实现了父线程数据传递到子线程的功能。

说白了,核心点就是,子线程对象.可传递map和父线程对象.可传递map的值一样,但是二者是独立的,只不过值是一样的。

那为什么值会一样呢?因为子线程对象.可传递map就是从父线程对象.可传递map复制过来的。

直接看源码:Thread.init方法

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); //把父线程map复制到子线程map

那Thread.init方法什么时候调用呢?就是在main方法里的new Thread的时候调用的。

 public Thread(Runnable target) {
 //调用init方法
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

到此为此,基本上把实现原理和源码分析讲清楚了。


缺点?InheritableThreadLocal有什么缺点呢?

线程池不能传递数据,即不能把父线程数据传递给子线程,为什么呢?因为线程池的线程对象是复用的,那为什么重复使用线程对象就不能传递呢?

准确的说,线程池不是不能传递,其实也能传递,而且实现原理和源码分析也完全一样。但是,如果main线程后面修改了数据,子线程读的数据仍然是旧数据。为什么呢?因为子线程只在创建线程的时候才会把父线程的数据复制到子线程,如果父线程后面又修改了数据,那么同一个子线程读的仍然是旧数据。

测试代码:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

/**
 * @author javaself
 */
public class InheritableThreadLocalTest5 {

  static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

  public static void main(String[] args) throws InterruptedException {
    ExecutorService executorService = Executors.newFixedThreadPool(1);

    inheritableThreadLocal.set("i am a parent");
    
    executorService.execute(new Runnable() {
      @Override
      public void run() {
        System.out.println(inheritableThreadLocal.get()); //i am a parent //旧数据
      }
    });

    TimeUnit.SECONDS.sleep(1);
    inheritableThreadLocal.set("i am a new parent");// 设置新的值

    executorService.execute(new Runnable() {
      @Override
      public void run() {
        System.out.println(inheritableThreadLocal.get()); //i am a parent //仍然是旧数据
      }
    });
  }
}

可以发现,两次打印结果都是旧数据,即两次读的读是旧数据。为什么呢?原因就是因为子线程始终是同一个子线程,即复用了线程池里的线程对象。

另外,如果是子线程修改了数据,那么重复使用同一个子线程的时候,读的就是子线程刚刚设置的新数据。这个时候为什么又可以读到新数据呢?因为修改的本来就是子线程的数据,所以如果是同一个子线程肯定是可以读到自己最新的数据的。

看下测试代码就知道了:

public static void main(String[] args) throws InterruptedException {
 
    ExecutorService executorService = Executors.newFixedThreadPool(1);
 
    inheritableThreadLocal.set("i am a inherit parent");
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(inheritableThreadLocal.get()); //旧数据
            inheritableThreadLocal.set("i am a old inherit parent");// 子线程中设置新的值
        }
    });
 
    TimeUnit.SECONDS.sleep(1);
    inheritableThreadLocal.set("i am a new inherit parent");// 主线程设置新的值
 
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println(inheritableThreadLocal.get()); //子线程可以读到自己的最新的数据
        }
    });
}
 
打印结果:
i am a inherit parent
i am a old inherit parent

可以看到,如果是子线程自己修改数据,那么当线程池里的子线程被复用的时候,读到的数据是新数据。因为修改数据和读数据,都是在同一个子线程自己的可传递map。


那这个问题咋解决呢?就是怎么才能实现线程池复用子线程的时候,始终可以读到父线程的最新数据呢?阿里ThreadLocal。

线程池传递数据-阿里ThreadLocal

这篇文章太长了,下篇文章再讲细节。

参考

blog.csdn.net/vivo_tech/a…

blog.csdn.net/weixin_3626…