ThreadLocal —— 让每个线程拥有自己的变量副本

在多线程编程中,数据共享是一个常见的话题。很多时候我们需要多个线程共享同一个数据,以实现数据的一致性和通信。然而,在一些场景下,我们并不希望数据被多个线程共享,而是希望每个线程都拥有自己独立的一份数据。这时,ThreadLocal 就可以派上用场了。

1.ThreadLocal是什么?

ThreadLocal 是 Java 中的一个类,它提供了线程本地变量的功能。简单来说,就是为每一个使用该变量的线程提供了一个独立的变量副本。这样一来,每个线程都可以独立地改变自己的副本,而不会影响到其他线程。

2.ThreadLocal的使用方法

2.1常用方法

// 创建ThreadLocal对象
public ThreadLocal() {}
// 设置当前线程绑定的局部变量
public void set(T value) {}
// 获取当前线程绑定的局部变量
public T get() {}
// 删除当前线程绑定的局部变量
public void remove() {}

2.2使用案例

下面是一个简单的示例,展示了如何使用 ThreadLocal

public class ThreadLocalExample {
    static ThreadLocal<Integer> localVariable = new ThreadLocal<>();


    public static void main(String[] args) {
        Runnable task = () -> {
            // 设置当前线程的localVariable值
            localVariable.set(Thread.currentThread().getId());


            System.out.println("线程 " + Thread.currentThread().getId() + " 的局部变量: " + localVariable.get());
        };


        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);


        thread1.start();
        thread2.start();


        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        System.out.println("主线程的局部变量: " + localVariable.get());
    }
}

在这个例子中,每个线程都有一个 localVariable 的独立副本,它们互不影响

3.ThreadLocal的内部结构

ThreadLocal 类的内部结构比较简单。它主要由两个部分组成:

1. ThreadLocal 对象本身,持有每个线程的引用。

  1. Thread 对象中有一个 ThreadLocalMap,用于存储线程与 ThreadLocal 变量之间的映射关系。

当调用 ThreadLocalget() 方法时,会根据当前线程找到对应的 ThreadLocalMap 并返回相应的值。如果调用 set() 方法,则会在当前线程的 ThreadLocalMap 中设置一个键值对。

4.核心方法及源码分析

ThreadLocal 提供了几个常用的方法:

  • public T get(): 获取当前线程的变量副本。
  • public void set(T value): 设置当前线程的变量副本。
  • public void remove(): 移除当前线程的变量副本。

4.1 set方法

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取线程绑定的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) { 
       // 不为空直接赋值
        map.set(this, value);
    } else {
      // 为空则创建Map
      // 将当前线程t作为key,将value作为值,存放ThreadLocalMap中
        createMap(t, value);
    }
}

4.2 get方法

public T get() {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取当前线程关联的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 如果map存在
    if (map != null) {
        // this是当前ThreadLocal
        // 以当前ThreadLocal为key,获取存放的entry对象
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 获取到存放的值,直接返回
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 进行初始化操作
    // 1.如果map不存在,则表示当前线程没有关联ThreadLocalMap
    // 2.如果e不存在,则当前ThreadLocal没有关联entry
    return setInitialValue();
}


// 初始化方法
private T setInitialValue() {
    // 获取空值
    T value = initialValue();
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 获取当前线程关联的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // map不为空,直接赋值
    if (map != null) {
        map.set(this, value);
    } else {
    // map不存在,创建map并赋值
        createMap(t, value);
    }
    // 判断是否是该类型TerminatingThreadLocal
    if (this instanceof TerminatingThreadLocal) {
        // 注册
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    // 返回值
    return value;
}

4.3 remove方法

public void remove() {
   // 获取当前线程绑定的ThreadLocalMap
   ThreadLocalMap m = getMap(Thread.currentThread());
   if (m != null) {
       // 不为空,删除key对应的entry实体
       m.remove(this);
   }
}

5. ThreadLocalMap源码分析

5.1 存储结构Entry

// ThreadLocalMap中存放数据的键值对
// 继承WeakReference(弱引用)
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;  // 具体存放的值
    // 构建Entry对象  key为弱引用
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

5.2 构造函数

 // 初始阈值16
 private static final int INITIAL_CAPACITY = 16;
 // ThreadLocalMap中存放的Entry对象
 private Entry[] table;
 // Entry数组的长度
 private int size = 0;
 
 // firstKey:entry中的key  
 // firstValue:entry中的value
 ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
     // 初始化Entry数组,容量是16
    table = new Entry[INITIAL_CAPACITY];
    // 通过key进行hash运算,获取存放的下标
    int i = firstKey.threadLocalHashCode &(INITIAL_CAPACITY - 1);
    // 初始化entry,并设置key、value
    table[i] = new Entry(firstKey, firstValue);
    // 设置初始大小
    size = 1;
    // 设置阈值参数
    setThreshold(INITIAL_CAPACITY);
}


// 设置调整阈值,用于扩展table时使用
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

5.3 ThreadLocalMap.set()方法

private void set(ThreadLocal<?> key, Object value) {
    // ThreadLocalMap中存放数据的Entry数组
    Entry[] tab = table;
    // 数组长度
    int len = tab.length;
    // 对key进行hash运算,获取存放下标
    int i = key.threadLocalHashCode & (len-1);


    // 1.tab[i]不为空,直接替换value结束循环
    /** 2.tab[i]不为空,出现hash冲突,
     * 则执行 e = tab[i = nextIndex(i, len)],
       去i+1下标 **/ 
    // 使用线性探测法查找元素
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
         // key是e的引用对象,则返回true
        if (e.refersTo(key)) {
            e.value = value;
            return;
        }
        // 如果e为空,则代表之前的ThreadLocal被回收了
        if (e.refersTo(null)) {
            // 用新元素替换旧元素,防止内存泄漏
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    // 构建新的entry对象,存放tab[i]
    tab[i] = new Entry(key, value);
    // 个数累加
    int sz = ++size;
    /**
   * cleanSomeSlots用于清除那些e.get()==null的元素,
   * 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
   * 如果没有清除任何entry,并且当前已达到了扩容(长度的2/3),那么进行rehash(执行一次全表的扫描清理工作)
   */
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

5.4 关于ThreadLocal出现内存泄露原因

笔者看过较多的答案都是说ThreadLocalMap的key是虚引用,gc执行后key引用的对象回收,导致内存泄漏。但是,如果ThreadLocalMap的key改成强引用,那么gc执行后,key的引用仍然存在,仍然无法进行回收,内存泄露仍然存在。根本原因在于Entry中的value没有销毁

笔者认为真正解决的办法应该是如下两点:

  1. 在使用完ThreadLocalMap中存放的变量后,要手动remove
  2. 等待线程执行完毕

6.总结

通过本文的介绍,我们了解到了 ThreadLocal 的概念、作用以及其实现原理。ThreadLocal 不仅简化了多线程编程中的数据管理,还能提高程序的并发性能。

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shangjg3

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值