java中ThreadLocal详解

什么是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 等。

自己的应用代码中也可以灵活运用ThreadLocal,详见我之前的文章:
Mybatis拦截器结合ThreadLocal实现数据库updateTime等操作字段的更新

ThreadLocal与线程池的坑

详见文章:ThreadLocal在线程池中被串用

原创文章 21 获赞 13 访问量 8393

猜你喜欢

转载自blog.csdn.net/iteye_19045/article/details/102935274