深入Threadlocal原理

ThreadLocal通过在每个线程中维护独立的变量副本实现线程隔离,解决线程安全问题。其内部使用ThreadLocalMap存储,Entry使用弱引用避免内存泄漏。文章详细解读了ThreadLocal的get()和set()方法,以及ThreadLocalMap的初始化、扩容和内存清理机制,强调了手动调用remove()的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Threadlocal

Threadlocal简介

Threadlocal是通过在每个线程维护自己的变量来实现线程隔离,所有它一般会用于不需要线程共享时解决线程安全问题

Threadlocal源码解读

Threadlocal的get()方法

源码

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 拿到当前线程的threadLocals实例
    ThreadLocalMap map = getMap(t);
    // 判断threadLocals是否初始化
    if (map != null) {
        // 通过this当前ThreadLocal对象实例调用ThreadLocalMap的getEntry()取出结果(后续文章详解)
        ThreadLocalMap.Entry e = map.getEntry(this);
        // 取得出值则返回
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // threadLocals未初始化或threadLocals通过this取出的值为null,则进行初始化
    return setInitialValue();
}

// 初始化方法
private T setInitialValue() {
    // 获取initialValue()的值(初始值),创建ThreadLocal实例时可以通过重写的方式赋上初始值
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取线程的threadLocals
    ThreadLocalMap map = getMap(t);
    // 如果threadLocals尚未初始化,则执行createMap,否者直接设置值
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    // 最后返回从initialValue()获取的初始值
    return value;
}

// 创建(初始化)ThreadLocalMap,并通过firstValue设置初始值
void createMap(Thread t, T firstValue) {
    // 这里涉及到ThreadLocalMap后续文章详解
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

流程图

在这里插入图片描述

总结

  • Threadlocal实际是通过ThreadlocalMap里的一个数组容器进行存储的,重点也都到里面

ThreadlocalMap部分源码解析

基本属性

源码

//继承WeakReference实现对k的弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
//默认初始容器大小
private static final intINITIAL_CAPACITY= 16;
//数组容器
private Entry[] table;
//容器中数据的数量
private int size = 0;
//扩容的临界值,默认为容器大小的2/3
private int threshold;

ThreadLocal的内存泄露问题

在这里插入图片描述

  • 每个Thread对象会引用一个ThreadlocalMap对象,ThreadLocal会引用Entry对象,而Entry对象中的key会引用Threadlocal对象,若是强引用,在栈中的Threadlocal对象引用销毁时,key还是会引用堆中Threadlocal对象,GC不会回收Threadlocal对象,容易造成内存泄漏。而如果是弱引用就会被回收。一定要记得手动使用remove()方法!!!

初始化

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            //利用hash值定位数组坐标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

    //获取hash值
    private final int threadLocalHashCode = nextHashCode();

    //利用原子类保证线程安全
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    //哈希值的黄金分割点
    private static final int HASH_INCREMENT = 0x61c88647;
    
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

set()方法

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // 计算key的下标
    int i = key.threadLocalHashCode & (len-1);
    
    // 开放寻址方式向后遍历
    for (Entry e = tab[i];
         e != null;  // 直到找到Entry为null
         e = tab[i = nextIndex(i, len)]) {  // nextIndex环状向后遍历(不存在越界问题,到末尾为设置为开头)
        // 获取当前Entry的key
        ThreadLocal<?> k = e.get();
        
        // 情况一:当前key和需要设置的key相同,直接更新值
        if (k == key) {
            e.value = value;
            return;
        }
        
        // 情况二:遇到过期的key,进行替换过期数据操作
        if (k == null) {
            // 替换过期数据,底层调用了启发式清理和探测式清理
            replaceStaleEntry(key, value, i);
            return;
        }
    }
    
    // 情况三:找到空的Entry,直接加入并更新size
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 最后调用一次启发式清理方法,清理过期的Key。清理完成后如果size还是超过了扩容阈值则进行rehash操作
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
}

流程图

在这里插入图片描述

get() and remove()

  • get()定位坐标与set()类似,特殊点在key==null时执行探测式清理
  • remove()定位坐标与set()类似,在清除后执行探测式清理

探测式

源码

// 探测式清理 staleSlot开始清理下标
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 1. 清理当前位置的Entry同时更新size
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;
    
    Entry e;
    int i;
    // 2. 从开始位置向后遍历Entry,直到遇到Entry为null时停止
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
         
        // 获取当前的key
        ThreadLocal<?> k = e.get();
        // 判断是否为过期数据
        if (k == null) {
            // 是过期数据则直接清理当前Entry,同时更新size
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 不是过期数据则再次获取Entry对于的下标
            int h = k.threadLocalHashCode & (len - 1);
            // 判断获得的下标是否是当前位置相同,不相同则重新定位
            if (h != i) {
                // 先置空当前位置
                tab[i] = null;

                // 从获取的下标处开始向后开放寻址找到能存放的下标
                while (tab[h] != null)
                    h = nextIndex(h, len);
                // 最后存储进空的Entry中
                tab[h] = e;
            }
        }
    }
    return i;
}

流程图

在这里插入图片描述

总结

  • 简单来说他的过程,就是从当前位置开,清除过期数据,对未过期的数据重新进行位置计算,直到遍历到空entry为止

启发式清理

源码

// 启发式清理 (i表示开始下标,n表示数组长度)
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    // 从开始下标位置向后循环遍历,只会循环 数组长度的对数(比如长度为16则循环4次)
    do {
        i = nextIndex(i, len);
        Entry e = tab[i];
        // 判断当Entry不为空且Entry又为过期数据时
        if (e != null && e.get() == null) {
            n = len;
            removed = true;
            // 从当前下标启动探测式清理
            i = expungeStaleEntry(i);
        }
    } while ( (n >>>= 1) != 0); // 每次将n >>> 1 (即 n / 2)
    return removed;
}

流程图

在这里插入图片描述

总结

  • 当添加新元素set()或删除另一个过时元素时,将调用此函数。它执行对数扫描次数作为 不扫描(保留过期数据)与元素数量成比例的扫描次数 之间的平衡,使其能够清除过期数据。

扩容

源码

private void rehash() {
    // 通过从头循环的方式判断是否有过期数据,有则执行探测式清理
    expungeStaleEntries();
    // 如果清理后的Entry数量size大于阈值的3/4时则执行扩容操作
    if (size >= threshold - threshold / 4)
        resize();
}
private void expungeStaleEntries() {
    Entry[] tab = table;
    int len = tab.length;
    // 从头遍历,遇到过期数据,则从当前位置进行探测式清理
    for (int j = 0; j < len; j++) {
        Entry e = tab[j];
        if (e != null && e.get() == null)
            expungeStaleEntry(j);
    }
}

总结

  • rehash() 触发条件:set()执行最后当启发式清理完毕后 Entry的数量size >= 扩容阈值threshold(Entry数组的长度的2/3) 时触发
  • resize() 触发条件:rehash()执行并且expungeStaleEntries()底层调用的探测式清理完毕后 Entry的数量size >= 扩容阈值的3/4 时触发
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值