@
文章导航
在多线程情况下,对于一个共享变量或者资源对象进行读或者写操作时,就必须考虑线程安全问题。而ThreadLocal采用的是完全相反的方式来解决线程安全问题。他实现了对资源对象的线程隔离,让每个线程各自使用各自的资源对象,避免争用引发的线程安全问题。ThreadLocal同时实现了线程内的资源共享。 例如方法1对ThreadLocal中的变量进行了设置,那么方法2中只要是同一个线程,那么他也能访问到线程1在ThreadLocal中设置的变量。
ThreadLocal原理
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 {
System.out.println(Utils.getConnection());
}, "t" + i).start();
}
}
public static void test2() {
for (int i = 0; i {
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 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,并设置初始值
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变量,如果存在则返回值,不存在则创建并返回初始值。
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
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
initialValue方法
( 1 )这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
( 2 )这个方法缺省实现直接返回一个null。
( 3 )如果想要一个除null之外的初始值,可以重写此方法。(备注∶该方法是一个protected的方法显然是为了让子类覆盖而设计的)
返回当前线程对应的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对象的生命周期和线程生命周期解绑。
static class ThreadLocalMap {
static class Entry extends WeakReference 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采用了“以空间换
时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,
因此可以同时访问而互不影响。