0、引言
我们知道在通常情况下,对于主存中的变量,每一个线程都能够访问并修改该变量(或对象)。与之相对的,如果我们需要实现每个线程拥有自己专属的本地变量,该如何操作呢?
此时引出ThreadLocal类,通过ThreadLocal
可以实现全局变量在多线程环境下的线程隔离。每个线程都可以独立地访问和修改自己的全局变量副本,不会影响其他线程的副本。这在某些场景下可以简化代码的编写和理解。
1. 源码解析
1.1 示例代码
我们先从一段简单的代码示例入手:
package Thread_;
public class ThreadLocal {
private static java.lang.ThreadLocal<Integer> counter = new java.lang.ThreadLocal<>();
public static void main(String[] args) {
Runnable runnable = () -> {
// 获取当前线程的计数器值,初始值为0
int count = counter.get() == null ? 0 : counter.get();
System.out.println(Thread.currentThread().getName() + " 的计数器值为: " + count);
// 对计数器进行累加操作
counter.set(count + 1);
// 模拟耗时操作
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 再次获取计数器值
count = counter.get();
System.out.println(Thread.currentThread().getName() + " 的累加后计数器值为: " + count);
};
// 创建三个线程并启动
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
Thread thread3 = new Thread(runnable);
thread1.start();
thread2.start();
thread3.start();
}
}
//结果如下:
Thread-1 的计数器值为: 0
Thread-0 的计数器值为: 0
Thread-2 的计数器值为: 0
Thread-1 的累加后计数器值为: 1
Thread-2 的累加后计数器值为: 1
Thread-0 的累加后计数器值为: 1
在代码中,我们定义了一个ThreadLocal类的整形对象counter,用三个线程进行累加操作。结果我们发现,counter的值并没有变为3,而是每个线程有一个自己的counter值,分别为1。由此引出ThreadLoca的作用:
ThreadLocal对象在每个线程内是共享的,在不同线程之间又是隔离的(每个线程都只能看到自己独有的ThreadLocal对象的值),即,实现了线程范围内的局部变量的作用。
1.2 线程独享原因:源码解析
首先我们可以推测,如果要保证每个线程独享一份数据,那这份数据应该要能够从线程内部进行引用。
实际上,线程的栈中存放了ThreadLocal.ThreadLocalMap这么一个属性(初始化为空),ThreadLocalMap是ThreadLocal的一个静态内部类,以后数据就要以ThreadLocalMap的形式在堆中实例化,并让threadLocals成为它的引用!这样就完成了线程的独享了。
public class Thread implements Runnable{
...
...
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
...
}
我们回到示例中,注意:ThreadLocal<Integer> counter这个ThreadLocal对象的set方法,跟进源码,详细解释如下:
// 对计数器进行累加操作
counter.set(count + 1);
//源码1. set源码:
public void set(T value) {
Thread t = Thread.currentThread();//获取当前线程
ThreadLocalMap map = getMap(t);//让map成为该线程内的threadLocals所引用的堆中的对象!
//见源码2.
//其实就是去尝试引用实例化的ThreadLocalMap,但此时初始化为null,所以我们看下面的判断:
if (map != null) {
map.set(this, value);//不为空,已经有map,就把堆中的值赋值为counter,count + 1
} else {
createMap(t, value);//为空,创建ThreadLocalMap的对象
//但要注意,createMap并不是让map实例化,见下面源码3.
}
}
//源码2. getMap源码:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
//意思就是,返回这个t线程的ThreadLocal.ThreadLocalMap threadLocals = null里面这个threadLocals 对象
}
//源码3. createMap源码:
//传入的this,其实就是counter。firstValue就是相应的值count + 1。
//注意,是将t.threadLocals线程内的这个ThreadLocalMap对象实例化,所以线程内部的这个对象指向了堆中内存的new...,实现了线程内部的数据独立。
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
概括:
看似是向ThreadLocal存入一个值,实际上是向当前线程对象中的ThreadLocalMap对象存入值(如果为空,则实例化当前线程对象中的ThreadLocalMap对象)ThreadLocalMap我们可以简单的理解成一个Map,Map存的key就是ThreadLocal实例本身(counter),value是具体的值。
get方法同理,也是先取出当前线程对象,再取出其指向的map里的值:
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
1.3 ThreadLocalMap的key的弱引用
源码如下:
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
通常ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal会被GC回收,不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
ThreadLocal中一个设计亮点是ThreadLocalMap中的Entry结构的Key用到了弱引用。
试想如果使用强引用,如果ThreadLocalMap的Key使用强引用,那么Key对应的ThreadLocal对象在没有被外部引用时仍然无法被GC回收,因为Key存在于ThreadLocalMap中,而且线程是长时间存活的。这就可能导致ThreadLocal对象无法被回收,从而造成内存泄漏。使用了弱引用的话,JVM触发GC回收弱引用后,ThreadLocalMap中会出现一些Key为null,但是Value不为null的Entry项,这些Entry项如果不主动清理,就会一直驻留在ThreadLocalMap中。此时,ThreadLocal在下一次调用get()、set()、remove()方法就可以删除那些ThreadLocalMap中Key为null的值,起到了惰性删除释放内存的作用。