本节,我们来探讨一个特殊的概念,线程本地变量,在 Java 中的实现是类 ThreadLocal,它是什么?有什么用?实现原理是什么?让我们接下来逐步探讨。
基本概念和用法
线程本地变量是说,每个线程都有同一个变量的独有拷贝,这个概念听上去比较难以理解,我们先直接来看类 TheadLocal 的用法。
ThreadLocal 是一个泛型类,接受一个类型参数 T,它只有一个空的构造方法,有两个主要的 public 方法:
- public T get()
- public voidset(T value)
set 就是设置值,get 就是获取值,如果没有值,返回 null,看上去,ThreadLocal 就是一个单一对象的容器,比如:
- public static void main(String[] args) {
- ThreadLocal local = newThreadLocal<>();
- local.set(100);
- System.out.println(local.get());
- }
输出为 100。
那 ThreadLocal 有什么特殊的呢?特殊发生在有多个线程的时候,看个例子:
- public class ThreadLocalBasic {
- staticThreadLocal local = newThreadLocal<>();
- public static voidmain(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 voidremove()
initialValue 用于提供初始值,它是一个受保护方法,可以通过匿名内部类的方式提供,当调用 get 方法时,如果之前没有设置过,会调用该方法获取初始值,默认实现是返回 null。remove 删掉当前线程对应的值,如果删掉后,再次调用 get,会再调用 initialValue 获取初始值。看个简单的例子:
- public class ThreadLocalInit {
- staticThreadLocal local = newThreadLocal(){
- @Override
- protected Integer initialValue() {
- return100;
- }
- };
- 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 {
- staticThreadLocal sdf =newThreadLocal() {
- @Override
- protected DateFormat initialValue() {
- return newSimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- }
- };
- public static String date2String(Date date) {
- return sdf.get().format(date);
- }
- public staticDate 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 finalThreadLocal localRandom =
- newThreadLocal() {
- protected ThreadLocalRandom initialValue() {
- return new ThreadLocalRandom();
- }
- };
上下文信息
ThreadLocal 的典型用途是提供上下文信息,比如在一个 web 服务器中,一个线程执行用户的请求,在执行过程中,很多代码都会访问一些共同的信息,比如请求信息、用户身份信息、数据库连接、当前事务等,它们是线程执行过程中的全局信息,如果作为参数在不同代码间传递,代码会很啰嗦,这时,使用 ThreadLocal 就很方便,所以它被用于各种框架如 Spring 中,我们看个简单的示例:
- public class RequestContext {
- public static classRequest {//...
- };
- private staticThreadLocal localUserId = newThreadLocal<>();
- private staticThreadLocal localRequest = newThreadLocal<>();
- 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 =newThreadLocalMap(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 {
- staticThreadLocal sequencer =newThreadLocal() {
- @Override
- protected AtomicInteger initialValue() {
- return newAtomicInteger(0);
- }
- };
- static classTaskimplements Runnable {
- @Override
- public void run() {
- AtomicInteger s = sequencer.get();
- intinitial = 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 对象并不会被清空,修改后的值带到了下一个异步任务。那怎么办呢?有几种思路:
我们分别来看下,对于第一种,在 Task 的 run 方法开始处,添加 set 或 remove 代码,如下所示:
- static classTaskimplements Runnable {
- @Override
- public void run() {
- sequencer.set(newAtomicInteger(0));
- //或者 sequencer.remove();
- AtomicInteger s = sequencer.get();
- //...
- }
- }
对于第二种,将 Task 的 run 方法包裹在 try/finally 中,并在 finally 语句中调用 remove,如下所示:
- static classTaskimplements Runnable {
- @Override
- public void run() {
- try{
- AtomicInteger s = sequencer.get();
- intinitial = s.getAndIncrement();
- // 期望初始为0
- System.out.println(initial);
- }finally{
- sequencer.remove();
- }
- }
- }
以上两种方法都比较麻烦,需要更改所有异步任务的代码,另一种方法是扩展线程池 ThreadPoolExecutor,它有一个可以扩展的方法:
- protected voidbeforeExecute(Thread t, Runnable r) { }
在线程池将任务 r 交给线程 t 执行之前,会在线程 t 中先执行 beforeExecure,可以在这个方法中重新初始化 ThreadLocal。如果知道所有需要初始化的 ThreadLocal 变量,可以显式初始化,如果不知道,也可以通过反射,重置所有 ThreadLocal,反射的细节我们会在后续章节进一步介绍。
我们创建一个自定义的线程池 MyThreadPool,示例代码如下:
- static classMyThreadPoolextends ThreadPoolExecutor {
- publicMyThreadPool(intcorePoolSize,int maximumPoolSize,
- long keepAliveTime, TimeUnit unit,
- BlockingQueue workQueue) {
- super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
- }
- @Override
- protected void beforeExecute(Thread t, Runnable r) {
- try {
- //使用反射清空所有ThreadLocalField 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 =newMyThreadPool(2, 2, 0,
- TimeUnit.MINUTES, newLinkedBlockingQueue());
- executor.execute(new Task());
- executor.execute(new Task());
- executor.execute(new Task());
- executor.shutdown();
- }
使用以上介绍的任意一种解决方案,结果就符合期望了。
小结
本节介绍了 ThreadLocal 的基本概念、用法用途、实现原理、以及和线程池结合使用时的注意事项,简单总结来说:
从 65 节到现在,我们一直在探讨并发,至此,基本就结束了,下一节,让我们一起简要回顾总结一下。
(与其他章节一样,本节所有代码位于 https://github.com/swiftma/program-logic,另外,与之前章节一样,本节代码基于 Java 7, Java 8 有些变动,我们会在后续章节统一介绍 Java 8 的更新)
----------------
未完待续,查看最新文章,敬请关注微信公众号 "老马说编程"(扫描下方二维码),从入门到高级,深入浅出,老马和你一起探索 Java 编程及计算机技术的本质。用心原创,保留所有版权。
来源: http://www.cnblogs.com/swiftma/p/6764821.html