目录
1- 什么是ThreadLocal ?
- ThreadLocal是Java中的一个工具类,它提供了线程局部变量,即这些变量对于使用它的每个线程来说都是独立的。每个线程都可以通过ThreadLocal存储、访问和更新自己的变量副本,而不会与其他线程的变量副本冲突。
2- ThreadLocal的作用?
ThreadLocal 两大作用:
- ① 线程间资源隔离
- ② 线程内资源共享
ThreadLocal实现线程间资源隔离
public class ThreadLocalExample {
// 创建一个 ThreadLocal 实例,用于存储每个线程的计数器
private static final ThreadLocal<Integer> threadLocalCounter = ThreadLocal.withInitial(() -> 0);
public static void main(String[] args) {
// 创建三个线程,每个线程都会更新自己的计数器
for (int i = 0; i < 3; i++) {
Thread thread = new Thread(() -> {
// 获取当前线程的计数器
int counter = threadLocalCounter.get();
// 更新计数器的值
counter += 5;
// 将更新后的值重新设置到 ThreadLocal 中
threadLocalCounter.set(counter);
// 打印当前线程的计数器值
System.out.println(Thread.currentThread().getName() + " counter: " + threadLocalCounter.get());
}, "Thread-" + i);
thread.start();
}
}
}
输出:
分析
- threadLocalCounter是一个ThreadLocal实例,它通过withInitial方法初始化,为每个线程提供了一个初始值0。
- 在输出结果中,我们可以看到每个线程都打印出了counter: 5 ,这表明每个线程都成功地将自己的计数器从0增加到了5,而且这个增加操作没有影响到其他线程的计数器。这正是ThreadLocal实现线程间资源隔离的效果。
ThreadLocal实现线程内资源共享
public class ThreadLocalExample2 {
// 创建一个 ThreadLocal 实例,用于存储每个线程的会话信息
private static final ThreadLocal<String> threadLocalSession = new ThreadLocal<>();
public static void main(String[] args) {
// 创建两个线程,模拟两个用户的会话
for (int i = 1; i <= 2; i++) {
final int userId = i;
Thread thread = new Thread(() -> {
// 在 ThreadLocal 中设置当前线程的会话信息
String session = "UserSessionForUser" + userId;
threadLocalSession.set(session);
// 执行线程内的不同方法,它们都可以访问到同一个会话信息
performAction("add item to cart");
performAction("checkout");
performAction("logout");
// 清理资源,避免内存泄漏
threadLocalSession.remove();
}, "Thread-for-User-" + userId);
thread.start();
}
}
// 模拟一个操作,该操作需要访问会话信息
private static void performAction(String action) {
// 从 ThreadLocal 获取当前线程的会话信息
String session = threadLocalSession.get();
System.out.println(Thread.currentThread().getName() + " performing action: " + action + " with " + session);
}
}
输出:
分析
- 在输出结果中,我们可以看到每个线程(例如Thread-for-User-1和Thread-for-User-2)都能够连续执行add item to cart、checkout 和 logout操作,并且每个操作都使用了与该线程关联的会话信息(UserSessionForUser1和UserSessionForUser2)。这表明ThreadLocal确实实现了同一线程内的资源共享。
3- ThreadLocal 原理
3-1 ThreadLocalMap
- **实际上 ThreadLocal 实现了资源的关联,本质上是通过 **
ThreadLocalMap
实现的线程间资源隔离
其原理是,每个线程内有一个ThreadLocalMap
类型的成员变量,用来存储资源对象
- ① 调用set方法,就是以ThreadLocal自己作为key,资源对象作为value,放入当前线程的 ThreadLocalMap集合中
- ② 调用get方法,就是以ThreadLocal自己作为key,到当前线程中查找关联的资源值
- ③ 调用remove方法,就是以ThreadLocal自己作为 key,移除当前线程关联的资源值
3-2 ThreadLocalMap的扩容
🔑1. 为什么会发生扩容?
- 一个线程可以拥有多个
ThreadLocal
对象,每个ThreadLocal
对象都可以存储在同一个线程的ThreadLocalMap
中,而且彼此独立。 - 这就是为什么 ThreadLocalMap 的容量是16(默认初始容量)并且具有扩容机制的原因。扩容机制确保了当一个线程使用多个 ThreadLocal 对象时,ThreadLocalMap 能够适应更多的条目。
例子
public class ThreadLocalExample {
// 创建两个不同的 ThreadLocal 实例
private static final ThreadLocal<String> threadLocal1 = new ThreadLocal<>();
private static final ThreadLocal<String> threadLocal2 = new ThreadLocal<>();
public static void main(String[] args) {
// 在同一个线程中为不同的 ThreadLocal 实例设置值
threadLocal1.set("Value for ThreadLocal 1");
threadLocal2.set("Value for ThreadLocal 2");
// 每个 ThreadLocal 实例的值是独立的
System.out.println("ThreadLocal 1 contains: " + threadLocal1.get());
System.out.println("ThreadLocal 2 contains: " + threadLocal2.get());
}
}
- 在这个例子中,我们有两个
ThreadLocal
实例。即使它们都在同一个线程中使用,每个 ThreadLocal 实例也会在 ThreadLocalMap 中占据不同的槽位。因此,threadLocal1.set()
和threadLocal2.set()
操作不会相互覆盖,因为它们是两个独立的条目。 - 如果一个线程使用的
ThreadLocal
实例数量超过了ThreadLocalMap
的当前容量,ThreadLocalMap
就会根据需要进行扩容,以便为新的ThreadLocal
实例提供空间。
🔑2. ThreadLocalMap索引计算?
- 索引计算:当线程每创建一个新的
ThreadLocal
对象时候,会为当前ThreadLocal
对象分配一个 哈希值,最初哈希值为0
因此,如下图ThreadLocal1
插入的位置为0
- 此时如果又创建
ThreadLocal2
会在 0 的基础上加一个 数字,会根据这个数字计算出当前ThreadLocal2
的索引,比如计算出索引为7
所以,ThreadLocal2
存储在索引为7
的位置 - 同理对于
ThreadLocal3
,计算出下标位11
🔑3. ThreadLocalMap扩容?
ThreadLocalMap
的 capacity 为 16ThreadLocalMap
的 扩容因子 为 2/3
因此大概在什么时候发生扩容?
- 16 * 2/3 =10.6
- 大约在第 10 个元素时,发生扩容,容量翻倍
若当前容量为16,则插入第 10 个元素时候,会发生扩容,扩容后容量为之前两倍,且会重新计算索引
- 当
ThreadLocalMap
扩容时,它的容量(即内部数组的大小)增加,通常是翻倍。由于哈希表的索引是通过哈希码与当前容量相关的运算得到的,增加容量后,原有元素的索引可能会发生变化。这是因为索引计算通常涉及到哈希码与容量的某种运算(如模运算),容量的改变意味着这个运算的结果也可能改变。 - 为了保持哈希表的正确性和效率,扩容后需要重新计算所有元素的索引,并将它们放置到正确的位置。这个过程称为“重新哈希”(
rehashing
)。重新哈希确保了元素在新的内部数组中仍然按照其哈希码分布,同时也解决了由于容量增加而可能导致的哈希冲突减少的情况。
3-3 ThreadLocalMap如何解决哈希冲突?
- HashMap、ConcurrentHashMap、HashTable 都是使用拉链法解决哈希冲突
- ThreadLocalMap使用的开放寻址法来解决哈希冲突
例如对于上述插入结果,如果再新加入一个 ThreadLocal11,固定哈希码从 0 开始,此时发现 0 位置被 ThreadLocal1占用,此时会寻找下一个地址,即寻找到 1 的位置,因此 ThreadLocal11 会插入到 索引为 1 的位置。
4- ThreadLocalMap 键的弱引用
- 可以看到如下
ThreadLocalMap
的源码,键和值为一个Entry
类型 Entry
继承了 弱引用,并在构造中调用super
,因此key
是一个弱引用类型
static class Entry extends WeakReference<ThreadLocal<?>>
- Entry的构造函数
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
🔑为什么ThreadLocal的key设计成弱引用?
- ① Thread 可能需要长时间运行(如线程池中的线程),如果 key 不再使用,需要在内存不足(GC)时释放其占用的内存
- ② 但 GC 仅是让 key 的内存释放,后续还要根据 key 是否为 null 来进一步释放内存
- 获取 key 发现 null key
- set key 时,会使用启发式扫描,清除临近的 null key,启发次数与元素个数,是否发现 null key 有关
- remove时(推荐),因为一般使用 ThreadLocal 时都把它作为静态变量,因此 GC 无法回收
5- ThreadLocal 中 value 内存回收
- 假设有如下场景,ThreadLocal中的 key 被 GC (用黑色表示key内存被释放了),而此时 三个 value值还存在,则值释放的场景有三种
❌1.值回收场景1:get(不推荐)
- 获取 key 发现 null key,此时对应的内存也会被释放,比如 get操作执行到 索引 7 的位置此时发现 7 位置的 key 为null,此时会释放掉 value2的内存,同时会放进去一个新 key
❌2.值回收场景2:set(不推荐)
- set key 时,会使用启发式扫描,清除临近的 null key,启发次数与元素个数,是否发现 null key 有关
- 启发式是指
- ① 如果元素个数多,则扫描范围大一点
- ② 是否发现 null key,如果发现 null key 则扫描范围大一点
get 方法和 set方法有局限性
- get方法:当调用get方法时,如果对应的ThreadLocal对象已经被回收,ThreadLocalMap中的条目的键会是null。虽然get方法会检查并清理掉这些键为null的条目,但如果不频繁调用get方法,这些条目就会一直存在,占用内存。
- set方法:set方法在添加新的线程局部变量时,会尝试清理已经被回收的ThreadLocal对象的条目(键为null的条目)。然而,如果一个线程不再设置新的线程局部变量,那么这种清理机制就不会被触发。
总结来说,虽然get和set方法在一定程度上可以减轻内存泄漏的问题(通过清理键为null的条目),但它们不能完全解决问题,因为它们依赖于对ThreadLocal变量的频繁访问。只有通过显式调用remove方法,才能确保ThreadLocalMap中相关的条目被及时清理,从而避免内存泄漏。
🔑3.值回收场景3:remove 防止内存泄漏
ThreadLocalMap
中使用的 key 为ThreadLocal
的弱引用,而value
是强引用。所以,如果ThreadLocal
没有被外部强引用的情况下,在垃圾回收的时候,key
会被清理掉,而value
不会被清理掉。- 这样一来,
ThreadLocalMap
中就会出现key
为null
的 Entry。假如我们不做任何措施的话,value
永远无法被 GC 回收,这个时候就可能会产生内存泄露。 ThreadLocalMap
实现中已经考虑了这种情况,在调用set()
、get()
、remove()
方法的时候,会清理掉 key 为 null 的记录。使用完ThreadLocal
方法后最好手动调用remove()
方法
补充知识
Java中的弱引用和强引用
- 强引用(Strong Reference)
- 强引用是Java中最常见的引用类型。如果一个对象具有强引用,垃圾回收器绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出
OutOfMemoryError
错误,使程序异常终止,也不会回收这种对象。 - 只要obj引用存在,所指向的对象就不会被回收。
Object obj = new Object(); // obj是一个强引用
- 软引用(Soft Reference)
- 软引用是用来描述一些还有用但非必需的对象。在系统将要发生内存溢出异常之前,会把这些对象列入回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
- 软引用可以用来实现内存敏感的高速缓存。
Object obj = new Object();
SoftReference<Object> softRef = new SoftReference<Object>(obj);
obj = null; // 取消强引用
- 弱引用(Weak Reference)
- 弱引用也是用来描述非必需对象的,但是其强度比软引用更弱一些,被弱引用关联对象只能生存到下一次垃圾回收发生之前。当垃圾回收器工作时,无论当前内存空间足够与否,都会回收掉只被弱引用关联的对象。
- 弱引用通常用于实现规范映射(Canonicalizing mappings)等,例如WeakHashMap。
Object obj = new Object();
WeakReference<Object> weakRef = new WeakReference<Object>(obj);
obj = null; // 取消强引用
ThreadLocal的弱引用
- 当一个类的构造器中调用了super(k),并且这个类是WeakReference的子类,这意味着这个类在构造时将传入的对象 k 作为一个弱引用来处理。
- 因此在 ThreadLocalMap 源码中的 key 是一个弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}