很是感謝洋哥的本週知識分享,灰常精闢~!洋哥的知識串起來了線程安全的大部分知識,我也根據個人知識儲備及網絡搜尋,整理了一份我本身當前的理解。html
一. 線程安全性的知識準備java
1.1 知識準備a:JVM 內存模型 與 線程安全git
線程安全,就是經過多個線程對某個資源進行有序訪問或者修改,這裏的某項資源對應的底層便是一個個的 JVM 內存模型。緩存
因此,針對 線程安全 來談的 JVM 內存模型,想要實現線程安全,那麼就要實現兩點:可見性及有序性,前者保證某項線程修改某共享變量以後能夠被其它線程感知,後者保證非原子操做的線程安全。安全
什麼是 可見性?網絡
普通狀況下,當線程須要對某一共享變量進行修改時,一般會進行以下的過程:多線程
a 從主內存中拷貝變量的一份副本,並裝載到工做內存中;併發
b 在工做內存中執行代碼,修改副本的值;ide
c 用工做內存中的副本值更新主存中的相關變量值。函數
當一個共享變量在多個線程的工做內存中都有副本時,若是一個線程修改了這個共享變量,那麼其餘線程應該可以看到這個被修改後的值,這就是多線程的可見性問題。
什麼是 有序性?
非原子操做時,因爲各線程的操做都是針對 Work-Memory 的,若是多個線程時無序操做的,那麼頗有可能出如今 a,b 線程同時讀取一項共享數據,而後分別作了修改,這樣就會出現線程安全問題,舉例以下:
假設有一個共享變量x,線程a執行x=x+1。從上面的描述中能夠知道x=x+1並非一個原子操做,它的執行過程以下: 1 從主存中讀取變量x副本到工做內存 2 給x加1 3 將x加1後的值寫回主 存 若是另一個線程b執行x=x-1,執行過程以下: 1 從主存中讀取變量x副本到工做內存 2 給x減1 3 將x減1後的值寫回主存 那麼顯然,最終的x的值是不可靠的。假設x如今爲10,線程a加1,線程b減1,從表面上看,彷佛最終x仍是爲10,可是多線程狀況下會有這種狀況發生: 1:線程a從主存讀取x副本到工做內存,工做內存中x值爲10 2:線程b從主存讀取x副本到工做內存,工做內存中x值爲10 3:線程a將工做內存中x加1,工做內存中x值爲11 4:線程a將x提交主存中,主存中x爲11 5:線程b將工做內存中x值減1,工做內存中x值爲9 6:線程b將x提交到中主存中,主存中x爲9 一樣,x有可能爲11,若是x是一個銀行帳戶,線程a存款,線程b扣款,顯然這樣是有嚴重問題
1.2 volatile 關鍵字
volatile是java提供的一種輕量級同步,該關鍵字只能保證 JVM 內存模型的可見性,volatile共享變量進行寫操做的時候,會多出一條lock前綴的指令,即告知JVM:它所修飾的域的原子操做都不須要通過線程的工做內存,而直接在主內存中進行修改。這樣就保證了線程從主內存中讀取(read)它的值的時候,老是最新的,同時另外一方面,volatile 並不能保證有序性。
可是問題在於,Java 中原子操做較少,volatile 只能保證賦值操做(原子操做)的安全性,而不能保證非原子操做的安全性。
補充:lock 操做的底層含義
lock指令在多核處理器下會引起兩件事: 1. 將當前處理器緩存行(cache line)的數據寫會到系統內存 2. 這個寫會內存操做會使其它CPU緩存了該內存地址的數據無效 緩衝行:緩存中能夠分配最小存儲單位。 爲了提升處理速度,處理器不直接與內存進行通訊。而是先將系統內存的數據讀到內部緩存(L1,L2或者其餘)後再作操做。但操做完不知道什麼時候回寫到內存。 若是對生命了volatile的變量進行寫操做,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是,就算寫回到內存,若是其餘處理器緩存的值仍是舊的,再執行計算就會有問題。因此,在多處理器下,爲了保證各個處理器緩存的值是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存。
二. 線程安全性基礎
2.1 什麼樣的數據會出現線程安全問題?
a. 共享的
b. 可變的
補充知識:線程安全性有哪幾類?
Bloch 給出了描述五類線程安全性的分類方法:不可變、線程安全、有條件線程安全、線程兼容和線程對立。
1. 不可變 不可變的對象必定是線程安全的,而且永遠也不須要額外的同步[1] 。由於一個不可變的對象只要構建正確,其外部可見狀態永遠也不會改變,永遠也不會看到它處於不一致的狀態。
Java 類庫中大多數基本數值類如 Integer 、 String 和 BigInteger 都是不可變的。 須要注意的是,對於Integer,該類不提供add方法,加法是使用+來直接操做。而+操做是不具線程安全的。這是提供原子操做類AtomicInteger的原緣由。 2. 線程安全 線程安全的對象具備在上面「線程安全」一節中描述的屬性;
由類的規格說明所規定的約束在對象被多個線程訪問時仍然有效,無論運行時環境如何排線程都不須要任何額外的同步。這種線程安全性保證是很嚴格的;
許多類,如 Hashtable 或者 Vector 都不能知足這種嚴格的定義。 3. 有條件的 有條件的線程安全類對於單獨的操做能夠是線程安全的,可是某些操做序列可能須要外部同步。
條件線程安全的最多見的例子是遍歷由 Hashtable 或者 Vector 或者返回的迭代器,由這些類返回的 fail-fast 迭代器假定在迭代器進行遍歷的時候底層集合不會有變化。
爲了保證其餘線程不會在遍歷的時候改變集合,進行迭代的線程應該確保它是獨佔性地訪問集合以實現遍歷的完整性。一般,獨佔性的訪問是由對鎖的同步保證的,
而且類的文檔應該說明是哪一個鎖(一般是對象的內部監視器(intrinsic monitor))。 若是對一個有條件線程安全類進行記錄,那麼您應該不只要記錄它是有條件線程安全的,並且還要記錄必須防止哪些操做序列的併發訪問。
用戶能夠合理地假設其餘操做序列不須要任何額外的同步。 4. 線程兼容 線程兼容類不是線程安全的,可是能夠經過正確使用同步而在併發環境中安全地使用。
這可能意味着用一個 synchronized 塊包圍每個方法調用,或者建立一個包裝器對象,其中每個方法都是同步的(就像 Collections.synchronizedList() 同樣)。
也可能意味着用 synchronized 塊包圍某些操做序列。爲了最大程度地利用線程兼容類,若是全部調用都使用同一個塊,那麼就不該該要求調用者對該塊同步。
這樣作會使線程兼容的對象做爲變量實例包含在其餘線程安全的對象中,從而能夠利用其全部者對象的同步。 許多常見的類是線程兼容的,如集合類 ArrayList 和 HashMap 、 java.text.SimpleDateFormat 、或者 JDBC 類 Connection 和 ResultSet 。 5. 線程對立 線程對立類是那些無論是否調用了外部同步都不能在併發使用時安全地呈現的類。
線程對立不多見,當類修改靜態數據,而靜態數據會影響在其餘線程中執行的其餘類的行爲,這時一般會出現線程對立。
線程對立類的一個例子是調用 System.setOut() 的類
2.2 如何避免線程安全問題?
1. 不可變(有final關鍵字修飾且已被賦值)
2. 不共享(局部變量,ThreadLocal等)
3. 加鎖(synchronized、lock、ReentrantLock等)
三. TreadLocal 定義及使用
網上查了一部分關於 Threadlocal 的資料,主要是從 線程安全 解析的居多,但按照做者的原意,ThreadLocal 類的設計不只用於多線程環境下的各線程維護獨立於其它線程以外的的變量,同時也能用於關聯線程的上下文。
This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).
根據winwill2012的中文翻譯:ThreadLocal的做用是提供線程內的局部變量,這種變量在線程的生命週期內起做用,減小同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度。
其實再翻譯下也就是說:某個不便於一直傳遞的,同時是該線程所特有的資源,該資源就能夠放置在 ThreadLocal 中。
3.1 實現原理
每一個 Thread 維護一個 ThreadLocalMap 映射表,這個映射表的 key 是 ThreadLocal 實例自己,value 是真正須要存儲的 Object。
JDK 1.8 中的 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(); }
getMap 方法的源碼:
1 ThreadLocalMap getMap(Thread t) { 2 return t.threadLocals; 3 }
setInitialValue函數的源碼:
1 private T setInitialValue() { 2 T value = initialValue(); 3 Thread t = Thread.currentThread(); 4 ThreadLocalMap map = getMap(t); 5 if (map != null) 6 map.set(this, value); 7 else 8 createMap(t, value); 9 return value; 10 }
createMap函數的源碼:
1 void createMap(Thread t, T firstValue) { 2 t.threadLocals = new ThreadLocalMap(this, firstValue); 3 }
從以上的代碼中,咱們能夠大體總結出 get 方法的流程:
1. 獲取當前線程;
2. 根據當前線程獲取一個 Map;
3. 若是 Map 不爲空,根據當前線程 ThreadLocal 的飲用做爲 key ,獲取 entry e;
4. 若是 e 不爲空,返回 e.value,不然轉到5;
5. Map 爲空或者 e 爲空,則經過 initialValue 函數獲取初始值 value,而後用 ThreadLocal 的引用和 value 做爲 firstKey 和 firstValue 建立一個新的 Map
3.2 ThreadLocalMap 的回收問題
補充知識:Java 中的強引用,軟引用,弱引用,虛引用
1.強引用 之前咱們使用的大部分引用實際上都是強引用,這是使用最廣泛的引用。若是一個對象具備強引用,那就相似於必不可少的生活用品,垃圾回收器毫不會回收它。當內存空 間不足,Java虛擬機寧願拋出OutOfMemoryError錯誤,使程序異常終止,也不會靠隨意回收具備強引用的對象來解決內存不足問題。 2.軟引用(SoftReference) 若是一個對象只具備軟引用,那就相似於可有可物的生活用品。若是內存空間足夠,垃圾回收器就不會回收它,若是內存空間不足了,就會回收這些對象的內存。只要垃圾回收器沒有回收它,該對象就能夠被程序使用。軟引用可用來實現內存敏感的高速緩存。 軟引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是軟引用所引用的對象被垃圾回收,JAVA虛擬機就會把這個軟引用加入到與之關聯的引用隊列中。 3.弱引用(WeakReference) 若是一個對象只具備弱引用,那就相似於可有可物的生活用品。弱引用與軟引用的區別在於:只具備弱引用的對象擁有更短暫的生命週期。在垃圾回收器線程掃描它 所管轄的內存區域的過程當中,一旦發現了只具備弱引用的對象,無論當前內存空間足夠與否,都會回收它的內存。不過,因爲垃圾回收器是一個優先級很低的線程, 所以不必定會很快發現那些只具備弱引用的對象。 弱引用能夠和一個引用隊列(ReferenceQueue)聯合使用,若是弱引用所引用的對象被垃圾回收,Java虛擬機就會把這個弱引用加入到與之關聯的引用隊列中。 4.虛引用(PhantomReference) "虛引用"顧名思義,就是形同虛設,與其餘幾種引用都不一樣,虛引用並不會決定對象的生命週期。若是一個對象僅持有虛引用,那麼它就和沒有任何引用同樣,在任什麼時候候均可能被垃圾回收。 虛引用主要用來跟蹤對象被垃圾回收的活動。虛引用與軟引用和弱引用的一個區別在於:虛引用必須和引用隊列(ReferenceQueue)聯合使用。當垃 圾回收器準備回收一個對象時,若是發現它還有虛引用,就會在回收對象的內存以前,把這個虛引用加入到與之關聯的引用隊列中。程序能夠經過判斷引用隊列中是否已經加入了虛引用,來了解。 被引用的對象是否將要被垃圾回收。程序若是發現某個虛引用已經被加入到引用隊列,那麼就能夠在所引用的對象的內存被回收以前採起必要的行動。 特別注意,在實際程序設計中通常不多使用弱引用與虛引用,使用軟用的狀況較多,這是由於軟引用能夠加速JVM對垃圾內存的回收速度,能夠維護系統的運行安全,防止內存溢出(OutOfMemory)等問題的產生。 軟引用代碼示例: import java.lang.ref.SoftReference; public class Test { public static void main(String[] args){ System.out.println("開始"); A a = new A(); SoftReference<A> sr = new SoftReference<A>(a); a = null; if(sr!=null){ a = sr.get(); } else{ a = new A(); sr = new SoftReference<A>(a); } System.out.println("結束"); } } class A{ int[] a ; public A(){ a = new int[100000000]; } }
而在 ThreadLocal 中,ThreadLocalMap是使用ThreadLocal的弱引用做爲Key的:
1 static class ThreadLocalMap { 2 /** 3 * The entries in this hash map extend WeakReference, using 4 * its main ref field as the key (which is always a 5 * ThreadLocal object). Note that null keys (i.e. entry.get() 6 * == null) mean that the key is no longer referenced, so the 7 * entry can be expunged from table. Such entries are referred to 8 * as "stale entries" in the code that follows. 9 */ 10 static class Entry extends WeakReference<ThreadLocal<?>> { 11 /** The value associated with this ThreadLocal. */ 12 Object value; 13 Entry(ThreadLocal<?> k, Object v) { 14 super(k); 15 value = v; 16 } 17 } 18 ... 19 ... 20 }
用圖例表示爲(實線爲強引用,虛線爲弱引用):
由圖可知,若是不作任何處理,可能會發生如下狀況:
ThreadLocalMap 使用 ThreadLocal 的弱引用做爲 key,若是一個 ThreadLocal 沒有外部強引用引用他,那麼系統 gc 的時候,這個 ThreadLocal 勢必會被回收,這樣一來,ThreadLocalMap 中就會出現 key 爲 null 的 Entry,就沒有辦法訪問這些 key 爲 null 的 Entry 的 value,若是當前線程再遲遲不結束的話,這些key 爲 null 的 Entry 的 value 就會一直存在一條強引用鏈:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永遠沒法回收,形成內存泄露。
因此爲了不這種狀況,在設計中已經作了一些防禦措施:
ThreadLocalMap的getEntry方法的源碼:
1 private Entry getEntry(ThreadLocal<?> key) { 2 int i = key.threadLocalHashCode & (table.length - 1); 3 Entry e = table[i]; 4 if (e != null && e.get() == key) 5 return e; 6 else 7 return getEntryAfterMiss(key, i, e); 8 }
getEntryAfterMiss函數的源碼:
1 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { 2 Entry[] tab = table; 3 int len = tab.length; 4 while (e != null) { 5 ThreadLocal<?> k = e.get(); 6 if (k == key) 7 return e; 8 if (k == null) 9 expungeStaleEntry(i); 10 else 11 i = nextIndex(i, len); 12 e = tab[i]; 13 } 14 return null; 15 }
expungeStaleEntry
函數的源碼:
1 private int expungeStaleEntry(int staleSlot) { 2 Entry[] tab = table; 3 int len = tab.length; 4 // expunge entry at staleSlot 5 tab[staleSlot].value = null; 6 tab[staleSlot] = null; 7 size--; 8 // Rehash until we encounter null 9 Entry e; 10 int i; 11 for (i = nextIndex(staleSlot, len); 12 (e = tab[i]) != null; 13 i = nextIndex(i, len)) { 14 ThreadLocal<?> k = e.get(); 15 if (k == null) { 16 e.value = null; 17 tab[i] = null; 18 size--; 19 } else { 20 int h = k.threadLocalHashCode & (len - 1); 21 if (h != i) { 22 tab[i] = null; 23 // Unlike Knuth 6.4 Algorithm R, we must scan until 24 // null because multiple entries could have been stale. 25 while (tab[h] != null) 26 h = nextIndex(h, len); 27 tab[h] = e; 28 } 29 } 30 } 31 return i; 32 }
根據這些源碼,咱們能夠分析出 getEntry 的運行流程:
1. 首先從 ThreadLocal 的直接索引位置(經過 ThreadLocal.threadLocalHashCode & (len-1) 運算獲得)獲取 Entry e,若是 e 不爲 null 而且 key 相同則返回 e;若是 e 爲 null 或者 key 不一致則向下一個位置查詢,若是下一個位置的 key 和當前須要查詢的 key 相等,則返回對應的 Entry,不然,若是 key 值爲 null,則擦除該位置的 Entry,不然繼續向下一個位置查詢。
2. 在這個過程當中遇到的 key 爲 null 的 Entry 都會被擦除,那麼 Entry 內的 value 也就沒有強引用鏈,天然會被回收。仔細研究代碼能夠發現,set 操做也有相似的思想,將 key 爲 null 的這些 Entry 都刪除,防止內存泄露。可是光這樣仍是不夠的,上面的設計思路依賴一個前提條件:要調用 ThreadLocalMap 的 getEntry 函數或者 set 函數。這固然是不可能任何狀況都成立的,因此不少狀況下須要使用者手動調用 ThreadLocal 的 remove 函數,手動刪除再也不須要的 ThreadLocal ,防止內存泄露。因此 JDK 建議將 ThreadLocal 變量定義成 private static 的,這樣的話 ThreadLocal 的生命週期就更長,因爲一直存在 ThreadLocal 的強引用,因此 ThreadLocal 也就不會被回收,也就能保證任什麼時候候都能根據 ThreadLocal 的弱引用訪問到 Entry的value 值,而後 remove 它,防止內存泄露。
3.3 ThreadLocal 總結(摘自洋哥的 PPT)
ThreadLocal 負責管理ThreadLocalMap,包括插入,刪除 等等,key就是ThreadLocal對象本身;同時,很重要的一點,就ThreadLocal把map存儲在當前線程對象裏面。 爲何在ThreadLocalMap 中弱引用ThreadLocal對象呢?固然是從線程內存管理的角度出發的。 使用弱引用,使得ThreadLocalMap知道ThreadLocal對象是否已經失效,一旦該對象失效,也就是成爲垃圾,那麼它所操控的Map裏的數據也就沒有用處了,由於外界再也沒法訪問,進而決定擦除Map中相關的值對象,即:Entry對象的引用,來保證Map老是儘量的小。 總之,線程經過ThreadLocal 來給本身的map 添加值,刪除值。同時一旦ThreadLocal自己成爲垃圾,Map也能自動清除該ThreadLocal所操控的數據。 這樣,經過設計一個代理類ThreadLocal,保證了咱們只須要往Map裏面塞數據,無需擔憂清除,這是普通map作不到的。
四. 同步加鎖的四大方式
4.1 volatile (前文已提到,此處再也不贅述)
4.2 JVM 內置鎖(監視器鎖) synchronized
很是久遠以及久經使用的鎖,也被稱爲重量級鎖,基本用法:
待續。。
參考文獻:
1. 線程安全的理解 http://www.cnblogs.com/kunpengit/archive/2011/11/18/2254280.html
2. 從JVM角度看線程安全與垃圾收集 http://blog.csdn.net/sadfishsc/article/details/10325879
3.《深刻理解java虛擬機》以內存模型與安全 http://blog.csdn.net/sjtu_chenchen/article/details/49132183
4. [Java併發包學習七]解密ThreadLocal http://qifuguang.me/2015/09/02/[Java%E5%B9%B6%E5%8F%91%E5%8C%85%E5%AD%A6%E4%B9%A0%E4%B8%83]%E8%A7%A3%E5%AF%86ThreadLocal/