Java | ThreadLocal 用法解析【Deprecated】

提示 2021年1月2日

这篇文章是2019年写的,写的不好。今年我重新梳理了一遍,你可以直接看:「Java 路线」| ThreadLocal

点赞关注,不再迷路,你的支持对我意义重大!

🔥 Hi,我是丑丑。这里有 Android 进阶成长路线笔记 & 博客,欢迎跟着彭丑丑一起成长。(联系方式在 GitHub)

前言

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

系列文章

延伸文章

ThreadLocal 思维导图

目录


1. ThreadLocal API

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

2. 生命周期

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

ThreadLocal生命周期 示意图

3. 使用案例

我们直接以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


4. 编程规约

记得吗?《阿里巴巴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修饰的

5. 使用场景

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

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

  • 线程级别的单例模式

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

  • 共享参数

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


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


参考

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

创作不易,你的「三连」是丑丑最大的动力,我们下次见!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容