问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

使用ThreadLocal——解决线程共享安全问题

创作时间:
作者:
@小白创作中心

使用ThreadLocal——解决线程共享安全问题

引用
CSDN
1.
https://blog.csdn.net/qq_73617408/article/details/140610867

ThreadLocal是Java中处理线程安全问题的重要工具,它通过为每个线程提供变量的副本,避免了线程间的共享数据冲突。本文将深入探讨ThreadLocal的工作原理、使用方法以及与Synchronized的区别,帮助读者更好地理解和应用这一技术。

一、什么是ThreadLocal?

ThreadLocal是一个类,为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。

二、如何使用?

先来看看ThreadLocal的几个核心方法:

方法声明
描述
public void set(T value)
设置当前线程绑定的局部变量
public T get()
获取当前线程绑定的局部变量
Public void remove()
移除当前线程绑定的局部变量
protected Object initialValue()
初始化值

使用时需要注意:initialValue()这个方法是一个延迟调用方法,在线程第1次调用get()set(Object)时才执行,并且仅执行1次。如果没有实现该方法,则直接赋值为null,后续操作时会报NullPointerException异常。

代码证明,该代码中,我们并没有调用initialValue方法进行初始化,那么变量的值是不是null,那我们进行取数据然后+1是不是就会报错,null+1能执行吗?对不对:

三、ThreadLocal内部是如何实现的呢?

  1. set方法——设置当前线程中 ThreadLocal 变量的值:
public void set(T value) {
    //1.获取当前线程实例对象
    set(Thread.currentThread(), value);
}

private void set(Thread t, T value) {
    //2.通过当前线程获取一个ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        //3.如果map不为null,则以key为当前ThreadLocal,值为value进行存入
        map.set(this, value);
    } else {
        //4.如果map为null,则创建ThreadLocalMap
        createMap(t, value);
    }
}

上面方法的逻辑很清晰,具体请看上面的注释,通过源码我们可以知道value是存放在ThreadLocal里的,当前先把它理解为一个普普通通的map即可,也就是说,数据value是存放在ThreadLocalMap这个容器中的,并且是以当前的ThreadLocal实例为key的。

首先 ThreadLocalMap 是怎样来的?源码很清楚,是通过

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
ThreadLocal.ThreadLocalMap threadLocals;

createMap方法,其实就是new了一个ThreadLocalMap对象,然后同样以当前 ThreadLocal 实例作为 key,值为 value 存放到 ThreadLocalMap 中的,然后将当前线程对象的 ThreadLocals 赋值为 ThreadLocalMap 对象。

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

总结一下set方法:通过当前线程对象Thread获取该thread所维护的ThreadLocalMap,如果ThreadLocalMap不为null,则以ThreadLocal实例为key,值为value的键值对存入ThreadLocalMap;若为null,就新建ThreadLocalMap,然后再以ThreadLocal实例为key,值为value的键值对存入即可。

  1. get方法
public T get() {
    //1.获取当前线程实例
    return get(Thread.currentThread());
}

private T get(Thread t) {
    //2.通过当前线程获取一个ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        if (map == ThreadLocalMap.NOT_SUPPORTED) {
            return initialValue();
        } else {
            //3.获取map中以ThreadLocal实例为key的值Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //4.如果e存在,则将返回对应的value
                T result = (T) e.value;
                return result;
            }
        }
    }
    //5.若map为null或者entry为null的话,通过该方法初始化,并返回初始化的值。
    return setInitialValue(t);
}

另外,看下 setInitialValue 主要做了些什么事情?该方法中就调用了initialValue()方法

private T setInitialValue(Thread t) {
    T value = initialValue();
    ThreadLocalMap map = getMap(t);
    assert map != ThreadLocalMap.NOT_SUPPORTED;
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal<?> ttl) {
        TerminatingThreadLocal.register(ttl);
    }
    return value;
}

并且你自己没有重写initialValue()方法的话,它默认返回null

protected T initialValue() {
    return null;
}

总结一下get方法:通过当前线程 thread 实例获取到它所维护的 ThreadLocalMap,然后以当前 ThreadLocal 实例为 key 获取该 map 中的键值对(Entry),如果 Entry 不为 null 则返回 Entry 的 value。如果获取 ThreadLocalMap 为 null 或者 Entry 为 null 的话,就以当前 ThreadLocal 为 Key,value 为 null 存入 map 后,并返回 null。

  1. remove方法
public void remove() {
    //1.获取当前线程实例
    remove(Thread.currentThread());
}

private void remove(Thread t) {
    //2.通过当前线程获取一个ThreadLocalMap对象 
    ThreadLocalMap m = getMap(t);
    if (m != null && m != ThreadLocalMap.NOT_SUPPORTED) {
        //3.如果不为null,从map中删除以当前ThreadLocal实例为key的键值对 m.remove(this);
    }
}

总结一下remove方法:通过当前线程 thread 实例获取到它所维护的 ThreadLocalMap,如果map不为null,则从 map 中删除该 ThreadLocal 实例为 key 的键值对即可。

四、ThreadLocalMap详解

从上面的分析我们已经知道,数据其实都放在了 ThreadLocalMap 中,ThreadLocal 的 get、set 和 remove 方法实际上都是通过 ThreadLocalMap 的 getEntry、set 和 remove 方法实现的。如果想真正全方位的弄懂 ThreadLocal,就必须搞懂ThreadLocalMap。

ThreadLocalMap 是 ThreadLocal 一个静态内部类,和大多数容器一样,内部维护了一个数组(Entry 类型的 table 数组)。

private Entry[] table;

接下来看下 Entry 是什么:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

Entry 是一个以 ThreadLocal 为 key,Object 为 value 的键值对,另外需要注意的是这里的ThreadLocal 是弱引用,因为 Entry 继承了 WeakReference,在 Entry 的构造方法中,调用了 super(k)方法,会将 ThreadLocal 实例包装成一个 WeakReferenece。

五、为什么使用弱引用,而不用强引用

  1. 当key为强引用时,引用 ThreadLocal 的对象置为null,但是ThreadLocalMap中的key还强引用着ThreadLocal,那么ThreadLocal就不能被回收,最后导致内存泄漏。
  2. 当key为软引用时,引用 ThreadLocal 的对象置为null,由于ThreadLocalMap的key持有的是ThreadLocal实例的弱引用,弱引用是可以被gc回收掉的,一旦回收是不是就会发生内存泄漏,但是我们在下一次调用get、set、remove方法时,它内部会调用expungeStaleEntryfan方法的,该方法的主要作用是确保
    ThreadLocal
    实例中的引用保持最新,防止内存泄漏。注意:set、get方法不一定调用expungeStaleEntryfan方法,但是remove一定会调用

所以可以得出使用弱引用可以多一层保障

六、ThreadLocal会引发内存泄漏

内存泄漏是指对象变成了垃圾,垃圾回收器回收不了。

接下来我们使用一张图来看看Thread、ThreadLocal、ThreadMap、Entry之间的关系

注意上图中的实线表示强引用,虚线表示弱引用。Entry 中的 key 是弱引用,当 ThreadLocal 外部强引用被置为 null( ThreadLocalInstance=null )时,那么系统 GC 的时候,根据可达性分析,这个 ThreadLocal 实例就没有任何一条链路能够引用到它,此时 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。

当然,如果当前 thread 运行结束,ThreadLocal、ThreadLocalMap、Entry 没有引用链可达,在垃圾回收的时候都会被系统回收。

七、如何避免内存泄漏呢?

使用 ThreadLocal 后,及时调用其 remove 方法,手动清理不再需要的 entry。这样可以确保在外部对象不再需要时,ThreadLocalMap 中的引用也被正确释放。

八、ThreadLocal和Synchronized有什么区别

Synchronized是通过锁的机制,来保证线程安全的

ThreadLocal是为每个线程提供了变量副本,这是一种“空间换时间”的方案,虽然会让内存占用大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待,从而提高时间效率。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号