ThreadLocal详解,ThreadLocal源码分析,ThreadLocal图解

时间:2023-03-08 22:46:01
ThreadLocal详解,ThreadLocal源码分析,ThreadLocal图解

本文脉路:

概念阐释 ----》  原理图解  ------》 源码分析 ------》  思路整理  ----》 其他补充。

一、概念阐述。

ThreadLocal 是一个为了解决多线程并发场景下的数据安全问题的一个工具类。它可以使得多线程环境下成员变量的使用变得安全。

在使用ThreadLocal的时候,每个线程在ThreadLocal上 set值之后,get到的还是自己set的值。并发情况下,线程之间的存值、取值互不影响。

实际上,ThreadLocal的名字取得并不贴切,如果按照它的功能和作用来命名,应该叫做 ThreadLocalVariable, 即:线程本地变量,下面我们来具体分析。

二、原理图解

首先引入一个例子,我们通常对ThreadLocal的使用就类似于下面的伪代码。

载这个伪代码中,我们只需要调用 set(x) 来设置值,调用get()来获得值。而这个context是一个共享变量。

通常情况下“共享”的变量是不安全的,那么凭什么这个ThreadLocal就是安全的呢?怎么保证数据不会出错?

public class Demo{

	ThreadLocal<String> context = new ThreadLocal<>();

	public String method1(String param1,String param2){
context.set(...);
//...
// other code...
} public String method2(String param1,String param2 ...){
String value = context.get();
// ...
// other code...
} }

OK,上图说明:
ThreadLocal详解,ThreadLocal源码分析,ThreadLocal图解

上图中,我们先看红线左边,左边图展示了ThreadLocal的内部大概实现。右边展示了使用的时候,对象在内存中的关系示意图。

通过左边的图可以看到,ThreadLocal 有个内部类——ThreadLocalMap, 其实这个就是存放数据的。 而Thread类具有一个ThreadLocalMap成员变量。

再看红线左边的示例代码和图形展示, 我们可以看到数据是跟着Thread走的,Thread所set的值被拴上了一根绳子,并握在自己手上。所以数据不会串。

如果没看明白,没关系,毕竟还没有看具体代码实现。先把这个图当做开胃菜,下面再来大吃代码。

三、源码分析

3.1.ThreadLocal源码注释。

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). 

For example, the class below generates unique identifiers local to each thread. A thread's id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls. 

 import java.util.concurrent.atomic.AtomicInteger;

 public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0); // Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();
}
}; // Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
} 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).

粗略翻译:

这个类提供了线程局部变量。它和其他变量不同之处在于,每条线程访问(通过其get或者set方法)的变量都有其自己的独立变量副本。

例如,下面的类为每条线程生成了唯一的标识。
当第一次调用ThreadId.get()方法的时候,线程的ID便分配了,并且在后续的调用中持续保持不变。 public class ThreadId {
// 原子的interger包含的了下一个要分配的线程ID
private static final AtomicInteger nextId = new AtomicInteger(0); // 线程局部变量包含了每个线程的ID.
private static final ThreadLocal<Integer> threadId =
new ThreadLocal<Integer>() {
@Override protected Integer initialValue() {
return nextId.getAndIncrement();//这里复写了初始化方法,使得每个线程在没有set线程值之前就具有默认值,使得get能返回这个初始化值,并且每个线程返回的初识值一样。
}
}; // Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
} 只要线程还存活并且ThreadLocal实例可以访问,那么每个线程都会持有一个自己对线程局部变量的副本的一个隐式引用。 当一个线程消失之后,它对线程局部变量的所有副本都会受到垃圾回收(处分这个副本有别的引用存在)。

为什么ThreadLocal可以实现每条线程访问操作各自互不相干的值呢,是如何实现的呢,我准备从set,get方法作为入口,看它是如何存取的。

3.2 如何 set 值?

ThreadLocal存值方法即set方法实现如下:

    public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

当某个线程调用ThreadLocal实例的set方法的时候。

第一行 先获得了当前调用set的线程 t 。

第二行 根据线程 t  调用 getMap(t)方法找到当前线程对应的ThreadLocalMap。这个map实际上就是 线程 t 的一个成员变量。从代码实现可以看出:

    ThreadLocalMap getMap(Thread t) {
return t.threadLocals;//返回当前线程的这个threadLocals变量
} 

第三行开始的 if判断中,如果有 ThreadLocalMap , 则 调用 set 方法  将:<当前ThreadLocal实例  ,当前set值 > 组成的键值对插入这个 ThreadLocalMap 。

如果获取不到其对应的ThreadLocalMap则创建。(这里的ThreadLocalMap其实就是一个可以存储键值对的数据结构。下面在分析)

createMap方法代码如下:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
//t 是当前线程,threadLocals是Thread类的一个成员变量,这里创建了一个新的ThreadLocalMap对象赋给这个变量
}

3.3 如何 get 值?

如何get 值?  怎么把大象放进冰箱,就怎么把大象从冰箱中拿出来,对不对?

在看get 代码实现之前,先回忆一下上 set 方法是如何存值的:

1. 取得当前线程。

2. 取得当前线程的局部变量ThreadLocalMap

3. 将 当前 ThreadLocal 实例,当前的“大象”    组成键值对,放到这个 ThreadLocalMap中。

4. 完成。

所以,取值的方法应该是和它相反的,也就是“将第3步中的 存,改为 取”。我们来看看代码是不是这样写的?

取值方法get 实现如下:

    public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}

果然,就是我们分析的那样。

3.4思路整理

为了存储一个数据,牵扯到了三个概念: ThreadLocal  , ThreadLocalMap ,Thread  。

貌似有点乱,整理一下是这样的:

假定 有一个 ThreadLocal 实例,名字叫做 beautifulGirl 。当前有两个线程 T1,T2 分别想将自己的染色体 Y1, Y2  给 beautifulGirl 保管  (你懂得)

实际上 Y1,Y2被放在了两个ThreadLocalMap实例 中,T1,T2 手中分别捏着一个 各自的map实例 map1,map2,Y1,Y2分别在其中。

当T1 想拿回自己的 Y1的时候,就找到beautifulGirl,让把手伸进map1 将Y1拿出来。   其实就是这个原理。

可以结合这个解释,回过头看看图 和代码就明白了。

看明白的朋友也许要问:你不说这个map内部是个 Entry[] 数组吗?这里T1,只是将自己的Y1放进去了,不需要数组啊。一个对象不就行了。实际上:

有一天,T1,又碰到了一个 ThreadLocal实例,beautifulGirl2 ,T1又兴致大发,调用beautifulGirl2.set(X1)方法将自己的染色体 X1 交给beautifulGirl2保管。于是,T1手中捏着的那个map1便有了两个值。所以,map 才被设计成数组。就是为了让T1,T2可以 把X,Y染色体都送出去保存。就是这样:

ThreadLocal详解,ThreadLocal源码分析,ThreadLocal图解

3.5.ThreadLocalMap是什么。

通过上面的set方法跟踪,可以发现ThreadLocalMap是ThreadLocal的一个静态内部类。这个类的注释如下:

ThreadLocalMap is a customized hash map suitable only for maintaining thread local values. 
No operations are exported outside of the ThreadLocal class.
The class is package private to allow declaration of fields in class Thread.
To help deal with very large and long-lived usages, the hash table entries use
WeakReferences for keys. However, since reference queues are not
used, stale entries are guaranteed to be removed only when the table starts running out of space.
ThreadLocalMap 是一个定制的hash map适用于保持线程本地变量。没有操作方法是在ThreadLocal类之外的发生的。
为了帮助处理非常大和长期存活的 使用情况,哈希表 的entry 采用了弱引用(WeakReferences )来作为KEY,并保证 当哈希表空间快要使用完的时候 就的entry会被删除。