Java中的ThreadLocal源码及弱引用解析
作者:好奇的7号
引言
我们知道在通常情况下,对于主存中的变量,每一个线程都能够访问并修改该变量(或对象)。
与之相对的,如果我们需要实现每个线程拥有自己专属的本地变量,该如何操作呢?
此时引出ThreadLocal类,通过ThreadLocal可以实现全局变量在多线程环境下的线程隔离。
每个线程都可以独立地访问和修改自己的全局变量副本,不会影响其他线程的副本。
这在某些场景下可以简化代码的编写和理解。
源码解析
示例代码
我们先从一段简单的代码示例入手:
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对象的值),即,实现了线程范围内的局部变量的作用。
线程独享原因
源码解析
首先我们可以推测,如果要保证每个线程独享一份数据,那这份数据应该要能够从线程内部进行引用。
实际上,线程的栈中存放了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的值,起到了惰性删除释放内存的作用。
到此这篇关于Java中的ThreadLocal源码及弱引用解析的文章就介绍到这了,更多相关ThreadLocal源码及弱引用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!