在Java中根据垃圾回收的方式不同,引用按照对象生命周期的长短分为四种,由高到低分别为强引用、软引用、弱引用和虚引用。
Java中默认的引用类型,一个对象如果具有强引用那么就没有资格被垃圾回收。
一个对象如果具有软引用,当JVM内存充足的时候和强引用并无区别,那么当JVM内存不足的时候,这个对象就会被垃圾回收。软引用可以和一个引用队列(ReferenceQueue)联合使用。如果软引用所引用对象被垃圾回收,JAVA虚拟机就会把这个软引用加入到与之关联的引用队列中。
如果一个对象只具有弱引用(即不具有强引用,软引用,虚引用),那么这个对象会被垃圾回收器标记回收。弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。弱引用指向的对象可以通过弱引用的get方法获得,因为弱引用不能阻挡垃圾回收器对其回收,所以当弱引用指向的对象被GC的时候get方法会返回null。
虚引用不会影响对象的生命周期,唯一用处就是能在对象被GC时收到系统通知,JAVA中用PhantomReference来实现虚引用。虚引用指向的对象十分脆弱,我们不可以通过get方法来得到其指向的对象。
首先ThreadLocal是一个用于创建线程本地变量的类。这个变量相对于本线程是全局的,相对于其他线程是隔离的,也就是说在不同的线程之间独立存在,一个线程无法访问和修改其他线程的ThreadLocal。
Talk is cheap,show me the code。
我们先通过一个实际的例子看一下ThreadLocal的使用。
假设有一个商城,客户下发一个订单,商城会分成多个步骤来处理这个订单(查库存,配货等),商城为每个订单分配一个唯一标识OrderID,并且在订单的各个处理步骤中都应该被随时读取。我们对于每个客户的订单处理new一个线程来表示,实际处理的步骤省略掉只是打印OrderID。
public class OrderIdHolder { public static final ThreadLocal<String> CURRENT_ORDERID = new ThreadLocal(); static String getCurrentOrderId() { return CURRENT_ORDERID.get(); } static void setCurrentOrderId(String Id) { CURRENT_ORDERID.set(Id); } static void remove() { CURRENT_ORDERID.remove(); } } public class OrderProcessingThread extends Thread { Random random = new Random(); OrderProcessingThread(String name) { super(name); } @Override public void run() { OrderIdHolder.setCurrentOrderId(getName() +" " + random.nextInt(100)); /*注意这里我们并没有显式的传递OrderId*/ BusinessService businessService = new BusinessService(); businessService.checkInventory(); businessService.ship(); OrderIdHolder.remove(); } public static void main(String args[]) { Thread threadOne = new OrderProcessingThread("ThreadA"); threadOne.start(); Thread threadTwo = new OrderProcessingThread("ThreadB"); threadTwo.start(); } } public class BusinessService { public void checkInventory() { System.out.println("checkInventory " + OrderIdHolder.getCurrentOrderId()); } public void ship() { System.out.println("ship " + OrderIdHolder.getCurrentOrderId()); } } 复制代码
结果:
checkInventory ThreadB 18 checkInventory ThreadA 42 ship ThreadA 42 ship ThreadB 18 复制代码
如上所示,虽然我们并没有显式的将OrderId传递到checkInventory和ship方法内,但是同一个订单处理(同一个线程)的两个方法获得的OrderId均相同,但是不同的订单处理(不同线程)的OrderId是不同的。可以看到ThreadLocal是每个线程“独自持有一份儿的”,两个线程其实是有两份儿不一样的ThreadLocal。
从上面的例子可以体会到, 当一个实例,不被允许在多个线程间共享,但是对于每个线程来说不同的类与方法都需 要共享并经常访问这个实例的时候,应该使用ThreadLocal 。
你可能会有疑问,我们存储变量时明明是只有一个CURRENT_ORDERID(ThreadLocal),为什么每个线程会自己有一份儿呢?下面我们一起揭开ThreadLocal的神秘面纱。
我们直接从ThreadLocal使用时的核心方法set入手。
public class ThreadLocal<T> { 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(Thread t) { return t.threadLocals; } } public class Thread implements Runnable { ThreadLocal.ThreadLocalMap threadLocals = null; } 复制代码
调用set方法时,先是取得了当前的线程,然后调用getMap方法,取得了一个Map,从这里可以看出ThreadLocal本身并不存储变量的值,数据实际存放在Thread内的一个Map里面,也就是说数据实际都是存放在各个线程本身的,使用者调用ThreadLocal的set()方法其实最终都是对这个Map进行操作的。ThreadLocal只是为我们操作这个Map提供了一个便捷入口。可以看到ThreadLocalMap的初始值是null,第一次ThreadLocal的set方法时会调用createMap(t, value)。
public class ThreadLocal<T> { …… private final int threadLocalHashCode = nextHashCode(); private static final int HASH_INCREMENT = 0x61c88647; private static AtomicInteger nextHashCode = new AtomicInteger(); private Entry[] table; private int size = 0; private int threshold; private static int nextHashCode() { return nextHashCode.getAndAdd(HASH_INCREMENT); } void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); } …… static class ThreadLocalMap { private Entry[] table; private int size = 0; private int threshold; private static final int INITIAL_CAPACITY = 16; ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } private void setThreshold(int len) { threshold = len * 2 / 3; } } } 复制代码
上面的代码中可以看出ThreadLocalMap其实就是一个依赖数组实现,定制化的HashTable。ThreadLocal对象实例作为Key用于定位数据实际在Entry数组中的下标,下标的值为ThreadLocal对象的threadLocalHashCode经过位运算取模得到(不太清楚原理的同学请参考https://blog.csdn.net/actionzh/article/details/78976082)。在下标处放入相应数据后,把当前Entry数组已存放数据的个数(size)设置为1,并把Threshold设置为当前容量的2/3,这个值在进行扩容时会作为判断条件使用。 除了第一次调用ThreadLocalMap的createMap(t, value)初始化ThreadLocalMap(实际上是ThreadLocalMap底层的Entry数组),以后都会调用ThreadLocalMap的set方法存放数据。
static class ThreadLocalMap { private void set(ThreadLocal<?> key, Object value) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { // 注释1:Entry的get方法 ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; return; } if (k == null) { replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } static class Entry extends WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } } 复制代码
上面代码中的注释1处我们看到调用了Entry对象的get方法,返回的类型是ThreadLocal,那么这里是什么意思呢?我们先来看一下Entry这个类。可以看到继承了WeakReference这个类,那么可以看到Entry的key是一个指向了ThreadLocal对象的弱引用。如果指向的对象被GC掉了(前面说过,弱引用是不会影响其指向对象的GC的),那么Entry对象的get方法就会返回null,可以由此来判断其指向的ThreadLocal对象是否已经无用被用户“弃用”。
上面这段代码总体逻辑比较简单,先根据ThreadLocal对象计算出以此为key的Entry应该放置在Entry数组中的Index,如果这个Index处没有Entry,直接放置,如果已经放置了Entry也即slot不为空,那么就说明两个Entry的key映射到了一个地方,也就是散列表产生了冲突,此时采用线性探测法解决冲突来探测空的slot。探测的过程中,如果查找到了目标key的Entry,直接替换value为我们的目标value即可。比较重点的地方是线性探测的过程中如果遇到了位置i此slot处的Entry的key指向的ThreadLocal已经被GC掉了,那么就将i与待插入的Entry作为参数传递给replaceStaleEntry方法,并执行然后直接return。
replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) 复制代码
replaceStaleEntry方法具体都做了什么呢?replaceStaleEntry会将作为其参数传递来的Entry存放在Entry数组的staleSlot处,并会清除夹在staleSlot前后两个null之间的一连串Entry中所有key为null(即指向的ThreadLocal已经被GC)的Entry。听起来有些绕,为了降低描述的复杂度,引入两个名词。
现在重新表述一下replaceStaleEntry的作用:将参数传递来的Entry存放在Entry数组的staleSlot(函数第三个参数)处,并清除Entry数组中staleSlot所在run中所有的Stale Entry。 代码读到这里我们可以大致画出ThreadLocal的整体原理图了:
下面我们就来看一下这个replaceStaleEntry的具体实现。
private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) { Entry[] tab = table; int len = tab.length; Entry e; int slotToExpunge = staleSlot; // 第一阶段 for (int i = prevIndex(staleSlot, len); (e = tab[i]) != null; i = prevIndex(i, len)) if (e.get() == null) slotToExpunge = i; // 第二阶段 for (int i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) { ThreadLocal<?> k = e.get(); if (k == key) { e.value = value; tab[i] = tab[staleSlot]; tab[staleSlot] = e; if (slotToExpunge == staleSlot) slotToExpunge = i; // 第三阶段 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); return; } if (k == null && slotToExpunge == staleSlot) slotToExpunge = i; } tab[staleSlot].value = null; tab[staleSlot] = new Entry(key, value); if (slotToExpunge != staleSlot) // 第三阶段 cleanSomeSlots(expungeStaleEntry(slotToExpunge), len); } 复制代码
根据ThreadLocalMap的set方法中replaceStaleEntry调用的情况,这三个参数分别代表要插入Entry的key,value以及线性探测法解决哈希冲突扫描的过程中遇到的第一个StaleEntry的Index。 进入方法内部查看具体的实现,可以发现replaceStaleEntry本身只负责了将Entry放到staleSlot处,实际上当前run的staleEntry清理操作交给了expungeStaleEntry方法,replaceStaleEntry内只是为expungeStaleEntry方法的调用做了准备工作。 简单来说此方法主要做了两件事:
情况1:
情况2: 情况3: 情况4: 第一阶段 情况1: 在run中从staleSlot处出发向前扫描,如果发现staleEntry那么将扫描过程中排在最前面的staleEntry的Index赋值给slotToExpunge。情况2,3,4: 在run中从staleSlot处出发向前扫描,没有发现staleEntry,不做任何事情。
接下来在run中从staleSlot处出发向后扫描,扫描过程中对于每一个slot内的数据:
1.先判断是否是具有目标key的Entry,如果是,将其value设置成将要插入的Entry的value并与staleSlot处的staleEntry交换(注意交换后当前slot处的Entry就是staleEntry了)。交换后判断slotToExpunge == staleSlot成立则说明此run内当前Slot位置前并无staleEntry。当前slot的Index就是当前run中的第一个staleEntry的Index,也即后续清扫工作的起始Index,将其赋值给slotToExpunge。如果slotToExpunge!=staleSlot说明此时slotToExpunge的值已经是当前run中第一个staleEntry的Index了,那就不对slotToExpunge值做更改。slotToExpunge被确定后,停止继续向后扫描,进入到第三阶段将slotToExpunge传递给清理函数进行staleEntry的清扫工作,然后return。
2.如果当前slot内的数据不是具有目标key的Entry,判断当前slot内的Entry如果满足是staleEntry并且slotToExpunge == staleSlot,那么就代表当前Entry的Index是当前run除了staleSlot外的第一个staleEntry的Index,也即后续清扫工作的起始Index,将其赋值给slotToExpunge。 在第二阶段的末尾,如果扫描过程中没有扫描到具有目标key的Entry,那么直接将要插入的Entry放到staleSlot处,如果此时slotToExpunge!=staleSlot, 说明当前run中有staleEntry并且slotToExpunge是第一个staleEntry,也即后续清扫工作的起始Index,那么进入第三阶段将slotToExpunge参数传递给清理函数进行staleEntry的清扫工作。如果slotToExpunge == staleSlot则证明当前run没有需要清理的staleEntry就不进入第三阶段。
总结一下:第二阶段的任务就是a.在第一阶段从staleSlot处向前扫描的基础上,向后扫描最终确定进行StaleEntry清扫工作的起始Index->slotToExpunge。 b.将待插入的Entry放到StaleEntry处。c.根据slotToExpung的值与staleSlot的值的相等关系来判断是否进入第三阶段。 注 :slotToExpunge值的更改,都是判断slotToExpunge==staleSlot成立后才进行的,因为初始值slotToExpunge就是等于staleSlot的,这样可以保证slotToExpunge的值只有在遇见当前run内除了staleSlot处外第一个staleEntry的时候才会更改,保证了slotToExpunge的值是当前run中的第一个staleEntry的Index,也即后续清扫工作的起始Index。
情况1: 按照上述步骤,情况1的第二阶段如图所示:(注意一点,扫描到具有目标key的Entry后,这个阶段的三个任务都已经完成,将不继续向后扫描,直接进入第三阶段)。
情况2: slotToExpunge会变成交换前的目标key所在位置,此时slotToExpunge为当前run的第一个StaleEntry的Index,如下图所示:(同样扫描到具有目标key的Entry后,不继续向后扫描,直接进入第三阶段。) 情况3: 在当前run从satleSlot向后扫描,slotToExpunge置为当前run除了staleSlot处外第一个StaleEntry的Index,并将待插入的Entry放到staleSlot位置。经判断staleSlot不等于slotToExpunge,表示当前run有staleEntry需要被清理,将其传递给expungeStaleEntry清扫方法,进行第三阶段staleEntry大清扫工作。如图所示: 情况4: 没有扫描到具有目标key的Entry以及staleEntry,将待插入的Entry放到staleSlot位置。判断一下staleSlot等于slotToExpunge,意味着当前的run并没有staleEntry需要清理(staleSlot处已经放置Entry),说明当前run很干净不用清扫,不做任何操作,不进入第三阶段。如图所示: 第三阶段情况1,情况2,情况3: 执行expungeStaleEntry清扫方法,进行staleEntry大清扫工作。 情况4: 未进入第三阶段。
前面说过,replaceStaleEntry把需要插入的数据放到了staleSlot处后,只是做了调用expungeStaleEntry前的准备工作,即扫描到了需要清理部分的最开始的位置,并当作参数staleSlot传递给了expungeStaleEntry方法,真正进行清扫工作的是expungeStaleEntry。
private int 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; } } } return i; } 复制代码
此方法清理了从staleSlot开始当前run内所有的staleEntry,让指向其的引用指向null,使其变成不可达对象,以便JVM垃圾回收。从staleSlot处开始清理,当前Entry如果是staleEntry就清理掉,如果是正常的Entry,但不在根据它的key计算的原本应在的Index处,说明这个Entry当时插入的时候产生了哈希冲突,目前所在位置的Index是线性探测后找到的位置。目前所在位置前经过清扫工作后可能会整理出很多空Slot(可用位置),将当前Entry前移整理到[Index(Cal),Index(Cur) ]这个闭区间内的第一个空Slot处,这样可以提高下次调用get方法查找此Entry的效率。此方法的返回值是整理后的run的末尾Slot的Index。
注:Index(Cal)代表根据它的key计算的原本应在的Index,Index(Cur)代表其目前所在的根据线性探测后找到的位置Index。
在replaceStaleEntry方法中,expungeStaleEntry和cleanSomeSlots都是成对出现的,expungeStaleEntry会将返回的当前run末尾的slot传递给cleanSomeSlots,cleanSomeSlots会尝试向后扫描logn次,如果发现了stale Entry那么将n置为table的长度len,做一次连续段的清理(expungeStaleEntry)(这里n是用来进行scan control的,初始值就为table的长度len,重置为table的长度len,意味着循环不会退出,会继续扫描下去,直到连续扫描(logn)+1次都没有遇见staleEntry)。如果至少有一个stale Entry被成功清理了,那么cleanSomeSlots就返回true否则就返回false。
private boolean cleanSomeSlots(int i, int n) { boolean removed = false; Entry[] tab = table; int len = tab.length; do { i = nextIndex(i, len); Entry e = tab[i]; if (e != null && e.get() == null) { n = len; removed = true; i = expungeStaleEntry(i); } } while ( (n >>>= 1) != 0); return removed; } 复制代码
这个方法除了会在replaceStaleEntry中expungeStaleEntry清理完成后调用,也会在set方法中当一个新元素添加后调用。
static class ThreadLocalMap { private int size = 0; private int threshold; private void setThreshold(int len) { threshold = len * 2 / 3; } ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { table = new Entry[INITIAL_CAPACITY]; int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); table[i] = new Entry(firstKey, firstValue); size = 1; setThreshold(INITIAL_CAPACITY); } private void set(ThreadLocal<?> key, Object value) { ...... tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } } 复制代码
添加新元素后,会再做一次启发式清理cleanSomeSlots,此时如果没有stale Entry被清理掉,并且size达到了threshold临界值,那么就有容量不够的风险,rehash会再次进行清理扩容。为什么cleanSomeSlots清理成功就不需要进行sz >= threshold的判断了呢? 首先我们来证明一件事情, 我们把ThreadLocalMap的set方法里面的
int sz = ++size; 复制代码
代码块记为 ,
if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); 复制代码
代码块记为 。
现在我们要证明,程序第n次运行完 时,size<=threshold总成立(n为全体正整数)。证明过程如下所示:
1.n=1时,size=0+1=1,threshold=16*2/3=10,size<=threshold成立。
2.若n=k时,程序第k次运行完 时,size<=threshold成立。 则n=k+1时,根据set代码的逻辑可知,第k次 执行完成到k+1次 执行完成之间,一定会执行一次
综上所述,k+1时也成立
3.所以结论可证 有了这个结论后,很明显就能得知,cleanSomeSlots执行前size<=threshold,如果cleanSomeSlots返回true那么size一定是小于threshold的,所以就不用判断sz >= threshold这个条件了,直接就可以认定不需要rehash。
get方法比较简单,我们来简单的分析一下:
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(); } private T setInitialValue() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } protected T initialValue() { return null; } private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e); } private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null) expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; } 复制代码
可以看到get方法也是取得了当前线程内的map,然后使用ThreadLocal对象作为key查找相应的Entry,并返回Entry的value值。 整体逻辑比较简单,有两点需要注意一下:
ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer> (){ @Override protected Integer initialValue() { return new Integer(1); } }; 复制代码
从ThreadLocalMap的set方法代码中我们可以看出来, 其搜索可能具有目标key的Entry,范围只是局限在根据它的ThreadLocal对象实例计算出的Index值(即其原本应该放置的Index处)处所在的run当中。 首先我们来思考一下,Entry数组的多个run是如何形成的? 只有两种情况