Java Concurrency - ThreadLocal, 本地线程变量
共享数据是多线程应用最常见的问题之一,但有时我们需要为每个线程保存一份独立的变量。Java API 提供了 ThreadLocal 来解决这个问题。
一个 ThreadLocal 作用的例子:
import java.util.Date; public class Main { public static void main(String[] args) { Runnable task = new Runnable() { private ThreadLocal<Date> dateVar = new ThreadLocal<Date>(); public void run() { dateVar.set(new Date()); System.out.printf("%s, GET dateVar: %s\n", Thread.currentThread().getName(), dateVar.get()); try { Thread.sleep(5000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.printf("%s, FINAL dateVar: %s\n", Thread.currentThread().getName(), dateVar.get()); } }; for (int i = 0; i < 3; i++) { String threadName = "Thread" + (i + 1); Thread thread = new Thread(task, threadName); thread.start(); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } }
观察运行结果:
Thread1, GET dateVar: Fri Oct 14 22:06:33 CST 2016
Thread2, GET dateVar: Fri Oct 14 22:06:34 CST 2016
Thread3, GET dateVar: Fri Oct 14 22:06:35 CST 2016
Thread1, FINAL dateVar: Fri Oct 14 22:06:33 CST 2016
Thread2, FINAL dateVar: Fri Oct 14 22:06:34 CST 2016
Thread3, FINAL dateVar: Fri Oct 14 22:06:35 CST 2016
可以看到每个线程都共用一个 Task 实例,线程之间间隔 1 秒启动。线程执行 run 方法的时候首先将 dateVar 的值设置为系统当前时间并打印 dateVar 值,然后线程会休眠 5 秒,最后再打印 dateVar 的值。注意到 run 方法设置 dateVar 值与最后打印 dateVar 值间隔 5 秒,而下一个线程启动时只间隔 1 秒,在当前线程打印 dateVar 之前,下个线程甚至是下下个线程已经重置 dateVar 的值,但是每个线程最后打印 dateVar 值的时候仍然是显示该线程最初设置的值。可见,每个线程中的 dateVar 并不会被其他线程所干扰。
再看下没有使用 ThreadLocal 的情况。
import java.util.Date; public class Main2 { public static void main(String[] args) { Runnable task = new Runnable() { private Date dateVar; public void run() { dateVar = new Date(); System.out.printf("%s, GET dateVar: %s\n", Thread.currentThread().getName(), dateVar); try { Thread.sleep(5000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.printf("%s, FINAL dateVar: %s\n", Thread.currentThread().getName(), dateVar); } }; for (int i = 0; i < 3; i++) { String threadName = "Thread" + (i + 1); Thread thread = new Thread(task, threadName); thread.start(); try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } } } }
观察运行结果:
Thread1, GET dateVar: Fri Oct 14 22:17:38 CST 2016
Thread2, GET dateVar: Fri Oct 14 22:17:39 CST 2016
Thread3, GET dateVar: Fri Oct 14 22:17:40 CST 2016
Thread1, FINAL dateVar: Fri Oct 14 22:17:40 CST 2016
Thread2, FINAL dateVar: Fri Oct 14 22:17:40 CST 2016
Thread3, FINAL dateVar: Fri Oct 14 22:17:40 CST 2016
可以看到,当直接使用 Date 类型时,线程最终打印 dateVar 的值与最初设置的值不一致。这个是因为所有的线程共享一个 Task 实例,所以 dateVar 是共享数据,在多个线程竞争时,造成数据不一致。
ThreadLocal 的初始化
可以通过覆盖 initialValue 方法初始化 ThreadLocal 的值。
ThreadLocal<Date> dateVar = new ThreadLocal<Date>() { @Override protected Date initialValue() { return new Date(); } };
InheritableThreadLocal
如果在线程 A 创建了子线程 B,那么线程 A 和 线程 B 都是各自维护一份 ThreadLocal 值,线程 A 的 ThreadLocal 值不会传递给子线程 B。Java API 提供了 InheritableThreadLocal 类,它是 ThreadLocal 的子类。如果使用 InheritableThreadLocal,那么在线程 A 创建子线程 B,线程 A 和 线程 B 仍然都是各自维护一份 InheritableThreadLocal 值,但是现场 A 的 InheritableThreadLocal 值则会传递给子线程 B。可以重写 childValue 方法修改从父现场继承的 InheritableThreadLocal 值。
public static ThreadLocal<Date> dateVar = new InheritableThreadLocal<Date>() { protected Date initialValue() { return new Date(); }; protected Date childValue(Date parentValue) { return new Date(parentValue.getTime() + 1000L); }; };