在Java中根據垃圾回收的方式不一樣,引用按照對象生命週期的長短分爲四種,由高到低分別爲強引用、軟引用、弱引用和虛引用。java
Java中默認的引用類型,一個對象若是具備強引用那麼就沒有資格被垃圾回收。 數組
一個對象若是隻具備軟引用,當JVM內存充足的時候和強引用並沒有區別,那麼當JVM內存不足的時候,這個對象就會被垃圾回收。軟引用能夠和一個引用隊列(ReferenceQueue)聯合使用。若是軟引用所引用對象被垃圾回收,JAVA虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。 緩存
若是一個對象只具備弱引用(即不具備強引用,軟引用,虛引用),那麼這個對象會被垃圾回收器標記回收。弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。弱引用指向的對象能夠經過弱引用的get方法得到,由於弱引用不能阻擋垃圾回收器對其回收,因此當弱引用指向的對象被GC的時候get方法會返回null。 微信
虛引用不會影響對象的生命週期,惟一用處就是能在對象被GC時收到系統通知,JAVA中用PhantomReference來實現虛引用。虛引用指向的對象十分脆弱,咱們不能夠經過get方法來獲得其指向的對象。dom
首先ThreadLocal是一個用於線程建立本身線程本地變量的類。這個變量相對於本線程是全局的,相對於其餘線程是隔離的,也就是說在不一樣的線程之間獨立存在,一個線程沒法訪問和修改其餘線程的線程本地變量。即使兩個線程執行同一段代碼,在代碼中都使用了ThreadLocal來建立線程本地變量,那它們也只能看到並訪問本身線程的本地變量,並不能相互看到對方的線程本地變量。ide
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());
}
}
複製代碼
結果:this
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方法或get方法試圖訪問操做這個Map時會調用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,這個值在進行擴容時會做爲判斷條件使用。 除了第一次訪問操做Map調用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方法的調用作了準備工做。 簡單來講此方法主要作了兩件事:
replaceStaleEntry方法執行前初始狀態
第二階段
接下來在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後,這個階段的三個任務都已經完成,將不繼續向後掃描,直接進入第三階段)。
狀況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次
執行完成之間,必定會執行一次
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是如何造成的? 只有兩種狀況
聲明: 本文由CodePlus_小小的我原創,採用 CC BY 3.0 CN協議進行許可。 可自由轉載、引用,但需署名做者且註明文章出處。如轉載至微信公衆號,請在文末添加做者公衆號二維碼。