并发(二、ThreadLocal)

1、简介

Java官方文档描述释义:ThreadLocal 类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过 get 和 set 访问)时能够保证各个线程的变量相对独立与其他线程内的变量。ThreadLocal 实例通常来说都是 private static 类型的,用于关联线程和线程上下文。

从释义中可知:ThreadLocal 就是提供线程内部的局部变量,不同线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

ThreadLocal,也就是线程本地变量。如果你创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作自己本地内存里面的变量,从而起到线程隔离的作用,避免了线程安全问题。

在这里插入图片描述

● JDK8 的新设计方案的两个好处

  1. 每个 Map 存储的 Entry 数量变少。
  2. 当 Thread 销毁时,ThreadLocalMap 也会随之销毁,减少内存开销避免内存泄漏。

● ThreadLocal 原理

  • Thread 类有一个类型为 ThreadLocal.ThreadLocalMap 的实例变量 threadLocals,每个线程都有一个属于自己的ThreadLocalMap。
  • ThreadLocalMap 内部维护着 Entry 数组,每个 Entry 代表一个完整的对象,key 是 ThreadLocal 的弱引用,value 是 ThreadLocal 的泛型值。
  • 每个线程在往 ThreadLocal 里设置值的时候,都是往自己的 ThreadLocalMap 里存,读也是以某个 ThreadLocal 作为引用,在自己的 Map 里找对应的 key,从而实现了线程隔离。
  • ThreadLocal 本身不存储值,它只是作为一个 key 来让线程往 ThreadLocalMap 里存取值。

2、核心方法

方法描述
public ThreadLocal()创建ThreadLoca对象
public void set(T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量
[一定要写,避免内存泄漏]

● 补充:Thread 内部 ThreadLocalMap

ThreadLocal 实现的秘密都在这个 ThreadLocalMap 了,Thread类中定义了一个 类型为 ThreadLocal.ThreadLocalMap 的成员变量 threadLocals

public class Thread implements Runnable {
    //ThreadLocal.ThreadLocalMap是Thread的属性
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

2.1、set(T)

首先看一下 ThreadLocal 的 set(T) 方法,发现先获取到当前线程,再获取 ThreadLocalMap ,然后把元素存到这个 Map 中。

// 设置当前线程对应 ThreadLocal 值
public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 判断map是否存在
    if (map != null)
    	map.set(this, value); // 存在则直接 set
    else
    	createMap(t, value); // 不存在则创建并初始化
}

// 获取 ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

// 创建并初始化
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

2.2、get()

// 返回当前线程中保存的 ThreadLocal 值
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 判断map是否存在
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); // 存在则获取
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result; // 获取的value非空,强转后直接返回
        }
    }
    return setInitialValue(); // map或value不存在则返回初始化值
}

// 初始化
private T setInitialValue() {
    // 调用 initialValue() 获取初始化值
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的 ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    // 判断map是否存在
    if (map != null)
        map.set(this, value); // 存在则赋值
    else
        createMap(t, value); // 不存在则创建并初始化
    return value; // 返回初始值
}

/**
 * 初始化值
 * 可被子类重写,以获取自定义的初始值
 */
protected T initialValue() {
    return null;
}

2.3、remove()

// 删除当前线程保存的 ThreadLocal 对应的 Entry 实体
public void remove() {
    // 获取当前线程对象中维护的 ThreadLocalMap 对象
    ThreadLocalMap m = getMap(Thread.currentThread());
    // 如果存在
    if (m != null)
        m.remove(this); // 嘎掉
}

3、基本结构 ThreadLocalMap

ThreadLocalMap 是 ThreadLocal 的内部类,并没有实现 Map 接口,用独立的方式实现了 Map 功能,其内部的 Entry 也是独立的。

在这里插入图片描述

public class ThreadLocal<T> {
    static class ThreadLocalMap {
        /**
         * Entry 依然保持 key-value 的结构,但 key 只能是 ThreadLocal 对象.
         * 另外 Entry 继承自 WeakReference,也就是 key (ThreadLocal) 是弱引用,目的是将 ThreadLocal 对象的生命周期和线程生命周期解绑.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        // 初始容量 -- 必须是2的整数次密.
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 存放数据的 table,根据需要调整大小.
         * table.length 必须始终是2的整数次密.
         */
        private Entry[] table;

        /**
         * table 中 entries 的数量.
         */
        private int size = 0;

        /**
         * 进行扩容的阈值.
         */
        private int threshold; // Default to 0
    }
}

4、例子

● Demo

ThreadLocal<String> tl = new ThreadLocal<>();

List<Thread> threadList = new ArrayList<>();

for (int i = 0; i < 10; i++) {
    String v = i + "";
    int time = i;
    Thread thread = new Thread(() -> {
        try {
            tl.set(v);
            if (time % 2 == 0) { // 增加一点参差感
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            String s = tl.get();
            System.out.println("----------------------------------");
            System.out.println(Thread.currentThread().getName() + " : " + s);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            tl.remove();
        }
    }, "thread-" + i);

    thread.start();
    threadList.add(thread);
}

threadList.forEach(t -> {
    try {
        t.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});

● Res

在这里插入图片描述


5、ThreadLocal 内存泄露

先分析一下使用 ThreadLocal 时的内存,在JVM中,栈内存线程私有,存储了对象的引用,堆内存线程共享,存储了对象实例。

所以,栈中存储了ThreadLocal、Thread的引用,堆中存储了它们的具体实例。

在这里插入图片描述

那么现在问题就来了,弱引用很容易被回收,如果 ThreadLocal(ThreadLocalMap的 Key)被垃圾回收器回收了,但是 ThreadLocalMap 生命周期和 Thread 是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap 的 key 没了,value 还在,这就会造成了 内存泄漏问题

5.1、解决内存泄漏问题

很简单,使用完 ThreadLocal 后,及时调用 remove() 方法释放内存空间。

ThreadLocal<String> tl = new ThreadLocal<>();
try {
	tl.set("xxx”);
} finally {
	tl.remove();
}

5.2、key 为什么设计成弱引用

key 设计成弱引用同样是为了防止内存泄漏问题。

在 ThreadLocal 中,每个 ThreadLocal 实例都对应着一个 ThreadLocalMap 对象,而 ThreadLocalMap 使用 ThreadLocal 实例作为键,用于存储线程局部变量的值。如果将 ThreadLocal 实例作为强引用存储在 ThreadLocalMap 中,那么即使线程不再使用某个 ThreadLocal 实例,该实例也无法被垃圾回收,从而可能导致内存泄漏。

事实上,在 ThreadLocalMap 中的 set\getEntry 方法中,会对 key 为 null(也就是 ThreadLocal 为 null)进行判断,如果为 null 的话,就会对 value 置 null。

这意味着使用完 ThreadLocal,CurrentThread 依然运行的情况下,即便忘记调用 remove 清除,弱引用也比强引用多一层保障:弱引用的 ThreadLocalMap 会被回收,对应的 value 会在下次 ThreadLocalMap 调用 set/get/remove 中任一方法时被清除,从而避免内存泄漏。

● 补充内容

b.1、Java的引用关系分类

◎ 强引用(Strong References)

Java中最常见的引用类型。一个对象如果有强引用与之关联,它就不会被垃圾回收器回收,即使当前内存空间不足,JVM宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

Object strongRef = new Object(); // strongRef 是强引用
◎ 软引用(Soft References)

用来描述一些还有用但并非必需的对象,如果内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。软引用可用来实现内存敏感的高速缓存。

SoftReference<Object> softRef = new SoftReference<>(new Object()); // softRef 是软引用
// 使用 get() 方法可以获取软引用关联的对象
Object obj = softRef.get();

// 也可继承 SoftReference
◎ 弱引用(Weak References)

也是用来描述非必需对象的,但是它的强度比软引用更弱一些。被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。

WeakReference<Object> weakRef = new WeakReference<>(new Object()); // weakRef 是弱引用
// 使用 get() 方法可以获取弱引用关联的对象
Object obj = weakRef.get();
// 但在垃圾回收后,很可能 obj 为 null

// 也可继承 WeakReference
◎ 虚引用(Phantom References)

也称为幽灵引用或幻影引用,是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。

ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue); // phantomRef 是虚引用
// 虚引用必须和引用队列一起使用
// 你可以通过监听队列来得知对象何时被回收

b.2、内存泄漏概念

  • 内存溢出(Memory overflow):没有足够的内存供申请者使用。
  • 内存泄漏(Memory leak):程序中已动态分配的堆内存由于某种原因导致程序未释放或无法释放,造成系统内存浪费,导致程序运行速度减慢甚至系统崩溃等严重后果,内存泄漏的堆积终将导致内存溢出。

6、ThreadLocalMap 解决 Hash冲突

我们可能都知道HashMap使用了链表来解决冲突,也就是所谓的链地址法。ThreadLocalMap 没有使用链表,自然也不是用链地址法来解决冲突了,它用的是另 外一种方式:开放定址法。简单来说,就是这个坑被 人占了,那就接着去找空着的坑。

在这里插入图片描述

如上图所示,如果插入一个 value=27 的数据,通过 hash 计算后应该落入第 4 个 槽位中,而槽位 4 已经有了 Entry 数据,而且 Entry 数据的 key 和当前不相等。此时就会线性向后查找,一直找到 Entry 为 null 的槽位才会停止查找,把元素放到空的槽 中。 在 get 的时候,也会根据 ThreadLocal 对象的 hash值,定位到 table 中的位置,然后判断该槽位 Entry 对象中的 key 是否和 get 的 key 一致,如果不一致,就判断下一个位置。


7、父子线程共享数据

此时就需要另外一个类 InheritableThreadLocal,使用起来很简单,在主线程的 InheritableThreadLocal 实例设置值,在子线程中就可以 拿到了。

final ThreadLocal threadLocal = new InheritableThreadLocal();
// 主线程
threadLocal.set("xxx");
//子线程
new Thread(() -> {
    System.out.println(threadLocal.get());
}).start();

7.1、InheritableThreadLocal原理

原理很简单,在Thread类里还有另外一个变量:

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

在 Thread.init 的时候,如果父线程的 inheritableThreadLocals 不为空,就把它 赋给当前线程(子线程)的 inheritableThreadLocals 。

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
	this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals
);

7.2、其它办法 TTL

TransmittableThreadLocal(TTL) 是阿里巴巴开源的基于 InheritableThreadLocal 的增强版,解决了线程池等场景下 InheritableThreadLocal 的值传递问题。TTL 通过重写 ThreadLocal 的方法和使用特定的数据结构,确保子线程能够正确获取父线程最新的值。它还提供了自定 义初始化方法和支持传递不可序列化对象的功能。


8、ThreadLocal 和 synchronized 的区别

ThreadLocalsynchronized
原理同步机制采用 “以空间换时间” 的方式,为每一个线程都提供了一份变量的副本,从而实现多线程同时访问而互不干扰。同步机制采用 “以时间换空间” 的方式,只提供了一份变量,让不同的线程排队访问。
侧重点多线程中让每个线程之间的数据相互隔离。多个线程之间访问资源的同步。
  • 20
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

纯纯的小白

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

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

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

打赏作者

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

抵扣说明:

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

余额充值