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
作者:Java个体户
原文链接:https://juejin.cn/post/7185898791470366781