写点什么

Java Core 「17」ThreadLocal

作者:Samson
  • 2022 年 6 月 24 日
  • 本文字数:2040 字

    阅读完需:约 7 分钟

摘要

在多线程环境下,最常用的实现线程安全的方式有如下几种:

  • 互斥同步机制,例如 Lock,synchronized 关键字,信号量等。

  • 非阻塞同步机制,例如自旋 + CAS。

  • 线程独享机制,即不在多线程间共享,例如本文要学习的 ThreadLocal。

那么,ThreadLocal 是如何实现每个线程独有一份副本的?

01-ThreadLocal 线程隔离原理

当我们在某个类中使用 ThreadLocal 类型的变量时,是如何做到每个线程存储一份副本的呢?我们知道,在 Java 中,每个线程都是一个独立的 Thread 对象。要实现线程之间互相独立,肯定要在 Thread 对象上做功夫。

Thread 类中有如下的一个属性:

ThreadLocal.ThreadLocalMap threadLocals = null;
复制代码

当我们使用 ThreadLocal 对象获得值时,一般会用如下的写法:

ThreadLocal<Object> tl = new ThreadLocal();Object obj = tl.get();
复制代码

让我们看一下,当我们调用 get 方法时,做了什么操作:

public T get() {		/** 获得调用此方法的当前线程 */    Thread t = Thread.currentThread();		/** getMap 返回的就是 Thread 中的 threadLocals 对象 */    ThreadLocalMap map = getMap(t);    if (map != null) {				/** 此处的 this 指的是上面的 tl 对象*/        ThreadLocalMap.Entry e = map.getEntry(this); 				/** 检查 threadLocals 中是否存在与 tl 相关的值(以 tl 为键的值) */        if (e != null) {            @SuppressWarnings("unchecked")            T result = (T)e.value;            return result;        }    }		/** 如果 t.threadLocals 为空,则为其初始化一个,     * 并将 initialValue() 方法的返回值写入 */    return setInitialValue();}
复制代码

02-示例分析

知道 ThreadLocal 实现线程间隔离的原理后,我们来分析一个实例,来具体看一下整个过程。假设我们有如下的程序:

/** private static 是一个典型的写法 */private static ThreadLocal<String> dbConnection = new ThreadLocal<String>() {    @Override    protected String initialValue() {        Thread t = Thread.currentThread();        return t.getName();    }};
public String getConnection() { return dbConnection.get();}
复制代码

假设有线程 t1 / t2:

new Thread(() -> {    String str = getConnection();    System.out.println(Thread.currentThread().getName() + " str = " + str);}, "thread-t1").start();
new Thread(() -> { String str = getConnection(); System.out.println(Thread.currentThread().getName() + " str = " + str);}, "thread-t2").start();
复制代码

当线程 t1 执行到 getConnection 时,会调用 ThreadLocal 中的 get 方法。在 get 方法中,先获取当前线程,即 t1。然后,获取 t1 的 threadLocals 映射表。此时,t1.threadLocals 为 null,即尚未初始化。然后,调用 setInitialValue,再到我们重写的方法 initialValue,返回当前线程 t1 的线程名”thread-t1“。最后,初始化 t1.threadLocals 并将 dbConnection 对象和”thread-t1”写入到 t1.threadLocals 中。

线程 t2 的执行过程与 t1 一样。因为 t1.threadLocals 和 t2.threadLocals 是两个 Thread 对象中的变量,所以互相不干扰。这样,借助 ThreadLocal 线程 t1 和 t2 实现了 dbConnection 的隔离访问。

03-ThreadLocal 导致的内存泄漏问题

ThreadLocal 有可能会导致内存泄漏的发生。从前面的学习中我们了解到,Thread 类中存在一个 ThreadLocal.ThreadLocalMap 对象,它是一个映射表,key 是对 ThreadLocal 对象(若引用),value 是一个特定类型的值。

内存泄漏出现的原因是,假如某个 ThreadLocal 对象的强引用不在了,其势必会被 GC 回收掉。但是,Thread 类中的 ThreadLocal.ThreadLocalMap 对象的生存周期与线程对象是一致的。当 ThreadLocal 对象被回收后,就无法访问其对应的 value,导致内存一致不能释放。

有一种常见的错误就是,在线程池中不恰当地使用 ThreadLocal,线程池中的线程对象一直存在,当上述情况发生时,发生内存泄漏的机率更大。

ThreadLocal 推荐的使用方法是:

  1. 每次使用完 ThreadLocal 后,就调用它的 remove 方法,移除 value

  2. 将 ThreadLocal 对象声明成 private static,确保存在强引用而不会被 GC 回收,从而能够访问到其对应的 value。

[1] 面试:为了进阿里,死磕了ThreadLocal内存泄露原因


历史文章推荐

Java Core 「16」J.U.C Executor 框架之 ScheduledThreadPoolExecutor

Java Core 「15」J.U.C Executor 框架

Java Core 「14」J.U.C 线程池 -Future & FutureTask

Java Core 「13」ReentrantReadWriteLock 再探析

Java Core 「12」ReentrantLock 再探析

Java Core 「11」AQS-AbstractQueuedSynchronizer

Java Core 「10」J.U.C 同步工具类 -2

Java Core 「9」J.U.C 同步工具类 -1

Java Core 「8」字节码增强技术

Java Core 「7」各种不同类型的锁

Java Core 「6」反射与 SPI 机制

Java Core 「5」自定义注解编程

Java Core 「4」java.util.concurrent 包简介

发布于: 刚刚阅读数: 3
用户头像

Samson

关注

还未添加个人签名 2019.07.22 加入

还未添加个人简介

评论

发布
暂无评论
Java Core 「17」ThreadLocal_学习笔记_Samson_InfoQ写作社区