Java专题-ThreadLocal

111 阅读5分钟

ThreadLocal

1.是什么

ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程自己的静态变量。

线程间交互的几种方式

  • 互斥同步锁:Sync和ReentranLock
  • 非阻塞同步:CAS和AtomicXXX变量
  • 可见变量: volatile可见变量
  • 本地存储: 线程ThreadLocal,多线程环境下数据的独立性

2.使用场景有哪些

场景一:管理连接对象 Mybatis的连接对象 解决当前线程的操作都是用同一个Connection,保证了事务
  • 方案一:直接多线程获取连接,线程不安全,连接可能被其他线程关闭
  • 方案二:每个线程独自场景连接,互补影响,问题:每个线程是复用的,都要频繁的创建和关闭连接,效率低
  • 方案三:ThreadLocal变量存储

ThreadLocal在每个线程中保存一个对该变量的副本,线程间互不影响,性能也比较好

场景二:Session管理 解决对象层层传递的问题

Session管理场景中避免多重修改

方法多层堆栈调用,A->B->C-D-E,如果A中的某个变量,在E中使用,而不期望中间改变

3.原理是什么

首先找到当前线程ID,根据线程IDThreadID,获取对应的ThreadLocalMap对象,map中存放的是多个Entry,每个Entry对应一个ThreadLocal对象

            ThreadLocal<String> a = new ThreadLocal<>();
            ThreadLocal<String> b = new ThreadLocal<>();
            b.set("valueB");
            a.set("valueA");
  • 线程类中有一个ThreadLocalMap类型的变量threadLocals
  • ThreadLocalMap的Entry实现继承了WeakReference<ThreadLocal<?>>

每一次set或者get的时候,都会将key为null的Entry清除掉

使用线程池,执行完成线程任务的时候要清理remove方法

整体结构

按照上面的代码示例,整体的map存储结构如下,

KeyValue _下一层的KeyValue
threadIdThreadLocalMap
This = ThreadLocal对象value
avalueA
bvalueB

image.png

方法详解

set

ThreadLocalMap 相当于一个 HashMap,是真正保存值的地方。

Map中有一个静态内部类Entry,是WeakReference的子类使用Entry<key,Value> 来存储具体内容

key为ThreadLocal对应 value为存储的内容

如果 ThreadLocalMap 不为 null,则将 value 保存到 ThreadLocalMap 中,并用当前 ThreadLocal 作为 key;否则创建一个 ThreadLocalMap 并给到当前线程,然后保存 value。

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

如果 ThreadLocalMap 不为 null,则把获取 key 为当前 ThreadLocal 的值;否则调用 setInitialValue() 方法返回初始值,并保存到新创建的 ThreadLocalMap 中

public T get() {
    Thread t = Thread.currentThread();
    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();
}
initialValue

initialValue() 是 ThreadLocal 的初始值,默认返回 null,子类可以重写改方法,用于设置 ThreadLocal 的初始值。

private T setInitialValue() {
    T value = initialValue(); // 就是null
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
remove

用来移除当前 ThreadLocal 对应的值。其实就是map的remove,用来释放Entry中的key

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}
常见属性

没有实现Map接口,自定义Map

// 初始容量,必须是 2 的幂
private static final int INITIAL_CAPACITY = 16;
​
// 存储数据的哈希表
private Entry[] table;
​
// table 中已存储的条目数
private int size = 0;
​
// 表示一个阈值,当 table 中存储的对象达到该值时就会扩容
private int threshold;
​
// 设置 threshold 的值
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

为什么会内存泄漏

原因

假设Entry中的key没有使用弱引用,而是普通的强引用,当线程结束后,引用ThreadLoal的对象被回收后,由于还有其他线程,ThreadLocalMap中还存在着,其中有ThreadLocal(Key)-value(value)这样的引用,导致value无法回收掉,造成内存泄漏

            ThreadLocal<String> b = new ThreadLocal<>();
            b.set("");
            b = null; // 内存泄漏
      此时map中的接口
      threadLocal中   
      key = b   ->  value = String // 由于threadlocal b的存在,执行value对象,导致value对象无法被清除

而key使用了弱引用,每次gc都会被清除掉,从而避免内存泄漏

ThreadLocalMap 已经考虑到这种情况,并且有一些防护措施:在调用 ThreadLocal 的 get(),set() 和 remove() 的时候都会清除当前线程 ThreadLocalMap 中所有 key 为 null 的 value。这样可以降低内存泄漏发生的概率。所以我们在使用 ThreadLocal 的时候,每次用完 ThreadLocal 都调用 remove() 方法,清除数据,防止内存泄漏。

有哪些需要我注意

  • 每一次set或者get的时候,都会将key为null的Entry清除掉,为了防止长时间没有set,get的调用,用完手动remove
  • 使用线程池时,执行完成线程任务的时候要清理threadlocal变量,即调用remove方法,防止变量中的值异常

Java中的四种引用类型

  • 强引用

一般对象,使用new 方法创建的

  • 软引用

    特点:内存不够的时候,gc会将其回收

场景:不重要内容的缓存(例如图片)

  • 弱引用

    特点:每次执行GC都会将其回收

    场景:JVM中ThreadLocal里的Entry使用

  • 虚引用

    特点:随时可以清理

场景:JVM用于管理直接内存,在直接内存使用场景下,用于监控直接内存被回收

当直接内存清理的时候,会通过Queue进行通知,此时Queue不为空

new PhantomReference(new Object, Queue)