本系列文章经补充和完善,已修订整理成书《Java编程的逻辑》,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接http://item.jd.com/12299018.html


本节,我们来探讨一个特殊的概念,线程本地变量,在Java中的实现是类ThreadLocal,它是什么?有什么用?实现原理是什么?让我们接下来逐步探讨。

基本概念和用法

线程本地变量是说,每个线程都有同一个变量的独有拷贝,这个概念听上去比较难以理解,我们先直接来看类TheadLocal的用法。

ThreadLocal是一个泛型类,接受一个类型参数T,它只有一个空的构造方法,有两个主要的public方法:

  1. public T get()
  2. public void set(T value)

set就是设置值,get就是获取值,如果没有值,返回null,看上去,ThreadLocal就是一个单一对象的容器,比如:

  1. public static void main(String[] args) {
  2. ThreadLocal<Integer> local = new ThreadLocal<>();
  3. local.set(100);
  4. System.out.println(local.get());
  5. }

输出为100。

那ThreadLocal有什么特殊的呢?特殊发生在有多个线程的时候,看个例子:

  1. public class ThreadLocalBasic {
  2. static ThreadLocal<Integer> local = new ThreadLocal<>();
  3.  
  4. public static void main(String[] args) throws InterruptedException {
  5. Thread child = new Thread() {
  6. @Override
  7. public void run() {
  8. System.out.println("child thread initial: " + local.get());
  9. local.set(200);
  10. System.out.println("child thread final: " + local.get());
  11. }
  12. };
  13. local.set(100);
  14. child.start();
  15. child.join();
  16. System.out.println("main thread final: " + local.get());
  17. }
  18. }

local是一个静态变量,main方法创建了一个子线程child,main和child都访问了local,程序的输出为:

  1. child thread initial: null
  2. child thread final: 200
  3. main thread final: 100

这说明,main线程对local变量的设置对child线程不起作用,child线程对local变量的改变也不会影响main线程,它们访问的虽然是同一个变量local,但每个线程都有自己的独立的值,这就是线程本地变量的含义。

除了get/set,ThreadLocal还有两个方法:

  1. protected T initialValue()
  2. public void remove()

initialValue用于提供初始值,它是一个受保护方法,可以通过匿名内部类的方式提供,当调用get方法时,如果之前没有设置过,会调用该方法获取初始值,默认实现是返回null。remove删掉当前线程对应的值,如果删掉后,再次调用get,会再调用initialValue获取初始值。看个简单的例子:

  1. public class ThreadLocalInit {
  2. static ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
  3.  
  4. @Override
  5. protected Integer initialValue() {
  6. return 100;
  7. }
  8. };
  9.  
  10. public static void main(String[] args) {
  11. System.out.println(local.get());
  12. local.set(200);
  13. local.remove();
  14. System.out.println(local.get());
  15. }
  16. }

输出值都是100。

使用场景

ThreadLocal有什么用呢?我们来看几个例子。

DateFormat/SimpleDateFormat

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

  1. public class ThreadLocalDateFormat {
  2. static ThreadLocal<DateFormat> sdf = new ThreadLocal<DateFormat>() {
  3.  
  4. @Override
  5. protected DateFormat initialValue() {
  6. return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  7. }
  8. };
  9.  
  10. public static String date2String(Date date) {
  11. return sdf.get().format(date);
  12. }
  13.  
  14. public static Date string2Date(String str) throws ParseException {
  15. return sdf.get().parse(str);
  16. }
  17. }

需要说明的是,ThreadLocal对象一般都定义为static,以便于引用。

ThreadLocalRandom

即使对象是线程安全的,使用ThreadLocal也可以减少竞争,比如,我们在34节介绍过Random类,Random是线程安全的,但如果并发访问竞争激烈的话,性能会下降,所以Java并发包提供了类ThreadLocalRandom,它是Random的子类,利用了ThreadLocal,它没有public的构造方法,通过静态方法current获取对象,比如:

  1. public static void main(String[] args) {
  2. ThreadLocalRandom rnd = ThreadLocalRandom.current();
  3. System.out.println(rnd.nextInt());
  4. }

current方法的实现为:

  1. public static ThreadLocalRandom current() {
  2. return localRandom.get();
  3. }

localRandom就是一个ThreadLocal变量:

  1. private static final ThreadLocal<ThreadLocalRandom> localRandom =
  2. new ThreadLocal<ThreadLocalRandom>() {
  3. protected ThreadLocalRandom initialValue() {
  4. return new ThreadLocalRandom();
  5. }
  6. };

上下文信息

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

  1. public class RequestContext {
  2. public static class Request { //...
  3. };
  4.  
  5. private static ThreadLocal<String> localUserId = new ThreadLocal<>();
  6. private static ThreadLocal<Request> localRequest = new ThreadLocal<>();
  7.  
  8. public static String getCurrentUserId() {
  9. return localUserId.get();
  10. }
  11.  
  12. public static void setCurrentUserId(String userId) {
  13. localUserId.set(userId);
  14. }
  15.  
  16. public static Request getCurrentRequest() {
  17. return localRequest.get();
  18. }
  19.  
  20. public static void setCurrentRequest(Request request) {
  21. localRequest.set(request);
  22. }
  23. }

在首次获取到信息时,调用set方法如setCurrentRequest/setCurrentUserId进行设置,然后就可以在代码的任意其他地方调用get相关方法进行获取了。

基本实现原理

ThreadLocal是怎么实现的呢?为什么对同一个对象的get/set,每个线程都能有自己独立的值呢?我们直接来看代码。

set方法的代码为:

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null)
  5. map.set(this, value);
  6. else
  7. createMap(t, value);
  8. }

它调用了getMap,getMap的代码为:

  1. ThreadLocalMap getMap(Thread t) {
  2. return t.threadLocals;
  3. }

返回线程的实例变量threadLocals,它的初始值为null,在null时,set调用createMap初始化,代码为:

  1. void createMap(Thread t, T firstValue) {
  2. t.threadLocals = new ThreadLocalMap(this, firstValue);
  3. }

从以上代码可以看出,每个线程都有一个Map,类型为ThreadLocalMap,调用set实际上是在线程自己的Map里设置了一个条目,键为当前的ThreadLocal对象,值为value。ThreadLocalMap是一个内部类,它是专门用于ThreadLocal的,与一般的Map不同,它的键类型为WeakReference<ThreadLocal>,我们没有提过WeakReference,它与Java的垃圾回收机制有关,使用它,便于回收内存,具体我们就不探讨了。

get方法的代码为:

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. if (e != null)
  7. return (T)e.value;
  8. }
  9. return setInitialValue();
  10. }

通过线程访问到Map,以ThreadLocal对象为键从Map中获取到条目,取其value,如果Map中没有,调用setInitialValue,其代码为:

  1. private T setInitialValue() {
  2. T value = initialValue();
  3. Thread t = Thread.currentThread();
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null)
  6. map.set(this, value);
  7. else
  8. createMap(t, value);
  9. return value;
  10. }

initialValue()就是之前提到的提供初始值的方法,默认实现就是返回null。

remove方法的代码也很直接,如下所示:

  1. public void remove() {
  2. ThreadLocalMap m = getMap(Thread.currentThread());
  3. if (m != null)
  4. m.remove(this);
  5. }

简单总结下,每个线程都有一个Map,对于每个ThreadLocal对象,调用其get/set实际上就是以ThreadLocal对象为键读写当前线程的Map,这样,就实现了每个线程都有自己的独立拷贝的效果。

线程池与ThreadLocal

我们在78节介绍过线程池,我们知道,线程池中的线程是会重用的,如果异步任务使用了ThreadLocal,会出现什么情况呢?可能是意想不到的,我们看个简单的示例:

  1. public class ThreadPoolProblem {
  2. static ThreadLocal<AtomicInteger> sequencer = new ThreadLocal<AtomicInteger>() {
  3.  
  4. @Override
  5. protected AtomicInteger initialValue() {
  6. return new AtomicInteger(0);
  7. }
  8. };
  9.  
  10. static class Task implements Runnable {
  11.  
  12. @Override
  13. public void run() {
  14. AtomicInteger s = sequencer.get();
  15. int initial = s.getAndIncrement();
  16. // 期望初始为0
  17. System.out.println(initial);
  18. }
  19. }
  20.  
  21. public static void main(String[] args) {
  22. ExecutorService executor = Executors.newFixedThreadPool(2);
  23. executor.execute(new Task());
  24. executor.execute(new Task());
  25. executor.execute(new Task());
  26. executor.shutdown();
  27. }
  28. }

对于异步任务Task而言,它期望的初始值应该总是0,但运行程序,结果却为:

  1. 0
  2. 0
  3. 1

第三次执行异步任务,结果就不对了,为什么呢?因为线程池中的线程在执行完一个任务,执行下一个任务时,其中的ThreadLocal对象并不会被清空,修改后的值带到了下一个异步任务。那怎么办呢?有几种思路:

  1. 第一次使用ThreadLocal对象时,总是先调用set设置初始值,或者如果ThreaLocal重写了initialValue方法,先调用remove
  2. 使用完ThreadLocal对象后,总是调用其remove方法
  3. 使用自定义的线程池

我们分别来看下,对于第一种,在Task的run方法开始处,添加set或remove代码,如下所示:

  1. static class Task implements Runnable {
  2.  
  3. @Override
  4. public void run() {
  5. sequencer.set(new AtomicInteger(0));
  6. //或者 sequencer.remove();
  7.  
  8. AtomicInteger s = sequencer.get();
  9. //...
  10. }
  11. }

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

  1. static class Task implements Runnable {
  2.  
  3. @Override
  4. public void run() {
  5. try{
  6. AtomicInteger s = sequencer.get();
  7. int initial = s.getAndIncrement();
  8. // 期望初始为0
  9. System.out.println(initial);
  10. }finally{
  11. sequencer.remove();
  12. }
  13. }
  14. }

以上两种方法都比较麻烦,需要更改所有异步任务的代码,另一种方法是扩展线程池ThreadPoolExecutor,它有一个可以扩展的方法:

  1. protected void beforeExecute(Thread t, Runnable r) { }

在线程池将任务r交给线程t执行之前,会在线程t中先执行beforeExecure,可以在这个方法中重新初始化ThreadLocal。如果知道所有需要初始化的ThreadLocal变量,可以显式初始化,如果不知道,也可以通过反射,重置所有ThreadLocal,反射的细节我们会在后续章节进一步介绍。

我们创建一个自定义的线程池MyThreadPool,示例代码如下:

  1. static class MyThreadPool extends ThreadPoolExecutor {
  2. public MyThreadPool(int corePoolSize, int maximumPoolSize,
  3. long keepAliveTime, TimeUnit unit,
  4. BlockingQueue<Runnable> workQueue) {
  5. super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
  6. }
  7.  
  8. @Override
  9. protected void beforeExecute(Thread t, Runnable r) {
  10. try {
  11. //使用反射清空所有ThreadLocal
  12. Field f = t.getClass().getDeclaredField("threadLocals");
  13. f.setAccessible(true);
  14. f.set(t, null);
  15. } catch (Exception e) {
  16. e.printStackTrace();
  17. }
  18. super.beforeExecute(t, r);
  19. }
  20. }

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

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

使用以上介绍的任意一种解决方案,结果就符合期望了。

小结

本节介绍了ThreadLocal的基本概念、用法用途、实现原理、以及和线程池结合使用时的注意事项,简单总结来说:

  • ThreadLocal使得每个线程对同一个变量有自己的独立拷贝,是实现线程安全、减少竞争的一种方案。
  • ThreadLocal经常用于存储上下文信息,避免在不同代码间来回传递,简化代码。
  • 每个线程都有一个Map,调用ThreadLocal对象的get/set实际就是以ThreadLocal对象为键读写当前线程的该Map。
  • 在线程池中使用ThreadLocal,需要注意,确保初始值是符合期望的。

65节到现在,我们一直在探讨并发,至此,基本就结束了,下一节,让我们一起简要回顾总结一下。

(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic,另外,与之前章节一样,本节代码基于Java 7, Java 8有些变动,我们会在后续章节统一介绍Java 8的更新)

----------------

未完待续,查看最新文章,敬请关注微信公众号“老马说编程”(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索Java编程及计算机技术的本质。用心原创,保留所有版权。

Java编程的逻辑 (82) - 理解ThreadLocal的更多相关文章

  1. 计算机程序的思维逻辑 (82) - 理解ThreadLocal

    本节,我们来探讨一个特殊的概念,线程本地变量,在Java中的实现是类ThreadLocal,它是什么?有什么用?实现原理是什么?让我们接下来逐步探讨. 基本概念和用法 线程本地变量是说,每个线程都有同 ...

  2. Java编程的逻辑 (66) - 理解synchronized

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  3. 【转载】计算机程序的思维逻辑 (82) - 理解ThreadLocal

    本节,我们来探讨一个特殊的概念,线程本地变量,在Java中的实现是类ThreadLocal,它是什么?有什么用?实现原理是什么?让我们接下来逐步探讨. 基本概念和用法 线程本地变量是说,每个线程都有同 ...

  4. 《Java编程的逻辑》 - 文章列表

    <计算机程序的思维逻辑>系列文章已整理成书<Java编程的逻辑>,由机械工业出版社出版,2018年1月上市,各大网店有售,敬请关注! 京东自营链接:https://item.j ...

  5. Java编程的逻辑 (83) - 并发总结

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  6. Java编程的逻辑 (84) - 反射

    ​本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http: ...

  7. Java编程的逻辑 (7) - 如何从乱码中恢复 (下)?

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

  8. 《Java编程的逻辑》终于上市了!

    2018年1月下旬,<Java编程的逻辑>终于出版上市了! 这是老马过去两年死磕到底.无数心血的结晶啊! 感谢"博客园"的广大读者们,你们对老马文章的极高评价.溢美之词 ...

  9. Java编程的逻辑 (51) - 剖析EnumSet

    本系列文章经补充和完善,已修订整理成书<Java编程的逻辑>,由机械工业出版社华章分社出版,于2018年1月上市热销,读者好评如潮!各大网店和书店有售,欢迎购买,京东自营链接:http:/ ...

随机推荐

  1. Git中的bash与CMD的区别

    Windows在使用git工具时,可以看到有两个命令输入窗: 1. Git CMD 2. Git Bash 两者的区别:Bash是基于CMD的,Bash在CMD的基础上新增了一些命令和功能,故建议使用 ...

  2. Fibonacci Modified

    题目来源:Fibonacci Modified We define a modified Fibonacci sequence using the following definition: Give ...

  3. Linux查看日志定位问题

    1.定位错误关键字所在行数 cat -n test.log |grep "查找的错误关键字" 2.得到错误关键字所在行号(假设为第500行),查询错误关键字前后100行数据 cat ...

  4. loj#6076「2017 山东一轮集训 Day6」三元组 莫比乌斯反演 + 三元环计数

    题目大意: 给定\(a, b, c\),求\(\sum \limits_{i = 1}^a \sum \limits_{j = 1}^b \sum \limits_{k = 1}^c [(i, j) ...

  5. C#基础_循环

    1,三元运算符 表达式1?表达式2:表达式3 栗子1:bool result=5>3?true:false; 栗子2: int num=5>3?1:0; 注意事项以及相应规则: 2, 3, ...

  6. oracle字符串载取及判断是否包含指定字符串

    oracle 截取字符(substr),检索字符位置(instr) case when then else end语句使用 收藏 常用函数:substr和instr1.SUBSTR(string,st ...

  7. 什么是less?

    什么是less? 简单的说,你可以在你的css文件中使用变量.函数等方式来编写你的css. less具有哪些功能? 混入(Mixins)——class中的class: 参数混入——可以传递参数的cla ...

  8. 在windows下安装git中文版客户端并连接gitlab

    下载git Windows客户端 git客户端下载地址:https://git-scm.com/downloads 我这里下载的是Git-2.14.0-64-bit.exe版本 下载TortoiseG ...

  9. 内核同步机制-RCU同步机制

    转自:https://blog.csdn.net/nevil/article/details/7718375 转自http://www.360doc.com/content/09/0805/00/36 ...

  10. IOS 数据存储之 SQLite详解

    在IOS开发中经常会需要存储数据,对于比较少量的数据可以采取文件的形式存储,比如使用plist文件.归档等,但是对于大量的数据,就需要使用数据库,在IOS开发中数据库存储可以直接通过SQL访问数据库, ...