美文网首页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实现无锁线程安全

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