Java ThreadLocal 深入底层源代码; 讲清楚为什么 ThreadLocalMap 的 Entry 的 key 使用弱引用;


highlight: paraiso-light

什么是 ThreadLocal

在多线程并发编程中,保证变量的线程安全性是十分重要的,在通常的做法是利用加锁来实现线程安全。这样虽然可以保证线程安全。但是程序运行的效率会显著的下降。

为了使得程序运行效率的提升并且兼顾变量被访问的线程安全性,ThreadLocal 应用而生。

ThreadLocal 的设计思想:

既然多线程访问同一个变量会造成线程安全的问题,那么创建出来一个变量,需要使用这个变量的线程将该变量拷贝一份,并且拷贝到每一个线程的变量是线程私有的,使得变量在线程之间隔离起来使用,避免了线程之间的交错使用数据造成的线程安全问题。

举个例子:

1、在 Java 中定义了一个变量是 ThreadLocal 类型的;

2、假设存在两个线程 1 以及线程 2 ,两个线程同时使用了 ThreadLocal 类型的变量,此时会在线程 1 以及线程 2 的内部存在一个 key 为 ThreadLocal 类型的变量,value 为线程内部封闭的值,也就是多个线程共用 ThreadLocal 类型的变量,value 是线程自己的;

3、这样一来线程 1 和线程 2 内部各自有键为 ThreadLocal 的变量,那么线程 1 中修改 value 之后,对于线程 2 里面的 value 是没有影响的。

4、因为 ThreadLocal 定义出来的变量是线程隔离的,避免了线程安全的问题。

import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}
输出
Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm

可以看出来,定义的 ThreadLocal 类型的变量 formatter 在被多个线程读取的时候,每个线程的内部都有 formatter 的拷贝,线程 1 中对 formatter 的数值修改时候,对于其他线程内部的 formatter 没有影响。

注:线程 0 刚开始的数据是:yyyyMMdd HHmm ,修改后的数据是:yy-M-d ah:mm

线程 1 刚开始的数据也是:yyyyMMdd HHmm ,可见这个变量是线程隔离的

ThreadLocal 有什么用

在多线程并发中,保证程序的运行效率以及变量的线程安全。

ThreadLocal 在实际开发中的应用场景

ThreadLocal 开发人员怎么使用

和上面示例代码相同

import java.text.SimpleDateFormat;
import java.util.Random;

public class ThreadLocalExample implements Runnable{

     // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }

    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());

        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }

}
输出
Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm

可以看出来,定义的 ThreadLocal 类型的变量 formatter 在被多个线程读取的时候,每个线程的内部都有 formatter 的拷贝,线程 1 中对 formatter 的数值修改时候,对于其他线程内部的 formatter 没有影响。

注:线程 0 刚开始的数据是:yyyyMMdd HHmm ,修改后的数据是:yy-M-d ah:mm

线程 1 刚开始的数据也是:yyyyMMdd HHmm ,可见这个变量是线程隔离的

Thread ThreadLocal ThreadLocalMap 之间的关系

image.png

1、Thread 里面有 ThreadLocalMap 字段。为了给每一个 Thread 都关联一个 ThreadLocalMap。
2、ThreadLocal 里面存在内部类 ThreadLocalMap 这个内部类

ThreadLocal 原理是什么

在 ThreadLocal 类的内部维护一个 ThreadLocalMap 内部类,这个类是 定制化的 HashMap ,因为它的设计思想以及实现与 HashMap 是类似的。

创建一个 ThreadLocal 变量,这个变量放在什么数据结构中,可以实现快速的读取操作呢,JDK 作者想到了使用类似 HashMap 的实现方法,利用哈希算法,将 ThreadLocal 的变量名字作为 key ,实际存储的数据作为 value 进行存储。

在底层的源代码阅读中,可以看到 ThreadLocalMap 的具体实现,抓住一点,数据的存储是基于 HashMap 的影子的。

具体代码参考下面的博客 :
ThreadLocal 源代码

源代码阅读 - ThreadLocal是如何将 key - value 保存的,如何获取到保存在 ThreadLocal 里面的 key - value

set() 方法

每个线程使用 set 方法,可以设置同一个 ThreadLocal 类型的变量在不同线程中取到不同的数值。

set() 方法是 ThreadLocal 类中的方法,本质上是使用了 ThreadLocal 的内部类 ThreadLocalMap 的set() 方法。ThreadLocalMap 使用了类似于 HashMap 这种数据结构将 key - value 保存。

key : ThreadLocal 变量,多个线程使用同一个 ThreadLocal 变量

value : 每个线程的 ThreadLocal 变量对应的具体值 ,每个线程都有自己的具体值。

 public void set(T value) {
     Thread t = Thread.currentThread();
     ThreadLocalMap map = getMap(t);
     if (map != null) {
         map.set(this, value);
     } else {
         createMap(t, value);
     }
 }
getMap()

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
map.set(this, value)
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}
createMap(t, value)

当使用 ThreadLocal 变量设置 value 的时候,如果没有与线程相互关联的 ThreadLocalMap 创建出来,那么就创建出来 ThreadLocalMap 的实例和 Thread 类的 ThreadLocalMap 类型的 threadLocals 变量绑定(threadLocals 变量指向堆内存的ThreadLocalMap 对象 )。这样一来,线程就拥有自己的 ThreadLocalMap 这种数据结构,可以使用类似于 HashMap 的这种形式保存多个 entry (多个 key - value 键值对)了。


    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

get() 方法

这个方法是 ThreadLocal 类中的方法,本质上调用的是 ThreadLocal 内部类 ThreadLocalMap 里面的 getEntry() 方法。


/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    Thread t = Thread.currentThread();
    
    // 获取到和线程相关联的 ThreadLocalMap 
    ThreadLocalMap map = getMap(t);
    
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
map.getEntry(this)

从线程 t 的 ThreadLocalMap 里面寻找键为 key 的 entry;如果 entry 的 key 存在的话,把相应的 value 返回。找不到的话,会做一些处理内存泄露的操作

/**
 * Get the entry associated with key.  This method
 * itself handles only the fast path: a direct hit of existing
 * key. It otherwise relays to getEntryAfterMiss.  This is
 * designed to maximize performance for direct hits, in part
 * by making this method readily inlinable.
 *
 * @param  key the thread local object
 * @return the entry associated with key, or null if no such
 */
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

其他需要注意的地方

预防内存泄漏 ThreadLocal 在设计之初的贴心设计

ThreadLocal 的变量存储在 ThreadLocalMap 这种数据结构中,里面存储的数据自然只有 key 以及 value ,所以考虑内存泄露,只需要考虑 key 以及 value 的引用即可。

在 JDK 设计的时候 key 使用的是 WeakReference ,而 value 使用的是强引用。那么就会出现下面的问题:key 被垃圾回收机制清理掉了,但是 value 由于强引用没有被清理掉,value 在线程结束的时候才会被清理掉。这样子造成了 value 占用的内存空间无法释放,导致了内存泄露的问题。

当然 JDK 的开发者想到了这个问题,在使用 set get remove 的时候,会对 key 为 null 的 value 进行清理,使得程序的稳定性提升。

当然,良好的编程习惯中,当线程对于 ThreadLocal 变量使用的代码块中,在代码块的末尾调用 remove 将 value 的空间释放,防止内存泄露。

为什么 ThreadLocalMap 的 Entry 中的 key 设置为弱引用

设置为弱引用最根本的原因就是防止内存泄露;

内存泄露:垃圾回收器无法回收某部分内存,这种现象就叫做内存泄露;

弱引用:JVM 将引用分为:强、软、弱、虚;
其区别就是:垃圾回收器回收引用指向的堆内存中的对象的时机不同。

而把 key 设置成为弱引用,就是在下一次的 GC 的时候,将弱引用指向的对象回收。在多线程中,假设多个线程使用同一个 ThreadLocal 类型的变量,也就是每个线程的 ThreadLocalMap 的其中一个 Entry 中的 key 使用的是同一个 ThreadLocal 类型变量的地址。

举例:三个线程中的每个线程的 ThreadLocalMap 的其中一个 Entry 中的 key 使用的是同一个 ThreadLocal 类型变量的地址。都指向了 ThreadLocal1 ;

image.png

此时假设是强引用:多个线程依赖同一个 ThreadLocal1 ,此时 线程 1 的 ThreadLocal1 使用结束了想要释放内存,但是由于是强引用(因为还有其他线程还在指向 ThreadLocal1),这就导致了线程1 的持有 ThreadLocal1 的 Entry 占有的内存无法释放,导致了内存泄露,使用弱引用的时候,这种问题就可以解决。

综上就是为什么使用弱引用的原因。

小结

本文详细的介绍了什么是 ThreadLocal 以及 ThreadLocal 的部分源代码解读。在目前网络上的大多数博客将 ThreadLocal 内部类的 ThreadLocalMap 中 Entry 的 key 为什么设置为软引用讲解的十分混乱,本文从内存泄露的角度进行了一定了阐述,阐述的比较简洁明了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值