@[toc] 在多线程情况下,对于一个共享变量或者资源对象进行读或者写操作时,就必须考虑线程安全问题。而ThreadLocal采用的是完全相反的方式来解决线程安全问题。他实现了对资源对象的线程隔离,让每个线程各自使用各自的资源对象,避免争用引发的线程安全问题。ThreadLocal同时实现了线程内的资源共享。 例如方法1对ThreadLocal中的变量进行了设置,那么方法2中只要是同一个线程,那么他也能访问到线程1在ThreadLocal中设置的变量。
ThreadLocal原理
java
package com.example.scheduledlovetoobject.threadPoolTest;
import lombok.SneakyThrows;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
/**
* @author: Zhangjinbiao
* @Date: 2022/12/7 10:33
* @Connection: qq460219753 wx15377920718
* Description:
* Version: 1.0.0
*/
public class ThreadLocalTest {
public static void main(String[] args) {
test1();
test2();
}
public static void test1() {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
System.out.println(Utils.getConnection());
}, "t" + i).start();
}
}
public static void test2() {
for (int i = 0; i < 2; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + Utils.getConnection());
System.out.println(Thread.currentThread().getName() + Utils.getConnection());
System.out.println(Thread.currentThread().getName() + Utils.getConnection());
}, "t" + i).start();
}
}
static class Utils {
public static final ThreadLocal<Connection> tl = new ThreadLocal<>();
public static Connection getConnection() {
Connection conn = tl.get();
if (conn == null) {
conn = innerGetConnection();
tl.set(conn);
}
return conn;
}
private static Connection innerGetConnection() {
try {
return DriverManager.getConnection("jdbc:mysql://localhost:3306/yoshino?useSSL=false", "root", "root");
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
}
大致设计
上面的代码很容易就看懂了,问题在于,ThreadLocal是怎么做到不同的线程存放不同的对象,而同一个线程能取出同样的对象的呢? 其实当你看到它使用的是set和get方法的时候,你就应该大致能猜到,他的底层应该有一个Map结构来存储线程以及这个线程中的资源,当然,这是JDK早期的设计了,下面是早期的设计 在JDK8中,ThreadLocal的结构已经改变了。 可以发现,我们并不是在ThreadLocal中存储我们的资源,而是在每一个线程中存储,由于每一个线程对象都有一个ThreadLocalMap的局部变量,因此每一个线程之间的ThreadLocalMap都是互相隔离的,所以同一个线程能访问到同一个ThreadLocalMap对象中的数据。我们使用ThreadLocal的set方法的时候其实是向Thread中的ThreadLocalMap设置值。那么下次我们调用ThreadLocal的get方法的时候,他其实会先获取当前线程对象,然后使用这个线程对象去访问ThreadLocalMap中的数据。
只有在你第一次使用这个Map集合的时候,他才会创建,也就是它使用的是懒加载。
因此ThreadLocal的原理是, 每一个线程对象中都有一个ThreadLocalMap类型的成员变量,用来存储资源对象。这个Map的key是ThreadLocal对象,value才是真正要存储的资源。 具体的过程是这样的: ( 1 ) 每个Thread线程内部都有一个Map (ThreadLocalMap) ( 2 ) Map里面存储ThreadLocal对象( key )和线程的变量副本 ( value ) ( 3 ) Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。 ( 4 )对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
那么现在的设计的好处在哪里呢?
- 每个Map存储的Entry变少,在早期的设计中key为Thread对象,而我们知道Thread对象的个数是很多的,而现在的设计key为ThreadLocal,我们一般设定ThreadLocal为static,所以能保证ThreadLocalMap存储的键值对更少。
- 现在在Thread销毁之后,ThreadLocalMap也会自动销毁,减少对内存的时候。而早期的设计,即使线程消失了,ThreadLocalMap依旧存在,还是要维护它。
方法介绍
- 调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的ThreadLocalMap集合中
- 调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值
- 调用remove方法,就是以ThreadLocal自己作为key,移除当前线程关联的资源值
set方法 执行流程: 首先获取当前线程,并根据当前线程获取一个Map 如果获取的Map不为空,则将次数设置到Map中 如果Map为空,则给该线程创建Map,并设置初始值
java
public void set(T value) {
//获取当前线程对象
Thread t = Thread.currentThread();
//获取此线程对象中的Map对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//已经创建那么直接放入此entry
map.set(this, value);
} else {
//不存在则创建 并将当前线程t和value值作为第一个entry存放到Map中
createMap(t, value);
}
}
//返回当前线程对应的Map
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//这里的this是调用createMap的ThreadLocal对象
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
get方法 执行流程: A.首先获取当前线程,根据当前线程获取一个Map B.如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D C.如果e不为null,则返回e.value,否则转到D D.Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map 总结:先获取当前线程的ThreadLocalMap变量,如果存在则返回值,不存在则创建并返回初始值。
java
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//以当前ThreadLocal为key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//初始化,有两种情况会执行这个代码
//map不存在,表示此线程没有维护ThreadLocalMap对象
//map存在,但是没有查询到与当前ThreadLocal关联的entry
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private T setInitialValue() {
//调用initialValue获取初始化的值
//子类可以重写,不重写返回null
T value = initialValue();
//获取当前线程对象
Thread t = Thread.currentThread();
//获取当前线程对象维护的Map
ThreadLocalMap map = getMap(t);
if (map != null) { //map存在设置实体entry
map.set(this, value);
} else {
//1:当前线程不存在ThreadLocalMap对象
//2:则调用这个方法创建Map
//并且将t和value设置进去
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
remove方法 执行流程: 首先获取当前线程,并且根据该线程获取一个Map 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry
java
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
initialValue方法 ( 1 )这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。 ( 2 )这个方法缺省实现直接返回一个null。 ( 3 )如果想要一个除null之外的初始值,可以重写此方法。(备注∶该方法是一个protected的方法显然是为了让子类覆盖而设计的)
java
返回当前线程对应的ThreadLocal的初始值
此方法的第一次调用发生在,当线程通过get方法访问此钱程的
ThreadLocal值时除非线程先调用了set方法,在这种情况下,
initialvalue 才不会被这个线程调用。通常情况下,每个线程最多调用一次这个方法。
这个方法仅仅简单的返回nu1l {@code nu11};
如果程序员想ThreadLoca1线程局部变量有一个除nu11以外的初始值,
必须通过子类继承{@code ThreadLocaT]的方式去重写此方法
通常,可以通过匿名内部类的方式实现
protected T initialValue() {
return null;
}
底层理解
ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map功能,其内部的Entry也是独立实现的。
在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。| 另外,Entry继承WeakReference,也就是key ( ThreadLocal)是弱引用,其目的是将ThreadLocal对象的生命周期和线程生命周期解绑。
java
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
//这里要注意的是它的entey的key必须是ThreadLocal
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 容量必须是2的n次幂
private static final int INITIAL_CAPACITY = 16;
//存放键值对的数组
private Entry[] table;
//table中元素个数
private int size = 0;
//扩容阈值
private int threshold; // Default to 0
}
下面是我对ThreadLocal的理解的结构图 首先我们知道,你使用ThreadLocal的时候,它的实例对象只有一个无参的get方法,也就是说你不能使用传统的Map结构去根据key获取value,而上面的ThreadLocalMap中的Entry结构也已经说明了,你使用那个ThreadLocal对象去调用这个get方法,那你就是获取那个ThreadLocal在某个Thread中的ThreadLocalMap中对应的value值。 如果你整个项目中只有一个ThreadLocal,但是有多个线程,那么这个ThreadLocal其实就是在不同的Thread中的ThreadLocalMap的key而已。 也就是其实ThreadLocal:Thread:ThreadLocalMap的关系为1:n:n,所以能做到在不同线程调用ThreadLocal对象的get或者set方法的时候,是对不同的Thread中的ThreadLocalMap对象获取和设置值。
如果还不理解,那么用下面的代码理解一下:
java
static class T {
public ThreadLocal<Object> tl1 = new ThreadLocal<>();
public ThreadLocal<Object> tl2 = new ThreadLocal<>();
}
public static void test3() {
T t = new T();
new Thread(()->{
t.tl1.set("t1线程设置的tl1");
t.tl2.set("t1线程设置的tl2");
System.out.println(t.tl1.get());
System.out.println(t.tl2.get());
},"t1").start();
new Thread(()->{
t.tl1.set("t2线程设置的tl1");
t.tl2.set("t2线程设置的tl2");
System.out.println(t.tl1.get());
System.out.println(t.tl2.get());
},"t2").start();
}
可以发现我在同一个线程中使用了多个ThreadLocal,但是即使是同一个线程,使用不同的ThreadLocal去去获取数据,得到的数据也是不同的。这也就是因为对于同一个Thread,他其实只有一个Map,但是这个Map的结构要求他的key必须是ThreadLocal类型,而值随意。因此你能做到如果使用不同的ThreadLocal作为key,那么就能取得不同的值。此时我们就可以得到下面的图: 而如果我们使用不同的ThreadLocal作为key,那么他们也是通过hash方法来找到对应的数组下标的。
因此,我们使用线程池的时候,按照上面的原理,由于这些线程被复用,所以如果我们在使用线程池中的线程的时候,如果这个对象中的ThreadLocalMap没有被清理,就可能导致我们会得到上一次的值,也就是"前世"。所以我们在使用ThreadLocal的时候要求使用完毕应该调用remove方法来清空ThreadLocalMap中的数据。当然,我们也可以利用线程池的这一点,来减少我们new对象的次数,来复用这些对象。
【Java面试】说说你对ThreadLocal内存泄漏问题的理解
hash冲突的解决
hash冲突的解决是Map中的一个重要内容。 而ThreadLocal也是使用了Map结构,因此也需要解决hash冲突。 首先看一下ThreadLocalMap的构造函数
java
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
//获取hash值
private final int threadLocalHashCode = nextHashCode();
//原子类
private static AtomicInteger nextHashCode =
new AtomicInteger();
//hash值增量
private static final int HASH_INCREMENT = 0x61c88647;
//返回下一个hash值
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
构造函数首先创建了一个长度为16的Entry数组,然后计算出firstKey对应的索引,然后存储到table中,并且设置size和thresholad。
这里定义了一个AtomicInteger类型,每次获取当前值并加上HASH_INCREMENT, HASH_INCREMENT等于0x61c88647,这个值跟斐波那契数列(黄金分割数)有关,其主要目的就是为了让哈希码能均匀的分布在2的n次方的数组里,也就是Entry[] table中,这样做可以尽量避免hash冲突。 关于 & (INITIAL_CAPACITY - 1) 计算hash的时候里面采用了hashCode & (size - 1)的算法,这相当于取模运算hashCode % size的一个更高效的实现。正是因为这种算法,我们要求size必须是2的整次幂,这也能保证保证在索引不越界的前提下,使得hash发生冲突的次数减小。
java
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
//计算索引
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//如果已经有这个key则直接覆盖
if (k == key) {
e.value = value;
return;
}
//key为null,但是值补位null,说明ThreadLocal对象已经被回收了
//当前数组中的Entry是一个陈旧的stale元素
if (k == null) {
//用新元素替换旧的,这个方法做了很多垃圾回收操作,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
//key不存在且没有旧元素,直接在空元素位置创建一个新的Entry
tab[i] = new Entry(key, value);
int sz = ++size;
//cleanSomeSlots用于清除e.get()==null的元素
//这种数据key关联的对象已经被回收,所以此时可以把对应的位置设置为null
//如果没有清除任何entry,表示当前使用量达到了负载因子,2/3,
//那么就rehash(会执行一次全表的扫描操作)
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
//线性探测
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
代码执行流程: A.首先还是根据key计算出索引 i ,然后查找 i 位置上的Entry , B.若是Entry已经存在并且key等于传入的key,那么这时候直接给这个Entry赋新的value值, C.若是Entry存在,但是key为null,则调用replaceStaleEntry来更换这个key为空的Entry, D.不断循环检测,直到遇到为null的地方,这时候要是还没在循环过程中return,那么就在这个null的位置新建一个Entry,并且插入,同时size增加1。 最后调用cleanSomeSlots,清理key为null的Entry,最后返回是否清理了Entry,接下来再判断sz是否>=threshold达到了rehash的条件,达到的话就会调用rehash函数执行一次全表的扫描清理。
重点分析: ThreadLocalMap使用线性探测法来解决哈希冲突的。 该方法一次探测下一地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则产生溢出。 举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。 按照上面的描述,可以把Entry[] table看成一个环形数组。
get/set/remove方法的一些细节
对于get方法,如果get的时候发现key是null,也就是此时ThreadLocal已经被回收了,那么此时就会把对应的value也设定为空来释放空间,但是会把key再次设定为当前ThreadLocal。
对于set方法,如果发现set的时候得到的索引处的key为null,那么说明已经被回收掉了,那么此时会把数据放入进去,然后使用一种启发式扫描,他会扫描邻近的key是否为null,如果为null就进行对value的清理。相比全表扫描效率更高。启发次数与元素个数,是否发现null key有关。
对于get和set方法,他们只有在没有引用key(null key)的时候才会触发垃圾回收。 但是我们使用ThreadLocal一般都是静态的,所以这个ThreadLocal一般不会被回收,也就是他是一个强引用,所以一般不会出现null key的情况。
因此一般我们都使用remove方法,在某一个key不使用的时候,手动使用remove方法来设定其为null。
总结
每个线程都可以独立修改属于自己的副本而不会互相影响,从而隔离了线程和线程. 避免了线程访问实例变量发生安全问题. 同时我们也能得出下面的结论: (1)ThreadLocal只是操作Thread中的ThreadLocalMap对象的集合; (2)ThreadLocalMap变量属于线程的内部属性,不同的线程拥有完全不同的ThreadLocalMap变量; (3)线程中的ThreadLocalMap变量的值是在ThreadLocal对象进行set或者get操作时创建的; (4)使用当前线程的ThreadLocalMap的关键在于使用当前的ThreadLocal的实例作为key来存储value 值; (5) ThreadLocal模式至少从两个方面完成了数据访问隔离,即纵向隔离(线程与线程之间的 ThreadLocalMap不同)和横向隔离(不同的ThreadLocal实例之间的互相隔离); (6)一个线程中的所有的局部变量其实存储在该线程自己的同一个map属性中; (7)线程死亡时,线程局部变量会自动回收内存; (8)线程局部变量时通过一个 Entry 保存在map中,该Entry 的key是一个 WeakReference包装的 ThreadLocal, value为线程局部变量,key 到 value 的映射是通过: ThreadLocal.threadLocalHashCode & (INITIAL_CAPACITY - 1) 来完成的; (9)当线程拥有的局部变量超过了容量的2/3(没有扩大容量时是10个),会涉及到ThreadLocalMap中 Entry的回收;
对于多线程资源共享的问题,同步机制采用了"以时间换空间"的方式,而ThreadLocal采用了"以空间换 时间"的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量, 因此可以同时访问而互不影响。