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_INCREMENTreturn 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冲突的概率,但仍有可能产生,由于其没有链表结构,所以其场上冲突采用的方法是:产生冲突后,线性向后查找,一直找到Entry
为null
的槽位才会停止查找,将当前元素放入此槽位中。
其有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 可能导致一定的延迟。