JUC(12) ThreadLocal
2024-01-30 23:36:14 # Language # Java

面试题

  • ThreadLocal中ThreadLocalMap的数据结构和关系
  • ThreadLocal的key是弱引用, 这是为什么?
  • ThreadLocal内存泄漏问题你知道吗?
  • ThreadLocal中最后为什么要加remove方法?

1. ThreadLocal简介

ThreadLocal 提供线程局部变量。这些变量与正常的变量不同, 因为每一个线程在访问 ThreadLocal 实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal 实例通常是类中的私有静态字段, 使用它的目的是希望将状态(例如, 用户ID或事物ID)与线程关联起来。

img

class MyData{
ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> 0);
public void add(){
threadLocal.set(threadLocal.get() + 1);
}
}

public class ThreadLocalDemo {
public static void main(String[] args) {
MyData myData = new MyData();
ExecutorService threadPool = Executors.newFixedThreadPool(3);
try {
for (int i = 0; i < 10; i++) {
threadPool.submit(() -> {
try {
Integer beforeInt = myData.threadLocal.get();
myData.add();
Integer afterInt = myData.threadLocal.get();
System.out.println(Thread.currentThread().getName() + " beforeInt: " + beforeInt + " afterInt: " + afterInt);
} finally {
myData.threadLocal.remove();
}
});
}
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
/*
pool-1-thread-1 beforeInt: 0 afterInt: 1
pool-1-thread-3 beforeInt: 0 afterInt: 1
pool-1-thread-2 beforeInt: 0 afterInt: 1
pool-1-thread-2 beforeInt: 0 afterInt: 1
pool-1-thread-3 beforeInt: 0 afterInt: 1
pool-1-thread-1 beforeInt: 0 afterInt: 1
pool-1-thread-2 beforeInt: 0 afterInt: 1
pool-1-thread-1 beforeInt: 0 afterInt: 1
pool-1-thread-3 beforeInt: 0 afterInt: 1
pool-1-thread-2 beforeInt: 0 afterInt: 1
*/

如果不在 finally 块中调用 remove 会导致原先的值没有被清理, 默认值不再是 0, 而是上一个线程设置的值

image-20240111154336936

因为每个 Thread 内有自己的实例副本且该副本只有当前线程自己使用

既然其他 ThreadLocal 不可访问, 那就不存在多线程间共享问题

统一设置初始值, 但是每个线程对这个值得修改都是各自线程互相独立的

如何才能不争抢

  • 加入synchronized或者Lock控制资源的访问顺序

  • ThreadLocal

2. ThreadLocal源码分析

2.1 Thread、ThreadLocal、ThreadLocalMap关系

image-20240111161342274

image-20240111162037251

img

  • Thread 对象维护着一个 ThreadLocalMap 的引用
  • ThreadLocalMap 是 ThreadLocal 的内部类, 用 Entry 来进行存储值
  • 调用 ThreadLocal 的 set() 方法时, 实际上就是往 ThreadLocalMap 设置值, key 是 ThreadLocal 对象, 值 Value 是传递进来的对象
  • 调用 ThreadLocal 的 get() 方法时, 实际上就是从 ThreadLocalMap 获取值, key 是 ThreadLocal 对象

2.2 源码分析

ThreadLocalMap 成员变量

/**
* The initial capacity -- MUST be a power of two.
* 初始容量——必须是2的幂。
*/
private static final int INITIAL_CAPACITY = 16;

/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
* 表可根据需要调整大小。表长度必须始终是2的幂。
*/
private Entry[] table;

/**
* The number of entries in the table.
* 表中的实体数。
*/
private int size = 0;

/**
* The next size value at which to resize.
* 扩容阈值
*/
private int threshold; // Default to 0

ThreadLocal#get()源码

/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); // 每个线程私有
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this); // 以当前ThreadLocal对象为key, 获取Entry, 其中存放了value
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

此处说明 ThreadLocalMap 采用的是懒加载模式, 用时再去初始化

ThreadLocalMap#getEntry()源码

/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

Entry 是 ThreadLocalMap 的内部类, 继承自WeakReference<ThreadLocal<?>>, 因此 e.get() 获取的就是该 Entry 对应的 ThreadLocal

算出当前 key 在数组中下标 i, 通过当前线程局部变量的线程局部哈希码与 数组长度 - 1 做与运算。根据下标 i 获取对象 e, 如果 e 不为 null 且 e 的 key 等于当前线程局部变量, 则返回 e, 否则开启线性探测

线程局部哈希码 threadLocalHashCode 源码

/**
* ThreadLocals rely on per-thread linear-probe hash maps attached
* to each thread (Thread.threadLocals and
* inheritableThreadLocals). The ThreadLocal objects act as keys,
* searched via threadLocalHashCode. This is a custom hash code
* (useful only within ThreadLocalMaps) that eliminates collisions
* in the common case where consecutively constructed ThreadLocals
* are used by the same threads, while remaining well-behaved in
* less common cases.
*/
private final int threadLocalHashCode = nextHashCode();

/**
* The next hash code to be given out. Updated atomically. Starts at
* zero.
*/
private static AtomicInteger nextHashCode =
new AtomicInteger();

/**
* The difference between successively generated hash codes - turns
* implicit sequential thread-local IDs into near-optimally spread
* multiplicative hash values for power-of-two-sized tables.
*/
private static final int HASH_INCREMENT = 0x61c88647;

/**
* Returns the next hash code.
*/
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}

这里定义了一个 AtomicInteger 类型, 每次获取当前值并加上 HASH_INCREMENT, HASH_INCREMENT = 0x61c88647, 这个值和斐波那契散列有关(这是一种乘数散列法, 只不过这个乘数比较特殊, 是2^32乘以黄金分割比例的值, 即是1640531527, 16进制表示为 0x61c88647, 其主要目的就是为了让哈希码能均匀的分布在 2 的 n 次方的数组里, 也就是 Entry[] table 中, 这样做可以尽量避免 hash 冲突

ThreadLocalMap#getEntryAfterMiss()源码

/**
* Version of getEntry method for use when key is not found in
* its direct hash slot.
*
* @param key the thread local object
* @param i the table index for key's hash code
* @param e the entry at table[i]
* @return the entry associated with key, or null if no such
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}

开启 while 循环, 当 e 的 key 与当前线程对象相等时, 返回 e

如果 e 的 key 等于 null, 开启探测式清理, 也就是 expungeStaleEntry() 方法

如果 e 的 key 不为 null, 且不等于当前线程对象, 说明哈希冲突了, 开启线性探测

ThreadLocalMap#expungeStaleEntry()源码

/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

当前位置的 key 等于 null, value 也置为 null, entry 也置为 null, size—

从当前位置的下一个位置 i 开始循环, 如果 e 等于 null, 退出循环, 否则向 i 的下一个位置继续循环。

如果 e 的 key 等于 null, value 也置为 null, entry 也置为 null, size–-。不为 null, 则计算当前线程局部变量的下标 h。如果 h 与 i (当前下标)不相等, 说明当时插入的时候发生了哈希冲突。开始 rehash, 将当前下标的 entry 清空, 从 h 开始寻找空位, 找到空位后插入当前对象。如果恰好 h 下标为 null, 那么当前线程局部变量则不会发生哈希冲突, 直接插入到 h 位置。

HashMap与ThreadLocalMap解决哈希冲突的区别:

  • HashMap 使用的是链地址法, 当发生哈希冲突的时候, 使用链表来存储相同哈希码的数据, 满足一定条件, 还可以链表红黑树互相转换
  • ThreadLocalMap 使用的是线性探测法, 当发生哈希冲突的时候, 就往后寻找空位置, 直到寻找到空位置插入。

ThreadLocal#setInitialValue()源码

/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
private T setInitialValue() {
T value = initialValue(); // 如果初始化ThreadLocal时没有重写就返回null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}

ThreadLocal#createMap()源码

/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocal#set()源码

/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}

ThreadLocalMap#set()源码

/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value; // 覆盖
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value); // 插入
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

ThreadLocal#remove()源码

/**
* Removes the current thread's value for this thread-local
* variable. If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim. This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}

ThreadLocal#remove()源码

/**
* Remove the entry for key.
*/
private void remove(ThreadLocal<?> key) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.get() == key) {
e.clear(); // this.referent = null;
expungeStaleEntry(i);
return;
}
}
}

3. ThreadLocal内存泄漏

image-20240111162037251

  • 使用 WeakReference<ThreadLocal<?>> 将 ThreadLocal 对象变成一个弱引用对象
  • 定义 Entry 类来继承 WeakReference<ThreadLocal<?>>

3.1 强、软、弱、虚引用

image-20240120185333481

Java 允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作

强引用

  • 对于强引用的对象, 就算是出现了 OOM 也不会对该对象进行回收
  • 当一个对象被强引用变量引用时, 它处于可达状态, 是不可能被垃圾回收机制回收的, 即使该对象以后永远都不会被用到, JVM 也不会回收, 因此强引用是造成 Java 内存泄露的主要原因之一
MyObject obj = new MyObject();
obj = null;
System.gc();

软引用

  • 是一种相对强引用弱化了一些的引用, 需要使用 java.lang.ref.SoftReference 类来实现, 可以让对象豁免一些垃圾回收
  • 对于只有软引用的对象而言, 当系统内存充足时, 不会被回收, 当系统内存不足时, 才会被回收
  • 软引用通常用在对内存敏感的程序中, 比如高速缓存, 内存够用就保留, 不够用就回收。
SoftReference<MyObject> softRef = new SoftReference<>(new MyObject());
softRef.get()

假如有一个应用需要读取大量的本地图片:

  • 如果每次读取图片都从硬盘读取则会严重影响性能
  • 如果一次性全部加载到内存中又可能造成内存溢出。此时使用软引用可以解决这个问题。

设计思路是: 用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系, 在内存不足时, JVM会自动回收这些缓存图片对象所占用的空间, 从而有效地避免了OOM的问题。

Map<String, SoftReference<Bitmap>> imageCache = new HashMap<>();

弱引用

  • 使用 java.lang.ref.WeakReference 类来实现, 比软引用的生命周期更短, 对于只有弱引用的对象而言, 只要垃圾回收机制一运行, 不管JVM的内存空间是否足够, 都会回收该对象占用的内存。
WeakSoftReference<MyObject> weakRef = new WeakReference<>(new MyObject());
weakRef.get()

虚引用

  • 虚引用必须和引用队列(ReferenceQueue)联合使用
    • 虚引用需要 java.lang.ref.PhantomReference 类来实现, 与其他引用都不同, 虚引用并不决定对象的生命周期。如果一个对象仅持有虚引用, 那么它就和没有任何引用一样, 在任何时候都有可能被垃圾回收器回收, 它不能单独使用也不能通过它访问对象。
  • PhantomReference#get() 总是返回 null
    • 虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一种确保对象被 finalize 后, 做某些事情的通知机制。
  • 处理监控通知使用
    • 设置虚引用关联对象的唯一目的, 就是在对象被 GC 的时候会收到一个系统通知或者后续添加进一步的处理, 用来实现比 finalize 机制更灵活的回收操作。

构造方法

PhantomReference(T referent, ReferenceQueue<? super T> q)

示例

MyObject myObject = new MyObject();
ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();
PhantomReference<MyObject> phantomReference = new PhantomReference<>(myObject, referenceQueue);

List<byte[]> list = new ArrayList<>();

new Thread(() -> {
while (true) {
list.add(new byte[1024 * 1024]);
try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
System.out.println(phantomReference.get() + " list add ok");
}
}, "t1").start();

new Thread(() -> {
while (true) {
Reference<? extends MyObject> reference = referenceQueue.poll();
if (reference != null) {
System.out.println("有虚对象回收加入了队列");
break;
}
}
}, "t2").start();

3.2 为什么使用弱引用

image-20240130222547134

为什么要用弱引用:

  • 当方法执行完毕后, 栈帧销毁, 强引用 tl 也就没有了, 但此时线程的 ThreadLocalMap 里某个 entry 的 key 引用还指向这个对象
  • 若这个 key 是强引用, 就会导致 key 指向的 ThreadLocal 对象及 v 指向的对象不能被 gc 回收, 造成内存泄露
  • 使用弱引用就可以使 ThreadLocal 对象在方法执行完毕后顺利被回收且 Entry 的 key 引用指向为 null

但如果当前线程迟迟不结束(例如线程池场景), 这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链

  • value 对象需要 ThreadLocalMap 调用 get, set 时发现 key 为 null 才会去回收整个 entry, 因此弱引用不能 100% 保证内存不泄露, 我们要在不使用某个 ThreadLocal 对象后, 手动调用 remove 方法来删除它
  • 在线程池中, 不仅仅是内存泄漏的问题, 因为线程池中的线程是重复使用的, 意味着这个线程的 ThreadLocalMap 对象也是重复使用的, 如果我们不手动调用 remove 方法, 那么后面的线程就有可能获取到上个线程遗留下来的 value 值

  • 此后调用 get, set, remove 方法时, 就会尝试删除 key 为 null 的 Entry, 从而释放 value 对象占用的内存

    • 通过 expungeStaleEntry, cleanSomeSlots, replaceStaleEntry 这三个方法回收键为 null 的 Entry 对象的值(即value)以及 entry 对象本身从而防止内存泄漏, 属于安全加固的方法