并发编程之 ThreadLocal( 二 )


使用ThreadLocal 的好处

  • 达到线程安全
  • 不需要加锁,提高执行效率
  • 合理利用内存,节省开销
以下代码,我们构建了一个内部类 ThreadSafeFormatter 类 , 在类内部定义 ThreadLocal 的成员变量,并重写了 initialValue 方法,返回的参数就是 new 出来的 SimpleDateFormat 对象 。
public class ThreadLocalTest {public static ExecutorService threadPool = Executors.newFixedThreadPool(10);public static void main(String[] args) {for (int i = 0; i < 1000; i++) {int finalI = i;threadPool.submit(() -> System.out.println(new ThreadLocalTest().sec2Date(finalI)));}}private String sec2Date(int seconds) {// 在 ThreadLocal 第一个 get 的时候把对象初始化出来,对象的初始化时机可以由我们控制SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();return dateFormat.format(seconds * 1000);}static class ThreadSafeFormatter {// 方式一(原始方式)public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {// 初始化@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");}};// 方式二(Lambda表达式)public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));}}输出结果:
并发编程之 ThreadLocal

文章插图
结果中我们可以看出,没有输出重复的时间值(可以多运行几次观察下),因此我们通过 ThreadLocal 这种方式就达到了线程安全,并且还节省了系统的开销,合理利用了内存 。
由此我们可以得到一个结论:每个线程的 SimpleDateFormat 是独立的,一共有 10 个 。每个线程会平均执行 100 个任务 , 每个线程之间都是复用一个 SimpleDateFormat 对象 。
ThreadLocal 源码分析在了解 ThreadLocal 源码之前 , 我们先了解以下 Thread,ThreadLocalMap 以及 ThreadLocal三者之间的关系 。
首先,我们创建的每一个 Thread 对象中都持有一个 ThreadLocalMap 成员变量,而 ThreadLocalMap 中可以存放着很多的 key 为 ThreadLocal 的键值对 。
并发编程之 ThreadLocal

文章插图

并发编程之 ThreadLocal

文章插图
主要方法介绍
  • T initialValue() : 初始化,返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发 。
  • void set(T t) : 为这个线程设置一个新值 。
  • T get() : 得到这个线程对应的value 。如果是首次调用 get(),则会调用 initialize 来得到这个值 。
  • void remove() :删除对应这个线程的值 。
initialValueSimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get();
在上述代码,我们并没有显式地调用这个 initialValue 方法,而是调用了 get 方法,而在 get 方法中,它会去调用
setInitialValue 方法,在 该方法内部它才会去调用我们重写的 initialValue 方法 。
并发编程之 ThreadLocal

文章插图

并发编程之 ThreadLocal

文章插图
如果没有重写 initialValue 时,默认会返回 null
并发编程之 ThreadLocal

文章插图
如果线程先前调用了set方法,在这种情况下,不会为线程调用本 initialValue 方法,而是直接用之前 set 进去的值 。
在通常情况下,每个线程最多只能调用一次 initialValue 方法,但是如果已经调用了 remove 方法之后,再调用 get 方法,则可以再次调用 initialValue 方法 。
getget 方法是先取出当前线程的 ThreadLocalMap ,然后调用 map.getEntry 方法,把本 ThreadLocal 的引用作为参数传入,取出 map 中属于本 ThreadLocal 的value 。
public T get() {// 获取当前线程Thread t = Thread.currentThread();// 获取当前线程的threadLocals 成员变量ThreadLocalMap map = getMap(t);if (map != null) {// this 指的是 ThreadLocal 对象,通过 map.getEntry 来获取我们通过 set 方法设置进去的 value 值ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {@SuppressWarnings("unchecked")T result = (T)e.value;return result;}}return setInitialValue();}ThreadLocalMap getMap(Thread t) {return t.threadLocals;}set跟 get 一样,同样是先获取当前线程的引用,然后再获取当前线程的 threadLocals 成员变量,如果 threadLocals 为null,即还未初始化 , 就会执行 createMap 方法来进行初始化 。

推荐阅读