Java学习————————ThreadLocal

        ThreadLocal 是 Java 中一个非常重要的线程级别的变量隔离机制,它提供了线程局部变量,使得每个线程都可以拥有自己独立的变量副本,从而避免了多线程环境下的共享变量竞争问题。

        ThreadLocal 的实现原理主要依赖于:
        (1)ThreadLocalMap:每个 Thread 对象内部都有一个 ThreadLocalMap 实例
        (2)弱引用键:ThreadLocalMap 使用 ThreadLocal 对象作为键,且是弱引用
        (3)变量副本存储:实际的值存储在线程自己的 ThreadLocalMap 中

        由于ThreadLocal 提供了线程本地的实例,所以每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

    其简单的视线如下:

// ThreadLocal使用案例
public class ThreadLocalTest {
    // 用于存储消息的列表,ArrayList();
    // 定义一个静态的 ThreadLocal 变量,初始值通过 ThreadLocal.withInitial 方法创建新实例
    // 每个线程第一次访问时会调用 ThreadLocalTest::new 创建一个独立的 ThreadLocalTest 实例
    public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);
    // 静态方法:向当前线程的 ThreadLocalTest 实例中添加消息
    public static void add(String message) {
        // 获取当前线程的 ThreadLocalTest 实例,并向其 messages 列表添加消息
        holder.get().messages.add(message);
    }
    // 静态方法:清除当前线程的 ThreadLocal 并返回之前存储的消息
    public static List<String> clear() {
        // 获取当前线程的消息列表
        List<String> messages = holder.get().messages;
        // 移除当前线程的 ThreadLocal 值(重要:防止内存泄漏)
        holder.remove();

        // 打印移除后新实例的消息列表大小(应该是0,因为每次 get() 会创建新实例)
        System.out.println("size: " + holder.get().messages.size());
        return messages;
    }
    public static void main(String[] args) {
        // 向当前线程的 ThreadLocal 中添加一条消息
        ThreadLocalTest.add("歪比歪比,歪比巴卜");
        // 打印当前线程存储的消息
        System.out.println(holder.get().messages);
        // 清除 ThreadLocal 并获取消息
        ThreadLocalTest.clear();
    }
}

        ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用,所以在GC回收之后会被回收,但由于ThreadLocal本身是强引用,ThreadLocal 被回收会导致其key虽然为null,但是value不是,导致出现内存泄露)。
        每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。
        ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。其采用的hash算法是使用了斐波那契数实现,代码如下:

/**
 * ThreadLocal 线程本地变量实现类
 * @param <T> 存储的变量类型
 */
public class ThreadLocal<T> {
    // 当前ThreadLocal实例的哈希码,用于在ThreadLocalMap中计算索引位置
    private final int threadLocalHashCode = nextHashCode();
    // 原子计数器,用于生成全局唯一的哈希码增量
    private static AtomicInteger nextHashCode = new AtomicInteger();

    /**
     * 黄金分割数(2^32 * (√5-1)/2)
     * 这个特殊数值能让哈希分布更均匀,减少碰撞
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * 生成下一个哈希码
     */
    private static int nextHashCode() {
        // 原子操作获取并增加HASH_INCREMENT
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
    /**
     * ThreadLocal内部自定义的哈希表
     * 采用线性探测法解决哈希冲突
     */
    static class ThreadLocalMap {
        /**
         * 构造方法
         * @param firstKey 第一个ThreadLocal键
         * @param firstValue 要存储的第一个值
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 初始化Entry数组(默认容量16)
            table = new Entry[INITIAL_CAPACITY];
            // 计算第一个键的存储位置:
            // 1. 使用ThreadLocal的threadLocalHashCode
            // 2. 通过 & 操作取模(INITIAL_CAPACITY-1 相当于取模运算)
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

            // 创建Entry并放入计算得到的位置
            table[i] = new Entry(firstKey, firstValue); 
            // 设置当前元素数量
            size = 1;
            // 计算扩容阈值(默认长度*2/3)
            setThreshold(INITIAL_CAPACITY);
        }
    }
}

        不过虽然采用这种方法会大大减少Hash冲突的概率,但仍有可能产生,由于其没有链表结构,所以其场上冲突采用的方法是:产生冲突后,线性向后查找,一直找到Entrynull的槽位才会停止查找,将当前元素放入此槽位中。

        其有get和set两个方法,流程分别如下:

        首先是set方法:

(1)获取当前线程对象:Thread t = Thread.currentThread()
(2)使用getMap方法获取线程的ThreadLocalMap:
(3)getMap() 方法实际上返回的是线程对象的 threadLocals 属性
(4)判断map是否存在:
(5)如果map存在:调用 map.set(this, value) 将当前ThreadLocal实例作为key,value作为值存入map
(6)如果map不存在:调用 createMap(t, value) 创建新的ThreadLocalMap并存储初始值

        get则如下:

(1)获取当前线程对象:Thread t = Thread.currentThread()
(2)获取线程的ThreadLocalMap:ThreadLocalMap map = getMap(t)
(3)判断map是否存在且包含当前ThreadLocal的entry:
(4)如果存在:返回对应的value
(5)如果不存在:调用 setInitialValue() 进行初始化
(6)调用 initialValue() 获取初始值(默认返回null,可重写)
(7)类似于set()方法,将初始值存入ThreadLocalMap


        ThreadLocalMap 的扩容机制基于内部数组的大小,当数组中的元素数量达到一定的阈值时,ThreadLocalMap 会进行扩容操作。具体而言,ThreadLocalMap 是一个数组,其中每个元素的 key 是 ThreadLocal 的弱引用,value 是线程本地存储的值。

        当元素数量达到一定比例时,ThreadLocalMap 会扩展其存储容量,通常通过加倍数组大小来避免频繁的扩容操作。在扩容过程中,系统会重新分配一个更大的数组,并将原有的元素重新计算哈希位置并复制到新数组中。需要注意的是,在扩容时,弱引用的 ThreadLocal 被垃圾回收后,相关的 key 会被置为 null,因此不会因为无效的 key 导致内存泄露。然而,由于 ThreadLocal 本身是强引用,若线程的 ThreadLocal 被回收,但 ThreadLocalMap 中的 value 没有被及时清理,可能会导致内存泄露问题。

        ThreadLocalMap 中的过期 key 清理机制主要是为了防止内存泄漏和有效管理线程本地存储的空间。ThreadLocal 的 key 是弱引用,这意味着当 ThreadLocal 对象没有强引用时,垃圾回收器会回收它,从而导致 ThreadLocalMap 中的 key 被置为 null。为了避免这些被回收的 ThreadLocal 键值对继续占用内存,ThreadLocalMap 会实施过期 key 的清理机制。常见的清理策略包括探测式清理和启发式清理。

1. 探测式清理
        探测式清理是指在访问 ThreadLocalMap 时,主动检查每个条目的 key 是否为 null。如果某个条目的 key 为 null,这表明对应的 ThreadLocal 对象已被 GC 回收。此时,ThreadLocalMap 会立即清理掉该条目的 value,并将其从映射表中移除,从而避免内存泄漏。

        优点:

        低开销:探测式清理只在访问时发生,因此只有在需要访问对应的 ThreadLocal 时才会进行清理,不会频繁的占用系统资源。
        延迟清理:清理时机较为灵活,只有在访问过期条目时才会进行清理。
缺点:

        可能导致清理不及时:如果某个 ThreadLocal 被回收,但该条目很长时间没有被访问到,可能导致内存中的无效条目长时间存在。
        2. 启发式清理
        启发式清理是基于一定的触发条件来主动清理过期的条目。常见的触发条件包括:

        扩容时:当 ThreadLocalMap 需要扩容时,内部会遍历现有的条目,清除所有 key 为 null 的条目。
        定期清理:某些实现可能会定期扫描整个 ThreadLocalMap,主动检查并移除过期条目。
在扩容过程中,ThreadLocalMap 会创建一个新的、更大的数组,并将旧数组中的元素复制到新数组中。在这个过程中,系统会检查每个条目的 key 是否为 null,并清除这些无效的条目,从而减少内存占用。

        优点:

        及时清理:启发式清理确保过期的条目尽早被清除,防止无效条目长时间占用内存。
        主动管理:这种方法在扩容或定期清理时都会进行有效的资源管理,减少了内存泄漏的风险。
        缺点:

        额外开销:启发式清理会带来额外的性能开销,尤其是在扩容时,遍历整个 ThreadLocalMap 可能导致一定的延迟。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值