Java | ThreadLocal与无锁线程安全

前言

  • ThreadLocal 是一种无同步的线程安全实现
  • 体现了Thread-Specific Storage模式:即使只有一个入口,内部也会为每个线程分配特有的存储空间。由于线程间没有共享资源,因此可以实现无锁线程安全
  • 本文将总结ThreadLocal的用法与实现细节,希望能帮上忙

系列文章

Java | ThreadLocal与无锁线程安全
[Java | ThreadLocal与无锁线程安全 — 源码分析] (coming soon...)


ThreadLocal 思维导图
线程安全 示意图

1. 用法

ThreadLocal的用法很简单,ThreadLocal提供了下列的public与protected方法:

ThradlLocal UML类图

现在我们查看ThreadLocal中与上述几个方法有关的代码,简化代码如下:

// ThreadLocal.java

// ThreadLocal构造方法里什么都没做
public ThreadLocal() {
    // do nothing
}

// 定义ThreadLocal变量的初始值
protected T initialValue() {
    // 默认的初始值为null
    return null;
}

// 内部方法:用于设置当前线程里ThreadLocal变量初始值  
private T setInitialValue() {
    T value = initialValue();
    // 其实ThreadLocal的源码并不是直接调用set(),但源码中这部分代码
    // 就相当于调用set()方法,这是为了防止子类重写set()造成异常
    set(value);
    return value;
}

// 获取当前线程中ThreadLocal变量的值  
public T get() {
    Thread t = Thread.currentThread();
    // ThreadLocalMap是什么?稍后介绍
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        // 存在匹配的Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            // 变量的值不为null,返回
            T result = (T)e.value;
            return result;
        }
    }
    // 获取的值为空,设置变量的初始值并返回
    return setInitialValue();
}
  
// 设置当前线程中ThreadLocal变量的值
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        // ThreadLocalMap懒初始化,直到设置值的时候才创建
        createMap(t, value);
}

// 移除当前线程中ThreadLocal变量的值
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

ThreadLocalMap存储在Thread的属性中,简化代码如下:

// Thread.java

ThreadLocal.ThreadLocalMap threadLocals = null;

// 线程退出之前,会置空threadLocals变量,以便随后GC
private void exit() {
    // ...
    threadLocals = null;
    // ...
}

分析代码,可以总结出方法的用法:

  • 1.get()获取当前线程ThreadLocal变量的值
    • 不同线程获取的值互不干扰
    • 如果取值为null,则调用initialValue()设置初始值
    1. set()设置当前线程ThreadLocal变量的值
    • 不同线程设置的值互不干扰,不会相互覆盖
    1. remove()移除当前线程之前设置在ThreadLocal变量上的值
    • 如果在当前线程下次调用get()之前,还没有调用set()设置新值,则依旧会调用setInitialValue()重新设置初始值。
    1. initialValue()子类重写此方法可以定义ThreadLocal变量的初始值
    • 默认的初始值为null

总结一下ThreadLocal的生命周期,如下图所示:

ThreadLocal生命周期 示意图

2. 例子

我们看看android.os.Looper.java 中是如何使用ThreadLocal,简化代码如下:

// /frameworks/base/core/java/android/os/Looper.java

public class Looper {
    // ...
    // 静态ThreadLocal变量,所有类实例共享同一个ThreadLocal变量
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

    private static void prepare(boolean quitAllowed) {
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        // 设置ThreadLocal变量的值
        sThreadLocal.set(new Looper(quitAllowed));
    }

    public static Looper myLooper() {
        // 获取ThreadLocal变量的值
        return sThreadLocal.get();
    }

    public static void prepare() {
        prepare(true);
    }
    // ...
}
  • ThreadLocal被声明为static final变量,泛型参数为Looper,表示ThreadLocal变量接受Looper类型的值
  • prepare()中调用ThreadLocal#set()设置当前线程Looper
  • myLooper()中调用ThreadLocal#get()获取当前线程Looper

我们可以画出Looper中访问ThreadLocal的Timethreads图,如下图所示,不同线程独占一个Looper变量,线程间不存在共享资源。可以看到ThreadLocal实现了无锁线程安全,避免了加解锁造成的上下文切换,体现了空间换时间的思想。

Timethreads图 - 01


3. 编程规约

记得吗?《阿里巴巴Java开发手册》中提到过关于ThreadLocal的编程规约,如下所示:

  • 5.【强制】SimpleDateFormate是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类。
    正例:

    private static final ThreadLocal<DataFormat> df = new ThreadLocal<DateFormat>(){
          @Override
          protected DateFormat initialValue(){
                  return new SimpleDateFormat("yyyy-MM-dd");
          }
    };
    

    说明:如果是JDK8的应用,可以使用Instant代替DateLocalDateTime代替CalendarDateTimeFormatter代替SimpleDateFormat,官方给出的解释:simple beautiful strong immutable thread-safe.

  • 15.【参考】(原文过于啰嗦,以下为笔者转述)ThreadLocal变量建议使用static修饰,可以保证变量在类初始化时创建,所有类实例可以共享同一个静态变量。

    注意到了吗?在文章开头的Looper.java源码中,ThreadLocal变量就是使用static修饰的

4. 使用场景

  • 以空间换时间实现无锁线程安全

    ThreadLocal相对于Synchronized等互斥锁避免了上下文切换损耗,有助于提高吞吐量

  • 线程级别的单例模式

    一般的单例对象是对整个进程可见的,假如这个对象不是线程安全的(比如SimpleDateFormat),就可以很方便的使用ThreadLocal实现线程级别的单例,保证线程安全

  • 共享参数

    如果一个模块有非常多地方需要使用同一个变量,相比于在每个方法中重复传递同一个参数,使用ThreadLocal作为一个全局变量也许是另一种选择方式。


看到这里,相信你已经掌握了ThreadLocal的用法,下一篇文章将深入ThreadLocal的核心,探讨数据结构ThreadLocalMap的实现细节,欢迎关注彭旭锐的简书!


推荐阅读


参考

  • ThreadLocal.java — Josh Bloch and Doug Lea
  • 《深入理解Java虚拟机 — JVM高级特性与最佳实践》 周志明 著
  • 《Java并发编程的艺术》 方腾飞 魏鹏 程晓明 著
  • 《数据结构与算法分析 — Java语言描述》 [美]Mark Allen Weiss 著
  • 《阿里巴巴Java开发手册》 杨冠宝 编著

感谢喜欢!你的点赞是对我最大的鼓励!欢迎关注彭旭锐的简书!

推荐阅读更多精彩内容