本文主要是介绍Java 面试题:从源码理解 ThreadLocal 如何解决内存泄漏 ConcurrentHashMap 如何保证并发安全 --xunznux,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- ThreadLocal
- ThreadLocal 的基本原理
- ThreadLocal 的实现细节
- 内存泄漏
- 源码
- 使用场景
- ConcurrentHashMap 怎么实现线程安全的
- CAS初始化源码
- 添加元素putVal方法
ThreadLocal
ThreadLocal 是 Java 中的一种用于在多线程环境下存储线程局部变量的机制,它可以为每个线程提供独立的变量副本,从而避免多个线程之间的竞争条件。ThreadLocal 在实际应用中,特别是在需要在线程间共享资源的场景下,发挥着重要作用。
ThreadLocal 的基本原理
ThreadLocal 的核心概念是为每个线程维护一个独立的变量副本。当一个线程通过 ThreadLocal 访问某个变量时,实际上访问的是属于该线程的独立副本。ThreadLocal 通过以下几个关键点实现了这一点:
- 每个线程持有自己的 ThreadLocalMap:
每个线程内部都有一个 ThreadLocalMap 对象,这个对象存储了 ThreadLocal 变量及其对应的值。ThreadLocalMap 是 Thread 类中的一个成员变量,因此它与线程的生命周期绑定。 - ThreadLocalMap** 的结构**:
ThreadLocalMap 是一个定制的哈希表,它的键是 ThreadLocal 对象,而值是对应的线程局部变量的值。每个线程持有的 ThreadLocalMap 可以存储多个 ThreadLocal 变量。 - 变量的存取过程:
- 当线程调用 ThreadLocal 的 get() 方法时,ThreadLocal 会获取当前线程持有的 ThreadLocalMap,然后通过自身(作为键)从 ThreadLocalMap 中获取变量的值。
- 当线程调用 ThreadLocal 的 set() 方法时,ThreadLocal 会将变量的值存储到当前线程持有的 ThreadLocalMap 中。
ThreadLocal 的实现细节
- ThreadLocal类:
ThreadLocal 本身只是提供了一套访问接口,它内部依赖于 ThreadLocalMap 来存储和获取线程局部变量。 - ThreadLocalMap的实现:
- ThreadLocalMap 是一个内部类,它的结构类似于一个简化的哈希表。ThreadLocalMap 使用了一个简单的开放地址法来处理哈希冲突。
- 每个键值对的键是一个 ThreadLocal 的弱引用(WeakReference),这有助于避免内存泄漏:当 ThreadLocal 对象被回收后,键会变成 null,相应的值也会被清理。
- ThreadLocalMap的垃圾回收:
由于 ThreadLocalMap 使用了弱引用,ThreadLocal 对象不会阻止其被垃圾回收机制回收。当 ThreadLocal 对象被回收后,ThreadLocalMap 中对应的键会变成 null,但是值仍然会存在。这种情况下,如果不及时清理,可能会导致内存泄漏。 - remove()方法:
ThreadLocal 提供了一个 remove() 方法,可以显式地将当前线程持有的 ThreadLocal 变量移除。这有助于防止内存泄漏,特别是在使用线程池的场景下,线程会被重复利用,如果不清理,可能会导致数据污染或内存泄漏。
内存泄漏
实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。
如何避免内存泄漏:
- 每次使用完threadlocal调用remove方法清除
- 尽可能把threadlocal变量定义为static final,这样可以避免频繁创建实例。
- 内部优化
- 调用set()方法,会采用采样清理,全量清理,扩容时还能继续检查
- 调用get()方法,如果没有命中,向后环形查找时进行清理。
- 调用remove()方法,清理当前entry,还会向后清理
最好是使用完之后手动调用remove方法,这个方法底层会调用map的remove将 Entry 移除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。弱引用只是保证了 ThreadLocal 会被 GC 自动回收。
最佳做法:每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
源码
static class ThreadLocalMap {static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}}ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {table = new Entry[INITIAL_CAPACITY];int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);table[i] = new Entry(firstKey, firstValue);size = 1;setThreshold(INITIAL_CAPACITY);}private Entry getEntry(ThreadLocal<?> key) {int i = key.threadLocalHashCode & (table.length - 1);Entry e = table[i];if (e != null && e.get() == key)return e;elsereturn getEntryAfterMiss(key, i, e);}
}
该Java静态内部类Entry继承自WeakReference<ThreadLocal<?>>,主要用于存储ThreadLocal与其关联对象的弱引用及其对应的值。具体说明如下:
value:存储与ThreadLocal关联的具体值。
构造方法接收一个ThreadLocal对象和一个值,创建一个弱引用来持有ThreadLocal对象,并保存关联值。
使用场景
ThreadLocal 常用于以下场景:
- 数据库连接管理:每个线程持有一个独立的数据库连接,避免多个线程同时使用同一个连接。
- 用户上下文信息:在 Web 应用中,每个线程处理一个用户请求,可以通过 ThreadLocal 存储和访问该用户的上下文信息。
- 线程安全的对象实例:通过 ThreadLocal 为每个线程创建独立的对象实例,避免线程间的竞争条件。
注意事项 - 内存泄漏:如果 ThreadLocal 变量不及时清理,可能会导致内存泄漏,尤其是在使用线程池时要特别注意调用 remove() 方法。
- 适用场景:ThreadLocal 适合用于线程独立的数据存储,不适合跨线程的数据共享。
总结来说,ThreadLocal 是一种通过在每个线程中创建独立变量副本的方式,来实现线程隔离的工具,它的底层依赖于每个线程持有的 ThreadLocalMap 来存储这些变量副本,从而确保线程间的数据独立性和安全性。
例子:
ThreadLocalMap 中的 Key 和 Value
- Key:
- 在 ThreadLocalMap 中,Key 是 ThreadLocal 实例本身。ThreadLocal 对象作为键,指向当前线程所持有的变量。
- Value:
- Value 是由 ThreadLocal 对象所关联的值。在本例中,Value 是 Integer,存储的是每个线程独立的计数器值。
每个线程都有一个 ThreadLocalMap,这个 ThreadLocalMap 使用 ThreadLocal 实例作为键来存储和获取对应的线程局部变量值,从而实现数据隔离。
public class MultipleThreadLocalExample {// 定义两个不同的 ThreadLocal 变量private static ThreadLocal<Integer> threadLocalCounter1 = ThreadLocal.withInitial(() -> 0);private static ThreadLocal<String> threadLocalCounter2 = ThreadLocal.withInitial(() -> "Initial Value");public static void main(String[] args) {// 启动一个线程,独立操作两个 ThreadLocal 变量Thread thread = new Thread(() -> {// 操作第一个 ThreadLocal 变量threadLocalCounter1.set(threadLocalCounter1.get() + 10);System.out.println(Thread.currentThread().getName() + " - Counter1: " + threadLocalCounter1.get());// 操作第二个 ThreadLocal 变量threadLocalCounter2.set(threadLocalCounter2.get() + " Updated");System.out.println(Thread.currentThread().getName() + " - Counter2: " + threadLocalCounter2.get());});thread.start();}
}
多个 ThreadLocal 变量在同一线程中的工作机制
- ThreadLocalMap 的键区分:
- 在同一个线程中,不同的 ThreadLocal 变量对应着不同的键。这些键就是 ThreadLocal 对象本身。因此,即使在同一个线程中,每个 ThreadLocal 实例都能独立存储和访问自己的值,不会与其他 ThreadLocal 变量发生冲突。
- ThreadLocalMap 的存储结构:
- ThreadLocalMap 通过一个哈希表来存储键值对。键是 ThreadLocal 对象,值是线程局部变量的实际数据。因此,同一线程中的多个 ThreadLocal 变量不会相互覆盖或混淆。
ConcurrentHashMap 怎么实现线程安全的
采用了CAS算法(compareAndSwapObject)和部分代码使用synchronized锁保证线程安全。
对应的非并发容器:HashMap
目标:代替Hashtable、synchronizedMap,支持复合操作。
原理:JDK6中采用一种更加细粒度的加锁机制 Segment “分段锁”,JDK8中采用 volatile + CAS 或者 synchronized 。
**添加元素时首先会判断容器是否为空:
- 如果为空则使用 volatile 加 CAS 来初始化
- 如果容器不为空,则根据存储的元素计算该位置是否为空。
- 如果根据存储的元素计算结果为空,则利用 CAS 设置该节点;
- 如果根据存储的元素计算结果不为空,则使用 synchronized ,然后,遍历桶中的数据,并替换或新增节点到桶中,最后再判断是否需要转为红黑树,这样就能保证并发访问时的线程安全了**。
如果把上面的执行用一句话归纳的话,就相当于是ConcurrentHashMap通过对头结点加锁来保证线程安全的,锁的粒度相比 Segment 来说更小了,发生冲突和加锁的频率降低了,并发操作的性能就提高了。
而且 JDK 1.8 使用的是红黑树优化了之前的固定链表,那么当数据量比较大的时候,查询性能也得到了很大的提升,从之前的 O(n) 优化到了 O(logn) 的时间复杂度。
CAS初始化源码
这段 Java 代码定义了一个名为 sizeCtl 的私有变量,其类型为 int,并且被 transient 和 volatile 修饰符所修饰。这个变量在类中的作用如下:
Table 初始化和调整大小控制:
- 当 sizeCtl 的值为负数时,表示当前正在进行表的初始化或调整大小操作。
- -1 表示正在初始化表。
- 其他负数值(如 -2, -3 等)表示正在进行调整大小的操作,并且 -1 减去该值即为当前活跃的调整大小线程的数量。
初始表大小或默认值:
- 如果表(table)为空,并且 sizeCtl 的值为非负数,则该值表示创建表时应使用的初始大小。
- 如果 sizeCtl 的值为 0,则表示使用默认大小创建表。
调整大小的阈值: - 在初始化之后,sizeCtl 的值表示下次应该调整表大小时元素的数量阈值。
变量修饰符说明: - transient:表示这个变量不会被序列化。当对象被序列化成字节流时,sizeCtl 的值不会被保存。
- volatile:确保多线程环境下的可见性和有序性,即任何线程对 sizeCtl 的修改都会立即反映到其他线程中。
通过这种方式,sizeCtl 变量帮助实现了并发哈希表(如 ConcurrentHashMap)在初始化和动态调整大小过程中的控制逻辑。
private transient volatile int sizeCtl;
private final Node<K,V>[] initTable() {Node<K,V>[] tab; int sc;while ((tab = table) == null || tab.length == 0) {if ((sc = sizeCtl) < 0)Thread.yield(); // lost initialization race; just spin提示调度器当前线程愿意放弃处理器使用权else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {try {if ((tab = table) == null || tab.length == 0) {int n = (sc > 0) ? sc : DEFAULT_CAPACITY;@SuppressWarnings("unchecked")Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];table = tab = nt;sc = n - (n >>> 2);}} finally {sizeCtl = sc;}break;}}return tab;
}
该函数初始化哈希表,主要功能如下:
- 检查当前表格是否为空或长度为零。
- 使用CAS操作安全地初始化表格数组。
- 如果成功,则根据sizeCtl记录的大小创建新数组,并更新sizeCtl值。
添加元素putVal方法
/*** 核心方法,用于处理put和putIfAbsent操作* 该方法实现了哈希表的插入逻辑,包括处理哈希冲突和数据结构转换(链表转红黑树)* * @param key 键,不能为null* @param value 值,不能为null* @param onlyIfAbsent 如果为true,则仅在键不存在时进行插入* @return 插入前该键对应的旧值,如果没找到则返回null*/final V putVal(K key, V value, boolean onlyIfAbsent) {// 检查键值对是否为null,为null则抛出异常if (key == null || value == null) throw new NullPointerException();// 扩散哈希码,以减少哈希冲突int hash = spread(key.hashCode());int binCount = 0; // 用于记录链表或红黑树中的元素数量// 循环尝试在哈希表中插入值for (Node<K,V>[] tab = table;;) {Node<K,V> first; int n, i, firstHash;// 表为空时初始化哈希表(volatile和CAS)if (tab == null || (n = tab.length) == 0)tab = initTable();// 位置i处的节点为空,直接插入新节点else if ((first = tabAt(tab, i = (n - 1) & hash)) == null) {// 存储的元素在该node数组的下标位置结果为空,则利用 CAS 设置该节点if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))break; // 成功插入,跳出循环}// 位置i处的节点处于迁移中,帮助完成迁移else if ((firstHash = first.hash) == MOVED)tab = helpTransfer(tab, first);// 位置i处的节点正常,进行插入或更新操作else {V oldVal = null;// 同步锁,确保线程安全。存储的元素计算结果不为空,则使用 synchronizedsynchronized (first) {// 再次检查节点,防止并发修改if (tabAt(tab, i) == first) {// 链表形式,遍历链表找到键或插入新节点if (firstHash >= 0) {binCount = 1;for (Node<K,V> e = first;; ++binCount) {K ek;// 找到匹配的键,更新值并跳出循环if (e.hash == hash &&((ek = e.key) == key ||(ek != null && key.equals(ek)))) {oldVal = e.val;if (!onlyIfAbsent)e.val = value;break;}Node<K,V> pred = e;// 链表末尾插入新节点if ((e = e.next) == null) {pred.next = new Node<K,V>(hash, key,value, null);break;}}}// 红黑树形式,调用红黑树的插入或更新方法else if (first instanceof TreeBin) {Node<K,V> p;binCount = 2;if ((p = ((TreeBin<K,V>)first).putTreeVal(hash, key,value)) != null) {oldVal = p.val;if (!onlyIfAbsent)p.val = value;}}}}// 链表节点数超过阈值,转换为红黑树if (binCount != 0) {if (binCount >= TREEIFY_THRESHOLD)treeifyBin(tab, i);// 找到旧值,返回旧值if (oldVal != null)return oldVal;break;}}}// 更新哈希表大小和修改次数addCount(1L, binCount);return null; // 未找到旧值,返回null}
这篇关于Java 面试题:从源码理解 ThreadLocal 如何解决内存泄漏 ConcurrentHashMap 如何保证并发安全 --xunznux的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!