彻底搞清楚ThreadLocal与弱引用
共 5322字,需浏览 11分钟
·
2021-04-01 11:20
众所周知,多线程访问同一个竞态变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种规避多线程访问出现线程不安全的方法,它实现了一种机制,这种机制可以复制一份竞态变量的副本,每个线程只访问一份副本,从而避免了对竞态变量的直接操作,消除了并发问题。那么ThreadLocal的作用原理是什么呢?下面我们将用一段代码来揭开ThreadLocal的面纱。
一、ThreadLocal基本原理
示例代码如下:
public class ThreadLocalDemo {
public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public static void main(String[] args) {
ThreadLocalDemo.threadLocal.set("hello world main");
System.out.println("创建新线程前,主线程" + Thread.currentThread().getName() + "的threadlocal字符值为:" + ThreadLocalDemo.threadLocal.get());
try {
Thread thread = new Thread() {
@Override
public void run() {
ThreadLocalDemo.threadLocal.set("new thread");
System.out.println("新线程" + Thread.currentThread().getName() + "的threadlocal字符值为:" + ThreadLocalDemo.threadLocal.get());
}
};
thread.start();
thread.join();
} catch (Exception e) {
System.out.println(e);
}
System.out.println("创建新线程后,主线程" + Thread.currentThread().getName() + "的threadlocal字符值为:" + ThreadLocalDemo.threadLocal.get());
}
}
代码的逻辑很简单:在主类中定义了一个静态变量threadLocal,在主线程中先设置这个变量的字符值为"hello world main",随后在主线程中创建一个新线程,并在新线程的run方法中修改threadLocal的字符值为“new thread”,然后主线程再把threadlocal的字符值打印一次。为了确保新线程一定会在主线程第二次打印前打印threadlocal的值,这里采用join方法,让新线程强行“加塞”,阻塞主线程,直到新线程执行完run方法后,主线程才解除阻塞,继续打印。执行结果如下:
从结果上来看,新线程对threadlocal字符值的修改,并没有影响到主线程的threadlocal的字符值的变化,即使threadlocal的类型是static的。这说明,新线程所修改的threadlocal是一份主线程的threadlocal的副本,那么这一点是怎么实现的呢?下面我们就一行行分析源代码来了解,首先我们先看main方法中的第一行代码:
ThreadLocalDemo.threadLocal.set("hello world main");
我们点进这个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的类,并且通过一个叫getMap(t)的方法来获取这个类的一个实例,随后把threadLocal变量的地址作为key,以字符值为value存放在这个map中。如果map为空的话,会调用createMap(t,value)来创建一个map,我们点进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);
}
threadLocals是Thread类的一个静态成员变量,它的类型是Thread的一个静态内部类ThreadLocalMap,如下所示:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
我们用图来总结一下上面的步骤,程序启动时,执行代码:
public static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
此时,整个栈内存和堆内存的情况如下图所示:
然后,在main方法中执行:
ThreadLocalDemo.threadLocal.set("hello world main");
该过程创建新的ThreadLocalMap实例,它的key指向ThreadLocal对象,value为“hello world main”并且这个key是个弱引用(弱引用是什么以及这里为什么使用弱引用,后面会提),如下图所示:
随后,main方法中创建Thread,并在Thread方法中又调用了
ThreadLocalDemo.threadLocal.set("new thread");
因此,堆内存中将创建两个对象,一个是Thread对象,代表新线程;一个是Thread的ThreadLoaclMap的实例,如下图所示:
总结:每在一个新线程中调用一次threadLocal.set("xxx")方法,就会在堆内存中创建一个新的ThreadLocalMap实例,这个实例通过Entry的方式保存key和value,value是不同的,而key都指向同一个ThreadLocal对象。
二、为什么使用弱引用
我们知道java的引用分为强、软、弱、虚四种类型,其他类型因篇幅有限,暂且不表。只说说弱引用,弱引用的定义是:如果一个对象仅被一个弱引用指向,那么当下一次GC到来时,这个对象一定会被垃圾回收器回收掉。观察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的key继承了弱引用,这是为什么呢?光结合定义来体会肯定无法深入体会,让我们结合图来分析一下。还是上面那张图,假设两条虚线不是弱引用,而是强引用,如下图红线所示:
此时,假设我们在主线程或者新线程中添加一行代码 :
ThreadLocalDemo.threadLocal = null;
即我们主动释放掉对ThreadLocalDemo.threadLocal 在两个线程中的引用,结果如下图所示:
我们可以看到,虽然两个线程都主动释放掉了对ThreadLocal对象的引用,但是,从主线程thread引用->ThreadLocal对象,依然存在这一条可达路径。众所周知,现今主流JVM判断一个对象是否可回收的算法通常为可达路径算法,而不是引用计数法。可达路径算法以GCROOT出发,如果存在一条通向某个对象的强引用通路,那么这个对象是永远不会回收掉的(即便发生OOM也不会回收)。thread的引用是主线程的一个本地变量,根据GCROOT算法,thread的引用是可以作为一个GCROOT的,那么现状就是:我们显式地释放掉了threadLocal的引用(ThreadLocalDemo.threadLocal = null;),因为我们确认后续我们不会使用到它了,但是,由于存在GCROOT的一条可达通路,程序并没有像我们希望的那样立刻释放掉ThreadLocal对象,直到我们所有的线程都释放掉了,即程序结束,ThreadLocal对象才会被真正的释放掉,这无疑就是内存泄露。为了解决这个问题,我们把图中的红线换成弱引用,如下图所示
本文来自知乎--冒蓝火加特林