ThreadLocal的深度解读

发布时间 2023-12-04 21:13:21作者: 爱睡懒觉的我

原文链接:https://zhuanlan.zhihu.com/p/624851777

一、J2SE的原始描述

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

该类提供线程局部变量。这些变量与普通变量的不同之处在于,每个线程访问的变量(通过其get或set方法)都是自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的private static字段,希望将有状态变化的对象与线程关联(例如用户ID或事务ID)。

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

只要线程是活的,并且ThreadLocal实例是可访问的,每个线程都持有对其线程局部变量副本的隐式引用,即弱引用(除非存在对这些副本的其他引用)。

二、ThreadLocal解读

ThreadLocal线程变量,即线程局部变量,该ThreadLocal的变量只属于当前线程独享,对其他线程而言是隔离的。ThreadLocal在每个线程中都创建一个副本,每个线程只访问自己的副本。

ThreadLocal 变量通常被private static修饰,因此是同一个对象,并且是同一个 ThreadLocal 对象在不同的 Thread 中创建不同的副本。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

三、ThreadLocal示例

package com.observer;
 
public class ThreadLocalTest {
 
	private static ThreadLocal<String> localVar = new ThreadLocal<String>();
 
	public static void print(String str) {
		// 打印当前线程中本地内存中本地变量的值
		System.out.println(str + " :" + localVar.get());
		// 清除本地内存中的本地变量
		localVar.remove();
	}
 
	public static void main(String[] args) throws InterruptedException {
 
		new Thread(new Runnable() {
			public void run() {
				for (int i = 1; i <= 5; i++) {
					ThreadLocalTest.localVar.set("local_A--" + i);
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					print("A-" + i);
					// 清楚后打印
					System.out.println("A-" + i + "---after remove : " + localVar.get());
				}
			}
		}, "A").start();
 
		new Thread(new Runnable() {
			public void run() {
				for (int i = 1; i <= 5; i++) {
					ThreadLocalTest.localVar.set("local_B--" + i);
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					print("B-" + i);
					// 清楚后打印
					System.out.println("B-" + i + "---after remove : " + localVar.get());
				}
			}
		}, "B").start();
	}
}

打印输出:
A-1 :local_A--1
A-1---after remove : null
B-1 :local_B--1
B-1---after remove : null
A-2 :local_A--2
A-2---after remove : null
B-2 :local_B--2
B-2---after remove : null
A-3 :local_A--3
A-3---after remove : null
B-3 :local_B--3
B-3---after remove : null
A-4 :local_A--4
A-4---after remove : null
B-4 :local_B--4
B-4---after remove : null
A-5 :local_A--5
A-5---after remove : null
B-5 :local_B--5
B-5---after remove : null

四、ThreadLocal的原理

4.1、set方法

public void set(T value) {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
        //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            // 初始化thradLocalMap 并赋值
            createMap(t, value);
}
 
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}
 
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}
 
ThreadLocal.ThreadLocalMap threadLocals = null;
 
static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
 
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
 
        .....
}

从代码可以看到,ThreadLocal调用set方法赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。

ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据,使用ThreadLocal对象作为key,使用我们设置的value作为value。

Threadlocal是当前线程中属性ThreadLocalMap集合中的某一个Entry的key值,不同的线程之间threadlocal这个key值是一样,但是不同的线程所拥有的ThreadLocalMap是独立的、隔离的,也就是不同的线程间同一个ThreadLocal(key)对应存储的值(value)不一样,从而到达了线程间变量隔离的目的,但是在同一个线程中这个value变量引用地址是一样的。如下图所示:

4.2、get方法

public T get() {
        //1、获取当前线程
        Thread t = Thread.currentThread();
        //2、获取当前线程的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //3、如果map数据不为空,
        if (map != null) {
            //3.1、获取threalLocalMap中存储的值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
        return setInitialValue();
}
 
 
private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
}

4.3、remove方法

 public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
 }

remove方法,直接将ThrealLocal 对应的值从当前Thread中的ThreadLocalMap中删除。

为什么要删除?这涉及到内存泄露的问题。

ThreadLocalMap中的Entry继承了弱应用WeakReference,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉,Entry空键(即entry.get())== null)表示该键不再被引用,因此该Entry可以被垃圾回收掉。ThreadLocal其实是与线程绑定的一个变量,如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

五、ThreadLocal的应用

ThreadLocal 适用于如下两种场景

1、每个线程需要有自己单独的实例

2、实例需要在多个方法中共享,但不希望被多线程共享

5.1、数据跨层跨方法调用(controller,service, dao)

每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。最常见的是用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。

5.2、数据库连接,处理数据库事务

5.3、Spring使用ThreadLocal解决线程安全问题

一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,Bean默认的作用域为singleton(单例)。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常运行。

Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图9-2所示。

这样用户就可以根据需要,将一些非线程安全的有状态的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。

六、使用ThreadLocal注意事项

1、将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值

2、每次使用完ThreadLocal,都调用它的remove()方法,清除数据,防止内存泄露。