
加入读书践行群,每天一个知识点,持续精进!


这是程序员chatbook第97篇原创
今日难度系数 :🌟🌟🌟
预计阅读时间 : 5 分钟

00、引用
ThreadLocal,即线程变量,是一个以ThreadLocal对象为key、任意对象为值的键-值对存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。一般地,可以通过set()方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。
举例:我们同样以学生时代大家在澡堂洗澡的经历为例进行说明。
之前,有1000个同学同时去洗澡,但是澡堂只有1个洗澡喷头,这意味着我们需要保证这1000个同学不能去哄抢这个喷头,不然大家都不能完成洗澡;最后,通过锁机制保证了1000个同学顺利完成了洗澡。这是大家熟知的“锁机制”。
后来,学校新购买了喷头共有1000个,这样就保证了每个同学都能有一个喷头,那么所有同学都可以同时洗澡,不用再抢喷头了。这就是今天我们要谈的“ThreadLocal机制”。
ThreadLocal机制并不是用于多线程之间进行数据共享,而是为线程提供了可以拥有自己局部变量的技术途径,该变量只有当前线程才能访问,进而也保证了线程安全的,这是典型的“用空间换时间”的思路。
01、ThreadLocal说明
ThreadLocal为每个使用该变量的线程提供独立的变量副本,每个变量都维护了自己的、完全独立的一个变量副本,所以每个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。
ThreadLocal<T>类的核心方法及功能介绍,详见代码1。
1
//设置当前线程的线程局部变量的值
public void set(T value)
//返回当前线程所对应的线程局部变量值
public T get()
//将当前线程局部变量的值删除。当线程结束后,对应该线程的局部变量将自动被GC回收
//所以,显式调用该方法清除线程的局部变量并不是必须的操作,但是可以加快内存回收的速度public void remove()
//返回当前线程局部变量的初始值,该方法是一个protected方法,是为了让子类覆盖而设计;
//该方法是一个延迟调用方法,在线程第一次调用get()或set(Object)方法时才执行,
//并且有且只执行一次。该方法缺省实现返回为nullprotected T initialValue()
为了说明如何使用ThreadLocal的核心API,最好的方式就是编写一个典型用例,详见代码2。
2
//ThreadLocal典型应用示例
public class ThreadLocalDemo {
//入职的第一天,单位给新员工办了一张ICBC卡
//为了保证办卡成功,预充值了10RMB
private static ThreadLocal<Integer> ICBCCard = new ThreadLocal<Integer>(){
public Integer initialValue(){
return 10;
}
};
//模拟发一笔工资
public int transfer(int income){
ICBCCard.set(ICBCCard.get() + income);
return ICBCCard.get();
}
//模拟发工资的场景
private static class PayOff extends Thread{
private ThreadLocalDemo demo;
public PayOff(ThreadLocalDemo t){
demo = t;
}
//将工资发放后的余额变化情况进行打印
public void report(int total){
System.out.println(Thread.currentThread().getName() + " => ICBC余额 : "+ total+"RMB");
}
//模拟发放多笔工资
public void run(){
//发通讯补助
int total = demo.transfer(1000);
report(total);
//发交通补助
total=demo.transfer(800);
report(total);
//发餐饮补助
total=demo.transfer(600);
report(total);
//发绩效奖金
//TODO
//很重要:每个线程用完以后都要执行删除操作
demo.getThreadLocl().remove();}
}
//测试客户端
public static void main(String[] args) {
ThreadLocalDemo tl = new ThreadLocalDemo();
PayOff user1 = new PayOff(tl);
user1.setName("员工1");
PayOff user2 = new PayOff(tl);
user2.setName("员工2");
PayOff user3 = new PayOff(tl);
user3.setName("员工3");
user1.start();
user2.start();
user3.start();
}
}
结果
员工3 => ICBC余额 : 1010RMB
员工2 => ICBC余额 : 1010RMB
员工1 => ICBC余额 : 1010RMB
员工2 => ICBC余额 : 1810RMB
员工3 => ICBC余额 : 1810RMB
员工2 => ICBC余额 : 2410RMB
员工1 => ICBC余额 : 1810RMB
员工3 => ICBC余额 : 2410RMB
员工1 => ICBC余额 : 2410RMB
从程序的输出结果可以非常清晰的看出:虽然每位员工在程序中是共享了ICBCCard,但是每位员工的收入都在独立的变化,并且互不影响,这是因为ThreadLocal为每个线程提供了单独的副本。
02、ThreadLocal实现机理
ThreadLocal实现线程隔离的秘密是什么了?
在揭开面纱之前,我们先来认识ThreadLocal实现中重要的数据结构——ThreadLocalMap。该数据结构可以理解为是一个类似HashMap的存储结构,其实现使用了弱引用。弱引用是比强引用弱得多的引用。JVM在GC时,一旦发现弱引用就会立即回收。ThreadLocalMap内部由一系列Entry构成,每个Entry都是WeakReference <ThreadLocal>,详细定义见代码3。
3
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
代码3中,参数k就是ThreadLocalMap的key,就是ThreadLocal的实例,作为弱引用使用;参数v就是ThreadLocalMap的value。
set与put操作是ThreadLocal的两个核心函数,下面我们结合这个两个函数的实现,来说明一下ThreadLocal的实现机理。其中set函数见代码3,put函数见代码4。
4
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取一个和当前线程相关的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
//获取一个和当前线程相关的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
在进行set操作时,首先获得当前线程对象,然后通过getMap(Thread t)得到和当前线程绑定的ThreadLocalMap,如果map不为空,则将变量值设置到这个ThreadLocalMap中;如果map为空,就通过createMap方法进行创建。这里我们要特别注意getMap函数,通过该函数我们知道ThreadLocalMap是定义在Thread中成员变量。
设置到ThreadLocal中的数据,都写入到了ThreadLocalMap中。其中key为ThreadLocal当前对象,value就是我们设置的值。而ThreadLocalMap本身就保持了当前自己所在线程的所有“局部变量”,也就是一个ThreadLocal变量的集合。
5
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}
/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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;
}
在进行get操作时,也是首先获取当前线程的ThreadLocalMap对象。然后,通过将自己作为key取得内部的实际数据。
通过对上述两个核心函数的讨论,下面我们来揭开ThreadLocal的线程隔离的秘密:其核心在于ThreadLocalMap类。ThreadLocalMap是ThreadLocal类的一个静态内部类,它实现了键值对的设置和获取,每个线程中都有一个独立的ThreadLocalMap副本,它所存储的值,只能被当前线程读取和修改。ThreadLocal类通过操作每一个线程所持有的ThreadLocalMap副本,从而实现了变量访问在不同线程中的隔离。因为每个线程的变量都是自己特有的,完全不会有并发错误。还有一点就是,ThreadLocalMap存储的键值对中的键是this对象指向的ThreadLocal对象,而值就是你所设置的对象了。
推而广之,我们可以创建不同的ThreadLocal实例来实现多个变量在不同线程间的访问隔离。根据ThreadLocalMap的机理,因为不同的ThreadLocal对象可作为不同键,相应的也可设置成相应的不同的值。通过ThreadLocal对象,在多线程中共享一个值和多个值的区别,可类比为在一个HashMap对象中存储一个键值对和多个键值对一样,其原理见图1。

图1 ThreadLocalMap原理
03、小结
本文详细讨论了ThreadLocal的用途、典型用法及其实现机理,欢迎老铁们在留言区写下你的感悟与大家共同交流。
【参考资料】
1 Java多线程编程实战指南, 黄文海, 中国工信出版集团。
2 实战Java高并发程序设计, 葛一鸣,郭超, 中国工信出版集团,电子工业出版社。
如果觉得文章有用,感谢老铁们转发分享,让更多的小伙伴建立连接!

程序员Chatbook

程序员都关注了,来不及解释,长按图片,快上车





