【转载】计算机程序的思维逻辑 (82) - 理解ThreadLocal
本节,我们来探讨一个特殊的概念,线程本地变量,在Java中的实现是类ThreadLocal,它是什么?有什么用?实现原理是什么?让我们接下来逐步探讨。
基本概念和用法
线程本地变量是说,每个线程都有同一个变量的独有拷贝,这个概念听上去比较难以理解,我们先直接来看类TheadLocal的用法。
ThreadLocal是一个泛型类,接受一个类型参数T,它只有一个空的构造方法,有两个主要的public方法:
public T get()
public void set(T value)
set就是设置值,get就是获取值,如果没有值,返回null,看上去,ThreadLocal就是一个单一对象的容器,比如:
public static void main(String[] args) {
ThreadLocal<Integer> local = new ThreadLocal<>();
local.set(100);
System.out.println(local.get());
}
输出为100。
那ThreadLocal有什么特殊的呢?特殊发生在有多个线程的时候,看个例子:

public class ThreadLocalBasic {
static ThreadLocal<Integer> local = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread child = new Thread() {
@Override
public void run() {
System.out.println("child thread initial: " + local.get());
local.set(200);
System.out.println("child thread final: " + local.get());
}
};
local.set(100);
child.start();
child.join();
System.out.println("main thread final: " + local.get());
}
}

local是一个静态变量,main方法创建了一个子线程child,main和child都访问了local,程序的输出为:
child thread initial: null
child thread final: 200
main thread final: 100
这说明,main线程对local变量的设置对child线程不起作用,child线程对local变量的改变也不会影响main线程,它们访问的虽然是同一个变量local,但每个线程都有自己的独立的值,这就是线程本地变量的含义。
除了get/set,ThreadLocal还有两个方法:
protected T initialValue()
public void remove()
initialValue用于提供初始值,它是一个受保护方法,可以通过匿名内部类的方式提供,当调用get方法时,如果之前没有设置过,会调用该方法获取初始值,默认实现是返回null。remove删掉当前线程对应的值,如果删掉后,再次调用get,会再调用initialValue获取初始值。看个简单的例子:

public class ThreadLocalInit {
static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 100;
}
};
public static void main(String[] args) {
System.out.println(local.get());
local.set(200);
local.remove();
System.out.println(local.get());
}
}

输出值都是100。
使用场景
ThreadLocal有什么用呢?我们来看几个例子。
DateFormat/SimpleDateFormat
ThreadLocal是实现线程安全的一种方案,比如对于DateFormat/SimpleDateFormat,我们在32节介绍过日期和时间操作,提到它们是非线程安全的,实现安全的一种方式是使用锁,另一种方式是每次都创建一个新的对象,更好的方式就是使用ThreadLocal,每个线程使用自己的DateFormat,就不存在安全问题了,在线程的整个使用过程中,只需要创建一次,又避免了频繁创建的开销,示例代码如下:

public class ThreadLocalDateFormat {
static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
public static String date2String(Date date) {
return sdf.get().format(date);
}
public static Date string2Date(String str) throws ParseException {
return sdf.get().parse(str);
}
}

需要说明的是,ThreadLocal对象一般都定义为static,以便于引用。
ThreadLocalRandom
即使对象是线程安全的,使用ThreadLocal也可以减少竞争,比如,我们在34节介绍过Random类,Random是线程安全的,但如果并发访问竞争激烈的话,性能会下降,所以Java并发包提供了类ThreadLocalRandom,它是Random的子类,利用了ThreadLocal,它没有public的构造方法,通过静态方法current获取对象,比如:
public static void main(String[] args) {
ThreadLocalRandom rnd = ThreadLocalRandom.current();
System.out.println(rnd.nextInt());
}
current方法的实现为:
public static ThreadLocalRandom current() {
return localRandom.get();
}
localRandom就是一个ThreadLocal变量:

private static final ThreadLocal<ThreadLocalRandom> localRandom =
new ThreadLocal<ThreadLocalRandom>() {
protected ThreadLocalRandom initialValue() {
return new ThreadLocalRandom();
}
};

上下文信息
ThreadLocal的典型用途是提供上下文信息,比如在一个Web服务器中,一个线程执行用户的请求,在执行过程中,很多代码都会访问一些共同的信息,比如请求信息、用户身份信息、数据库连接、当前事务等,它们是线程执行过程中的全局信息,如果作为参数在不同代码间传递,代码会很啰嗦,这时,使用ThreadLocal就很方便,所以它被用于各种框架如Spring中,我们看个简单的示例:

public class RequestContext {
public static class Request { //...
};
private static ThreadLocal<String> localUserId = new ThreadLocal<>();
private static ThreadLocal<Request> localRequest = new ThreadLocal<>();
public static String getCurrentUserId() {
return localUserId.get();
}
public static void setCurrentUserId(String userId) {
localUserId.set(userId);
}
public static Request getCurrentRequest() {
return localRequest.get();
}
public static void setCurrentRequest(Request request) {
localRequest.set(request);
}
}

在首次获取到信息时,调用set方法如setCurrentRequest/setCurrentUserId进行设置,然后就可以在代码的任意其他地方调用get相关方法进行获取了。
基本实现原理
ThreadLocal是怎么实现的呢?为什么对同一个对象的get/set,每个线程都能有自己独立的值呢?我们直接来看代码。
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);
}

它调用了getMap,getMap的代码为:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
返回线程的实例变量threadLocals,它的初始值为null,在null时,set调用createMap初始化,代码为:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
从以上代码可以看出,每个线程都有一个Map,类型为ThreadLocalMap,调用set实际上是在线程自己的Map里设置了一个条目,键为当前的ThreadLocal对象,值为value。ThreadLocalMap是一个内部类,它是专门用于ThreadLocal的,与一般的Map不同,它的键类型为WeakReference<ThreadLocal>,我们没有提过WeakReference,它与Java的垃圾回收机制有关,使用它,便于回收内存,具体我们就不探讨了。
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();
}

通过线程访问到Map,以ThreadLocal对象为键从Map中获取到条目,取其value,如果Map中没有,调用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;
}

initialValue()就是之前提到的提供初始值的方法,默认实现就是返回null。
remove方法的代码也很直接,如下所示:
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
简单总结下,每个线程都有一个Map,对于每个ThreadLocal对象,调用其get/set实际上就是以ThreadLocal对象为键读写当前线程的Map,这样,就实现了每个线程都有自己的独立拷贝的效果。
线程池与ThreadLocal
我们在78节介绍过线程池,我们知道,线程池中的线程是会重用的,如果异步任务使用了ThreadLocal,会出现什么情况呢?可能是意想不到的,我们看个简单的示例:

public class ThreadPoolProblem {
static ThreadLocal<AtomicInteger> sequencer = new ThreadLocal<AtomicInteger>() {
@Override
protected AtomicInteger initialValue() {
return new AtomicInteger(0);
}
};
static class Task implements Runnable {
@Override
public void run() {
AtomicInteger s = sequencer.get();
int initial = s.getAndIncrement();
// 期望初始为0
System.out.println(initial);
}
}
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.execute(new Task());
executor.execute(new Task());
executor.execute(new Task());
executor.shutdown();
}
}

对于异步任务Task而言,它期望的初始值应该总是0,但运行程序,结果却为:
0
0
1
第三次执行异步任务,结果就不对了,为什么呢?因为线程池中的线程在执行完一个任务,执行下一个任务时,其中的ThreadLocal对象并不会被清空,修改后的值带到了下一个异步任务。那怎么办呢?有几种思路:
- 第一次使用ThreadLocal对象时,总是先调用set设置初始值,或者如果ThreaLocal重写了initialValue方法,先调用remove
- 使用完ThreadLocal对象后,总是调用其remove方法
- 使用自定义的线程池
我们分别来看下,对于第一种,在Task的run方法开始处,添加set或remove代码,如下所示:

static class Task implements Runnable {
@Override
public void run() {
sequencer.set(new AtomicInteger(0));
//或者 sequencer.remove();
AtomicInteger s = sequencer.get();
//...
}
}

对于第二种,将Task的run方法包裹在try/finally中,并在finally语句中调用remove,如下所示:

static class Task implements Runnable {
@Override
public void run() {
try{
AtomicInteger s = sequencer.get();
int initial = s.getAndIncrement();
// 期望初始为0
System.out.println(initial);
}finally{
sequencer.remove();
}
}
}

以上两种方法都比较麻烦,需要更改所有异步任务的代码,另一种方法是扩展线程池ThreadPoolExecutor,它有一个可以扩展的方法:
protected void beforeExecute(Thread t, Runnable r) { }
在线程池将任务r交给线程t执行之前,会在线程t中先执行beforeExecure,可以在这个方法中重新初始化ThreadLocal。如果知道所有需要初始化的ThreadLocal变量,可以显式初始化,如果不知道,也可以通过反射,重置所有ThreadLocal,反射的细节我们会在后续章节进一步介绍。
我们创建一个自定义的线程池MyThreadPool,示例代码如下:

static class MyThreadPool extends ThreadPoolExecutor {
public MyThreadPool(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected void beforeExecute(Thread t, Runnable r) {
try {
//使用反射清空所有ThreadLocal
Field f = t.getClass().getDeclaredField("threadLocals");
f.setAccessible(true);
f.set(t, null);
} catch (Exception e) {
e.printStackTrace();
}
super.beforeExecute(t, r);
}
}

这里,使用反射,找到线程中存储ThreadLocal对象的Map变量threadLocals,重置为null。使用MyThreadPool的示例代码如下:

public static void main(String[] args) {
ExecutorService executor = new MyThreadPool(2, 2, 0,
TimeUnit.MINUTES, new LinkedBlockingQueue<Runnable>());
executor.execute(new Task());
executor.execute(new Task());
executor.execute(new Task());
executor.shutdown();
}

使用以上介绍的任意一种解决方案,结果就符合期望了。
小结
本节介绍了ThreadLocal的基本概念、用法用途、实现原理、以及和线程池结合使用时的注意事项,简单总结来说:
- ThreadLocal使得每个线程对同一个变量有自己的独立拷贝,是实现线程安全、减少竞争的一种方案。
- ThreadLocal经常用于存储上下文信息,避免在不同代码间来回传递,简化代码。
- 每个线程都有一个Map,调用ThreadLocal对象的get/set实际就是以ThreadLocal对象为键读写当前线程的该Map。
- 在线程池中使用ThreadLocal,需要注意,确保初始值是符合期望的。
从65节到现在,我们一直在探讨并发,至此,基本就结束了,下一节,让我们一起简要回顾总结一下。
【转载】计算机程序的思维逻辑 (82) - 理解ThreadLocal的更多相关文章
- 计算机程序的思维逻辑 (82) - 理解ThreadLocal
本节,我们来探讨一个特殊的概念,线程本地变量,在Java中的实现是类ThreadLocal,它是什么?有什么用?实现原理是什么?让我们接下来逐步探讨. 基本概念和用法 线程本地变量是说,每个线程都有同 ...
- Java编程的逻辑 (82) - 理解ThreadLocal
本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...
- 计算机程序的思维逻辑 (66) - 理解synchronized
上节我们提到了多线程共享内存的两个问题,一个是竞态条件,另一个是内存可见性,我们提到,解决这两个问题的一个方案是使用synchronized关键字,本节就来讨论这个关键字. 用法 synchroniz ...
- 计算机程序的思维逻辑 (8) - char的真正含义
看似简单的char 通过前两节,我们应该对字符和文本的编码和乱码有了一个清晰的认识,但前两节都是与编程语言无关的,我们还是不知道怎么在程序中处理字符和文本. 本节讨论在Java中进行字符处理的基础 - ...
- 计算机程序的思维逻辑 (29) - 剖析String
上节介绍了单个字符的封装类Character,本节介绍字符串类.字符串操作大概是计算机程序中最常见的操作了,Java中表示字符串的类是String,本节就来详细介绍String. 字符串的基本使用是比 ...
- 计算机程序的思维逻辑 (64) - 常见文件类型处理: 属性文件/CSV/EXCEL/HTML/压缩文件
对于处理文件,我们介绍了流的方式,57节介绍了字节流,58节介绍了字符流,同时,也介绍了比较底层的操作文件的方式,60节介绍了随机读写文件,61节介绍了内存映射文件,我们也介绍了对象的序列化/反序列化 ...
- 【转载】计算机程序的思维逻辑 (8) - char的真正含义
看似简单的char 通过前两节,我们应该对字符和文本的编码和乱码有了一个清晰的认识,但前两节都是与编程语言无关的,我们还是不知道怎么在程序中处理字符和文本. 本节讨论在Java中进行字符处理的基础 - ...
- 计算机程序的思维逻辑 (31) - 剖析Arrays
数组是存储多个同类型元素的基本数据结构,数组中的元素在内存连续存放,可以通过数组下标直接定位任意元素,相比我们在后续章节介绍的其他容器,效率非常高. 数组操作是计算机程序中的常见基本操作,Java中有 ...
- 计算机程序的思维逻辑 (73) - 并发容器 - 写时拷贝的List和Set
本节以及接下来的几节,我们探讨Java并发包中的容器类.本节先介绍两个简单的类CopyOnWriteArrayList和CopyOnWriteArraySet,讨论它们的用法和实现原理.它们的用法比较 ...
随机推荐
- 登录密码忘记修改jenkins
find / -type f -name 'config.xml' 然后需要删除config.xml文件中的以下内容: <useSecurity>true</useSecurity& ...
- 小小知识点(十八)U盘中病毒了,System Volume Information文件夹删除不掉
win+R调出命令窗口后搜索cmd,启用cmd命令编辑器,并输入以下命令: attrib "H:\System Volume Information" -s //这句话可以选择 ...
- asp.net core 实现支持多语言
asp.net core 实现支持多语言 Intro 最近有一个外国友人通过邮件联系我,想用我的活动室预约,但是还没支持多语言,基本上都是写死的中文,所以最近想支持一下更多语言,于是有了多语言方面的一 ...
- JavaScript 继承小记
面向对象编程很重要的一个方面,就是对象的继承.A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法.这对于代码的复用是非常有用的. 大部分面向对象的编程语言,都是通过“类”(class) ...
- Git 合并多次提交
在合并分支的时候,希望将多次提交合并成一个,然后再 cherry-pick 到主分支. 合并分支 develop 分支做开发,可能会进行多次提交,但是在发布或者进行 PR 的时候,我们只希望看到一次提 ...
- 79.纯 CSS 创作单元素麦当劳金拱门 Logo(原文)
1. 效果图: 效果地址:https://codepen.io/flyingliao/pen/JgavjX 原理:m是伪元素::before弄出来的,::after遮挡中间下方一小块. 感想:学到一个 ...
- python集合的运算
& 交集 | 并集 - 差集 ^ 异或集 # 在对集合做运算时,不会影响原来的集合,而是返回一个运算结果 # 创建两个集合 s = {1,2,3,4,5} s2 = {3,4,5, ...
- 如何实施DevOps
对于长期在孤立的架构下工作的组织来说,转移到协作式DevOps系统似乎是难以成功的.为了进一步提高效率,必须改变观念,并进行团队文化改变.例如:许多人认为只有自动化工具才能解决DevOps,其实这是不 ...
- play framework 相关
1.下载 官网下载解压,安装有jkd即可使用 2.helloworld $ activator new my-first-app play-java https://www.playframework ...
- 吸取教训:一段网上找的代码突然爆了,项目出现大BUG
本人是做游戏服务器开发的,碰到一个需求,给符某些要求的玩家的发送道具奖励,奖励的数量根据离线的天数计算. 这个需求实现起来很简单,只需要在玩家上线的时候计算上次离线时间和当前时间间隔的天数,然后根据策 ...