ThreadLocal
我是 javapub,一名 Markdown
程序员从👨💻,八股文种子选手。
: 你好,请问你对 ThreadLocal 有了解吗?
您好,我知道 ThreadLocal 是一个 Java 中的类,它可以让每个线程都拥有自己的变量副本,从而避免了线程安全问题。
: 非常好,那你能否详细介绍一下 ThreadLocal 的使用方法?
当然可以。ThreadLocal 的使用方法非常简单,我们只需要创建一个 ThreadLocal 对象,然后调用它的 set 方法来设置当前线程的变量值,调用 get 方法来获取当前线程的变量值即可。下面是一个简单的示例代码:
public class ThreadLocalDemo {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
threadLocal.set("Hello from thread1");
System.out.println(threadLocal.get());
});
Thread thread2 = new Thread(() -> {
threadLocal.set("Hello from thread2");
System.out.println(threadLocal.get());
});
thread1.start();
thread2.start();
}
}
这个示例代码中,我们创建了一个 ThreadLocal 对象,并在两个线程中分别设置了不同的变量值。由于每个线程都有自己的变量副本,所以这两个线程互不干扰,输出的结果也是不同的。
: 非常好,那你能否解释一下 ThreadLocal 的原理是什么?
当然可以。ThreadLocal 的原理其实很简单,它是通过一个 ThreadLocalMap 对象来存储每个线程的变量副本的。当我们调用 ThreadLocal 的 set 方法时,实际上是在当前线程的 ThreadLocalMap 对象中存储了一个键值对,其中键是当前 ThreadLocal 对象,值是我们设置的变量值。当我们调用 ThreadLocal 的 get 方法时,实际上是在当前线程的 ThreadLocalMap 对象中查找当前 ThreadLocal 对象对应的变量值。
下面是 ThreadLocalMap 的源码实现,我在代码中加了注释,希望能够帮助您更好地理解:
class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
// 初始容量为 16
private static final int INITIAL_CAPACITY = 16;
// 扩容因子为 2
private static final float LOAD_FACTOR = 0.75f;
// 存储键值对的数组
private Entry[] table;
// 数组中键值对的数量
private int size = 0;
// 下一个要清理的键值对的索引
private int threshold;
// 清理键值对的阈值
private void setThreshold(int len) {
threshold = (int) (len * LOAD_FACTOR);
}
// 获取键值对的值
private Object getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key) {
return e.value;
} else {
return null;
}
}
// 设置键值对的值
private void setEntry(ThreadLocal<?> key, Object value) {
// 清理键值对
expungeStaleEntries();
// 计算键值对的索引
int i = key.threadLocalHashCode & (table.length - 1);
// 如果该位置已经有键值对了,则往后查找空位置
for (Entry e = table[i]; e != null; e = table[i = nextIndex(i, table.length)]) {
ThreadLocal<?> k = e.get();
// 如果找到了相同的 ThreadLocal 对象,则直接替换值
if (k == key) {
e.value = value;
return;
}
// 如果找到了一个空的位置,则插入新的键值对
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 如果该位置没有键值对,则插入新的键值对
table[i] = new Entry(key, value);
int sz = ++size;
if (sz >= threshold) {
// 扩容
rehash();
}
}
// 清理过期的键值对
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int i = 0; i < len; i++) {
Entry e = tab[i];
if (e != null && e.get() == null) {
// 清理过期的键值对
expungeStaleEntry(i);
}
}
}
// 清理指定位置的键值对
private void expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清理指定位置的键值对
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 重新散列该位置之后的键值对
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;
// 往后查找空位置
while (tab[h] != null) {
h = nextIndex(h, len);
}
// 插入键值对
tab[h] = e;
}
}
}
}
// 扩容
private void rehash() {
expungeStaleEntries();
// 如果当前数组长度已经达到最大值,则不再扩容
if (size >= threshold - threshold / 4) {
return;
}
int newCapacity = table.length * 2;
Entry[] newTable = new Entry[newCapacity];
int count = 0;
for (Entry e : table) {
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
} else {
int i = k.threadLocalHashCode & (newCapacity - 1);
while (newTable[i] != null) {
i = nextIndex(i, newCapacity);
}
newTable[i] = e;
count++;
}
}
}
setThreshold(newCapacity);
size = count;
table = newTable;
}
// 计算下一个索引
private static int nextIndex(int i, int len) {
return (i + 1) % len;
}
}
: 非常好,那你能否解释一下 ThreadLocal 的优缺点是什么?
当然可以。ThreadLocal 的优点是它可以让每个线程都拥有自己的变量副本,从而避免了线程安全问题。另外,ThreadLocal 的使用方法非常简单,只需要调用 set 和 get 方法即可。
ThreadLocal 的缺点是它可能会导致内存泄漏问题。由于每个线程都有自己的变量副本,如果我们没有及时清理这些变量副本,就可能会导致内存泄漏。另外,ThreadLocal 的使用也可能会导致上下文切换的开销增加,因为每个线程都需要维护自己的变量副本。
: 非常好,你对 ThreadLocal 的了解非常深入,今天就到这里吧。
谢谢您的提问,我很高兴能够分享我的知识。
最近我在更新《面试1v1》系列文章,主要以场景化的方式,讲解我们在面试中遇到的问题,致力于让每一位工程师拿到自己心仪的offer,感兴趣可以关注公众号JavaPub追更!
🎁目录合集:
Gitee:https://gitee.com/rodert/JavaPub
GitHub:https://github.com/Rodert/JavaPub