美文网首页Android开发经验谈程序员Android开发
Java | 使用ThreadLocal实现无锁线程安全

Java | 使用ThreadLocal实现无锁线程安全

作者: 彭旭锐 | 来源:发表于2019-07-29 20:57 被阅读335次

前言

  • ThreadLocal 是一种无同步的线程安全实现
  • 体现了Thread-Specific Storage模式:即使只有一个入口,内部也会为每个线程分配特有的存储空间,线程间没有共享资源,实现了无锁线程安全
  • 本文将总结ThreadLocal的用法与实现细节,希望能帮上忙
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开发手册》 杨冠宝 编著

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

相关文章

  • Java | 使用ThreadLocal实现无锁线程安全

    前言 ThreadLocal 是一种无同步的线程安全实现 体现了Thread-Specific Storage模式...

  • 多线程juc锁

    java_basic 1 线程安全 在Java多线程编程当中,实现线程安全: 内部锁(Synchronized...

  • java学习记录--ThreadLocal使用案例

    java学习记录--ThreadLocal使用案例 标签(空格分隔): java 本文借由并发环境下使用线程不安全...

  • 套路之ThreadLocal-续1

    上次讲到ThreadLocal的使用场景,这次接着讲。 管理应用程序的线程安全 使用ThreadLocal来实现线...

  • 安卓基础

    1.ThreadLocal 的使用及其源码实现ThreadLocal中存储的值是如何保证绝对的线程安全的? 实际上...

  • java并发之ThreadLocal

    java并发之ThreadLocal 知识导读 ThreadLocal主要作用于线程的上下文,而不是线程安全,如果...

  • Java并发编程笔记(九):避免活跃性危险

    在安全性和活跃性之间通常存在某种制衡。我们使用锁来确保线程安全,但过度使用锁,则可能会导致死锁。Java应用程序无...

  • JUC并发集合总结

    ConcurrentLinkedQueue 线程安全的支持高并发的队列,使用链表实现。非阻塞,无锁,无界。该队列也...

  • Java 之 ThreadLocal 详解

    ThreadLocal 在java中是充当“线程本地变量”使用的,在每个线程都会创建该对象从而实现线程之间的隔离,...

  • ThreadLocal 实现

    一. ThreadLocal 如何实现和每个 Thread 绑定, 从而避免线程安全问题 ThreadLocal ...

网友评论

    本文标题:Java | 使用ThreadLocal实现无锁线程安全

    本文链接:https://www.haomeiwen.com/subject/ciwirctx.html