ThreadLocal 到底是个啥?

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */

JDK 官方描述 : 该类用来提供线程内部的局部变量, 这种变量在多线程环境下访问 (通过getter/setter 方法) 时能保证各个线程的变量性对于独立于其他线程.

ThreadLocal实例通常是 private static 类型的, 用于关联线程和线程上下文

ThreadLocal 的作用

变量只在线程的生命周期内起作用, 使变量为线程私有, 各个线程的变量互不影响

使用方式:

class Data 

//    private String value;  //传统方式并不会使该变量为线程私有

// 如果该 ThreadLocal 对象不再使用, 请务必调用其 remove 方法
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
    public String getValue() {
        return threadLocal.get();
//        return value;
    }

    public void setValue(String value) {
//        this.value = value;
        threadLocal.set(value);
    }
}

ThreadLocal 与 synchronized 的区别

使用 synchronized 同样能解决多线程并发访问变量的问题, 相对而言使用 synchronized 吞吐量更低, 但 ThreadLocal 有可能导致内存泄露

synchronized ThreadLocal
原理 采用 时间换空间 的方式, 只提供一份变量, 让线程以排队的方式对变量进行访问 采用以 空间换时间 的方式, 为每一个线程都提供了一份变量的副本从而实现变量的访问而互不干扰
侧重点 多个线程之间访问变量的同步 多线程中让每个线程之间的数据隔离

具体的使用场景: 数据库的事务(银行转账)

一个事务只能使用一个 Connection 对象, 使用 ThreadLocal 就可以解决这个问题, 在每个线程从连接池中获取连接的时候, 将连接与线程绑定

还需要注意的是提交完成后需要将 Connection 对象与 ThreadLocal 解绑

底层内部结构原理

  • 每个 Thread 对象内部都有一个 ThreadLocalMap ,
  • ThreadLocalMap 是一个 Map, 其 Entry 的 key 为 ThreadLocal 对象
  • ThreadLocalMap 是由 ThreadLocal 维护的, 由 ThreadLocal 对象调用 get/set 方法获变量副本

对于不同的 Thread, 他们的 ThreadLocalMap 不一致, 每次获取变量副本的时候, 只能从本 Thread 对象中的 ThreadLocalMap 变量中获取, 形成了副本的隔离, 互不干扰

public class Thread implements Runable {
   ....
     /* ThreadLocal values pertaining to this thread. This map is maintained
      * by the ThreadLocal class. 
      */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ....
}

在这里插入图片描述

ThreadLocalMap 结构

ThreadLocalMap 是 ThreadLocal 的静态内部类, 没有实现 Map 接口, 结构如下

//初始容量, 必定为2的指数次幂
private static final int INITIAL_CAPACITY = 16;
//由Entry数组来维护Map
private Entry[] table;
//键值对的数量
private int size = 0;
//需要扩容的阈值
private int threshold; // Default to
...

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

可以看到, Entry 继承自弱引用, 并调用了 WeakReference 的构造器, 说明 Entry 的 key 作为一个弱引用指向 ThreadLocal 对象

内存泄露问题

这里需要清楚一点, 如果有一个强引用和一个弱引用同时指向一个对象, 即使发生 GC , 弱引用是不会被回收的

public static void main(String[] args){
    Data data = new Data();
    WeakReference<Data> reference = new WeakReference<>(data);
//    data = null;
    System.gc();// 即使发生 GC, reference.get() 不为 null
    System.out.println(data.hashCode());
    System.out.println(reference.get());
        
}
class Data{
}

因此, 只有使用完 ThreadLocal 对象后发生 GC, Entry 的 key 才会为 null

为什么 Entry 继承自弱引用?

主要的作用就是防止内存泄露, 为什么这么说呢? 假设 Entry 的 key 的引用类型为强引用, 那么无论如何, GC 都不会将其回收, 随着时间的推移 (Thread 对象不死亡的情况下) , 越来越多的 Entry 挂在 ThreadLocalMap 上, 导致 Thread 对象变得很大, 最终引发内存泄露

要避免内存泄漏的两种方式:

  1. 使用完 ThreadLocal, 调用器 remove 方法删除对应的 entry
  2. 使用完 ThreadLocal, 当前 Thread 也随之运行结束

相对于第一种方式, 第二种方式不好控制, 特别是使用线程池的时候, 线程结束是不会销毁的

那么, 使用弱引用就不会发生内存泄露了吗?

并非如此, 如果使用完了 ThreadLocal 对象, 此时就没有强引用指向该 ThreadLocal 对象了, 当发生 GC 后, Entry 上的 key 被回收, 值为 null, 但是 value 却不为 null, 此时 Entry 对象仍然挂在 ThreadLocalMap 上, 随着时间的推移, 仍然会导致内存泄露

那么 Entry 的 key 为弱引用的意义在哪?

事实上, 在 ThreadLocalMap 中的 set/getEntry 方法中, 会对 key 为 null 的 Entry 进行判断, 如果为 key == null 的话, 那么会相应的将该 Entry 的 value 也置空 , 这样就把 Entry 成功移除

这也就意味着, 如果使用完 ThreadLocal, 而当前 Thread 仍然在运行, 就算忘记调用 remove 方法, 弱引用比强引用多一层保障 : 虽然 key 被回收的 Entry 仍然会保留在 ThreadLocalMap 中, 但在下一次 ThreadLocalMap 调用 set/getEntry 方法时会被清除, 从而避免内存泄露

// 查看 getEntry() , 如果 e == null 或 e.get() != key, 就会调用该方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        // 当 key == null, 会将 value 置空, 同时移除 Entry
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
// set 方法在下面

hash冲突的解决方案

private void set(ThreadLocal<?> key, Object value) {
    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();

        // 如果找到相同的 key 的 Entry, 则将其 value 覆盖
        if (k == key) {
            e.value = value;
            return;
        }

        //如果 key 为 null (弱引用被回收了)
        if (k == null) {
            // 替换这个 Entry 元素 (相当于 Entry 被回收, 就不会导致内存泄露)
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    // 如果找了一圈找到位置了, 将其插入 Map 中
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 判断 Entry 的数量是否大于阈值
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        // 扩容
        rehash();
}

总结 : 使用 线性探测法 来解决 hash 冲突, 该方法一次探测下一个地址, 如果该地址没有保存 Entry 元素或者 key 与传进来的 key 一致, 则在该位置插入 (或者覆盖) 数据, 如果 key 不一致, 则发生 hash 冲突, 继续探测一个地址.

若整个数组都找不到插入的位置, 则产生溢出 (基本不会出现, 因为 Entry 的数量大于阈值后会进行扩容, 阈值为数组长度的 2/3)

private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

可以把 Entry[] table 看做是一个环形队列

猜你喜欢

转载自blog.csdn.net/Gp_2512212842/article/details/107286979