在多线程编程中,数据共享是一个常见的话题。很多时候我们需要多个线程共享同一个数据,以实现数据的一致性和通信。然而,在一些场景下,我们并不希望数据被多个线程共享,而是希望每个线程都拥有自己独立的一份数据。这时,ThreadLocal 就可以派上用场了。
1.ThreadLocal是什么?
ThreadLocal 是 Java 中的一个类,它提供了线程本地变量的功能。简单来说,就是为每一个使用该变量的线程提供了一个独立的变量副本。这样一来,每个线程都可以独立地改变自己的副本,而不会影响到其他线程。
2.ThreadLocal的使用方法
2.1常用方法
// 创建ThreadLocal对象
public ThreadLocal() {}
// 设置当前线程绑定的局部变量
public void set(T value) {}
// 获取当前线程绑定的局部变量
public T get() {}
// 删除当前线程绑定的局部变量
public void remove() {}
2.2使用案例
下面是一个简单的示例,展示了如何使用 ThreadLocal:
public class ThreadLocalExample {
static ThreadLocal<Integer> localVariable = new ThreadLocal<>();
public static void main(String[] args) {
Runnable task = () -> {
// 设置当前线程的localVariable值
localVariable.set(Thread.currentThread().getId());
System.out.println("线程 " + Thread.currentThread().getId() + " 的局部变量: " + localVariable.get());
};
Thread thread1 = new Thread(task);
Thread thread2 = new Thread(task);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程的局部变量: " + localVariable.get());
}
}
在这个例子中,每个线程都有一个 localVariable 的独立副本,它们互不影响。
3.ThreadLocal的内部结构
ThreadLocal 类的内部结构比较简单。它主要由两个部分组成:
1. ThreadLocal 对象本身,持有每个线程的引用。
- Thread 对象中有一个 ThreadLocalMap,用于存储线程与 ThreadLocal 变量之间的映射关系。
当调用 ThreadLocal 的 get() 方法时,会根据当前线程找到对应的 ThreadLocalMap 并返回相应的值。如果调用 set() 方法,则会在当前线程的 ThreadLocalMap 中设置一个键值对。
4.核心方法及源码分析
ThreadLocal 提供了几个常用的方法:
- public T get(): 获取当前线程的变量副本。
- public void set(T value): 设置当前线程的变量副本。
- public void remove(): 移除当前线程的变量副本。
4.1 set方法
public void set(T value) {
// 获取当前线程
Thread t = Thread.currentThread();
// 获取线程绑定的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
// 不为空直接赋值
map.set(this, value);
} else {
// 为空则创建Map
// 将当前线程t作为key,将value作为值,存放ThreadLocalMap中
createMap(t, value);
}
}
4.2 get方法
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取当前线程关联的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果map存在
if (map != null) {
// this是当前ThreadLocal
// 以当前ThreadLocal为key,获取存放的entry对象
ThreadLocalMap.Entry e = map.getEntry(this);
// 获取到存放的值,直接返回
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 进行初始化操作
// 1.如果map不存在,则表示当前线程没有关联ThreadLocalMap
// 2.如果e不存在,则当前ThreadLocal没有关联entry
return setInitialValue();
}
// 初始化方法
private T setInitialValue() {
// 获取空值
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取当前线程关联的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// map不为空,直接赋值
if (map != null) {
map.set(this, value);
} else {
// map不存在,创建map并赋值
createMap(t, value);
}
// 判断是否是该类型TerminatingThreadLocal
if (this instanceof TerminatingThreadLocal) {
// 注册
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
// 返回值
return value;
}
4.3 remove方法
public void remove() {
// 获取当前线程绑定的ThreadLocalMap
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
// 不为空,删除key对应的entry实体
m.remove(this);
}
}
5. ThreadLocalMap源码分析
5.1 存储结构Entry
// ThreadLocalMap中存放数据的键值对
// 继承WeakReference(弱引用)
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value; // 具体存放的值
// 构建Entry对象 key为弱引用
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
5.2 构造函数
// 初始阈值16
private static final int INITIAL_CAPACITY = 16;
// ThreadLocalMap中存放的Entry对象
private Entry[] table;
// Entry数组的长度
private int size = 0;
// firstKey:entry中的key
// firstValue:entry中的value
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
// 初始化Entry数组,容量是16
table = new Entry[INITIAL_CAPACITY];
// 通过key进行hash运算,获取存放的下标
int i = firstKey.threadLocalHashCode &(INITIAL_CAPACITY - 1);
// 初始化entry,并设置key、value
table[i] = new Entry(firstKey, firstValue);
// 设置初始大小
size = 1;
// 设置阈值参数
setThreshold(INITIAL_CAPACITY);
}
// 设置调整阈值,用于扩展table时使用
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
5.3 ThreadLocalMap.set()方法
private void set(ThreadLocal<?> key, Object value) {
// ThreadLocalMap中存放数据的Entry数组
Entry[] tab = table;
// 数组长度
int len = tab.length;
// 对key进行hash运算,获取存放下标
int i = key.threadLocalHashCode & (len-1);
// 1.tab[i]不为空,直接替换value结束循环
/** 2.tab[i]不为空,出现hash冲突,
* 则执行 e = tab[i = nextIndex(i, len)],
去i+1下标 **/
// 使用线性探测法查找元素
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
// key是e的引用对象,则返回true
if (e.refersTo(key)) {
e.value = value;
return;
}
// 如果e为空,则代表之前的ThreadLocal被回收了
if (e.refersTo(null)) {
// 用新元素替换旧元素,防止内存泄漏
replaceStaleEntry(key, value, i);
return;
}
}
// 构建新的entry对象,存放tab[i]
tab[i] = new Entry(key, value);
// 个数累加
int sz = ++size;
/**
* cleanSomeSlots用于清除那些e.get()==null的元素,
* 这种数据key关联的对象已经被回收,所以这个Entry(table[index])可以被置null。
* 如果没有清除任何entry,并且当前已达到了扩容(长度的2/3),那么进行rehash(执行一次全表的扫描清理工作)
*/
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
5.4 关于ThreadLocal出现内存泄露原因
笔者看过较多的答案都是说ThreadLocalMap的key是虚引用,gc执行后key引用的对象回收,导致内存泄漏。但是,如果ThreadLocalMap的key改成强引用,那么gc执行后,key的引用仍然存在,仍然无法进行回收,内存泄露仍然存在。根本原因在于Entry中的value没有销毁。
笔者认为真正解决的办法应该是如下两点:
- 在使用完ThreadLocalMap中存放的变量后,要手动remove
- 等待线程执行完毕
6.总结
通过本文的介绍,我们了解到了 ThreadLocal 的概念、作用以及其实现原理。ThreadLocal 不仅简化了多线程编程中的数据管理,还能提高程序的并发性能。