什么是ThreadLocal
ThreadLocal是一个线程局部变量,如果同一个变量需要在多个线程中使用,并且在某个线程中改变这个变量的值,不影响其他线程中这个变量的值,那么我们需要为每一个线程拷贝一个该变量的副本,ThreadLocal就是实现了这个功能。
我们知道变量是有作用域的,如局部变量、全局变量,局部变量只能在一个方法内或一段代码块之内可见(如for循环的计数变量i),全局变量在所有类中都可见(如公有静态成员变量)。
从作用域的角度分析ThreadLocal,它是一个作用域在一个线程之内的变量,作用域既没有限定在某一个方法之内,也没有限定在某个类中,而是在一个线程中,
就算该线程跨类调用了多个方法,在这些方法中都可以读写这个变量,但是跳出这个线程读写这个变量,则读写的已经不是原来线程中的变量(而是这个变量的副本)。
这也是为什么叫“线程局部变量”,见名知意。
使用方式
ThreadLocal 主要方法有4个:
//值的初始化:默认初始化值为null,protected修饰的方法可以通过子类覆盖
protected T initialValue();
//获取线程局部变量的值
publict T get();
//设置线程局部变量的值
public void set(T value);
//删除线程局部变量的值,在线程池环境下,线程执行完要记得清理变量,否则造成变量污染
public void remove();
三种方式初始化
//1. 定义一个整型的ThreadLocal,该整型初始值为0
public static ThreadLocal<Integer> local1=ThreadLocal.withInitial(()->0);
//2. 定义一个整型的ThreadLocal,初始化值为null
public static ThreadLocal<Integer> local2=new ThreadLocal();
//3. 定义一个整型的ThreadLocal,初始化值为0
public static ThreadLocal<Integer> local3=new ThreadLocal(){
@Override
protected Integer initialValue() {
return 0;
}
};
读写方式
//定义一个整型的ThreadLocal,该整型初始值为0
public static ThreadLocal<Integer> local=ThreadLocal.withInitial(()->0);
//设置值为1
local.set(1);
//读取
System.out.println(local.get());
下面可以通过静态方法ThreadLocal.withInitial()直接构造一个带初始值的ThreadLocal,参数为一个lambda表达式。
然后启动5个线程来读写这个ThreadLocal
public class ThreadLocalDemo {
//定义一个值为整型的ThreadLocal,该整型初始值为0
public static ThreadLocal<Integer> local=ThreadLocal.withInitial(()->0);
public static void main(String[] args) {
//启动5个线程
for(int i=0;i<5;i++){
//每个线程中将ThreadLocal值打印两次:先打印初始值,然后加1后再次打印。
new Thread(()->{
Integer k=local.get();
System.out.println("thread id = "+Thread.currentThread().getId()+" , local="+k);
//调用set方法修改变量的值
local.set(k+1);
k=local.get();
System.out.println("thread id = "+Thread.currentThread().getId()+" , local="+k);
}).start();
}
}
}
运行结果如下:
thread id = 12 , local=0
thread id = 16 , local=0
thread id = 15 , local=0
thread id = 15 , local=1
thread id = 14 , local=0
thread id = 13 , local=0
thread id = 14 , local=1
thread id = 16 , local=1
thread id = 12 , local=1
thread id = 13 , local=1
根据结果可以看出,每个线程两次打印分别为0和1,没有累加到5,也就是说ThreadLocal值的累加,只对当前线程有影响,变量只定义了一次,但是每个线程却拥有了一个该变量的副本。
实现原理
源码分析:我们从ThreadLocal.get()方法点进去看
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
可以看出,每一个线程都有一个ThreadLocalMap,线程局部变量正是通过这个Map来存放的,再来看看ThreadLocal.getMap()方法,直接返回t.threadLocals :
线程类Thread里面有一个ThreadLocal.ThreadLocalMap的成员变量:threadLocals
ThreadLocal.ThreadLocalMap里面的entry的key就是ThreadLocal, value就是变量值(即上面提到的副本变量):
结构示意图如下:
如果定义多个ThreadLocal,则每个ThreadLocal在每个线程中都会有一个对应的值,每个线程有自己的ThreadLocalMap,ThreadLocalMap中就保存这这多个ThreadLocal和其对应的值。
也就是说每一个线程都有维护着一个ThreadLocalMap,这个Map的key就是ThreadLocal对象实例,value就是ThreadLocal.set()的值。这样对于同一个ThreadLocal,每个线程都会有一个自己的值(即上文中提到的副本),
每个线程调用ThreadLocal.set()或者ThreadLocal.remove(),只会修改自己的副本,
调用ThreadLocal.get()只会获取自己的副本值,线程之间互不影响。比如:
在Thread1中调用ThreadLocal1.get()则读取的是ThreadLocalMap1中的value1,
在Thread2中调用ThreadLocal1.get()则读取的是ThreadLocalMap2中的value1,
显然这个是两个不同的值。
应用场景
实际上很多框架中都用到了ThreadLocal类,
如Spring事务管理中的Connection对象就是存储在ThreadLocal中的,这样在做事务管理的时候,在框架层面就可以从ThreadLocal中获取当前线程所在的数据库连接(Connection)对象,通过Connection对象进行事务提交或者回滚操作。
还有Hiberante的Session 工具类HibernateUtil 等。