专栏首页老马说编程(82) 理解ThreadLocal / 计算机程序的思维逻辑

(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对象并不会被清空,修改后的值带到了下一个异步任务。那怎么办呢?有几种思路:

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

我们分别来看下,对于第一种,在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节到现在,我们一直在探讨并发,至此,基本就结束了,下一节,让我们一起简要回顾总结一下。

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

本文分享自微信公众号 - 老马说编程(laoma_shuo),作者:老马

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2017-04-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • (69) 线程的中断 / 计算机程序的思维逻辑

    本节主要讨论一个问题,如何在Java中取消或关闭一个线程? 取消/关闭的场景 我们知道,通过线程的start方法启动一个线程后,线程开始执行run方法,run...

    swiftma
  • (83) 并发总结 / 计算机程序的思维逻辑

    从65节到82节,我们用了18篇文章讨论并发,本节进行简要总结。 多线程开发有两个核心问题,一个是竞争,另一个是协作。竞争会出现线程安全问题,所以,本节首先总结...

    swiftma
  • (78) 线程池 / 计算机程序的思维逻辑

    上节,我们初步探讨了Java并发包中的任务执行服务,实际中,任务执行服务的主要实现机制是线程池,本节,我们就来探讨线程池。 基本概念 线程池,顾名思义,就是一...

    swiftma
  • Java并发学习之ThreadLocal使用及原理介绍

    ThreadLocal使用及原理介绍 线程本地变量,每个线程保存变量的副本,对副本的改动,对其他的线程而言是透明的(即隔离的) 1. 使用姿势一览 使用方式也...

    一灰灰blog
  • 聊聊ThreadLocal那些事

    这篇文章聊聊 ThreadLocal,我们经常会在一些开源中间件的源码中见到它的身影,比较常见的用途是保存上下文信息,还有就是保证了线程安全。

    pjmike
  • ThreadLocal 原理和使用场景分析

    ThreadLocal 不知道大家有没有用过,但至少听说过,今天主要记录一下 ThreadLocal 的原理和使用场景。 使用场景 直接定位到 Threa...

    古时的风筝
  • ThreadLocal源码分析

    ThreadLocal是JDK提供的一个工具类,其作用是在多线程共享资源的情况下,使每个线程持有一份该资源的副本,每个线程的副本都是独立互不影响的。线程操作各自...

    张申傲
  • 深入理解ThreadLocal:拨开迷雾,探究本质

    本文将带领读者深入理解ThreadLocal,为了保证阅读质量,我们可以先一起来简单理解一下什么是ThreadLocal?如果你从字面上来理解,很容易将Thre...

    itlemon
  • 深入理解ThreadLocal:拨开迷雾,探究本质

    本文将带领读者深入理解ThreadLocal,为了保证阅读质量,我们可以先一起来简单理解一下什么是ThreadLocal?如果你从字面上来理解,很容易将Thre...

    itlemon
  • 深入理解ThreadLocal:拨开迷雾,探究本质

    > ThreadLocal是JDK1.2提供的一个工具,它为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,解决共...

    itlemon

扫码关注云+社区

领取腾讯云代金券