五分鐘帶你玩轉多線程(五)volatile、ThreadLocal的使用場景和原理
并發編程中的三個概念
原子性
一個或多個操作。要么全部執行完成并且執行過程不會被打斷,要么不執行。最常見的例子:i++/i--操作。不是原子性操作,如果不做好同步性就容易造成線程安全問題。
可見性
多個線程訪問同一個變量,一個線程改變了這個變量的值,其他線程可以立即看到修改的值。可見性的問題,有兩種方式保證。一是volatile關鍵字,二是通過synchronized和lock。詳細在后面。
有序性
程序執行的順序按照代碼的先后順序執行。
要了解有序性需要了解一下指令重排序。處理器為了提供運行效率,會將代碼優化,不保證各個語句的執行順序,但會保證執行結果跟代碼順序執行一致,其不影響單線程的執行結果,但會影響線程并發執行的正確性。指令重排序會考慮指令之間的數據依賴性,如果一個指令B必須用到指令A的結果,那么處理器會保證A在B之前執行。
要保證并發程序正確的執行,必須要保證原子性、可見性及有序性。只要有一個沒有被保證,就可能導致程序運行不正確。
Java內存模型
Java內存模型規定:所有變量存在主內存,每個線程有自己的工作內存。
線程對變量的操作必須在工作內存進行,而不能直接對主內存進行操作。并且每個線程不能訪問其他線程的工作內存。
JAVA語言本身提供的對原子性、可見性及有序性的保證:
原子性:java中,對于引用變量,和大部分的原始數據類型的讀寫(除long 和 double外)操作都是原子的。這些操作不可被中斷,要么執行,要么不執行。對于所有被聲明為volatile的變量的讀寫,都是原子的(除long和double外)
可見性:java提供了volatile關鍵字來保證可見性。當一個共享變量被volatile修飾時,它會保證修改的值立即被更新到主內存。其他線程讀取時會從內存中讀到新值。普通的共享變量不能保證可見性,其被寫入內存的時機不確定。當其他線程去讀,可能讀到的是舊的值。另外通過synchronized和lock也可以保證可見性。它們能保證同一時刻只有一個線程獲取鎖然后執行同步代碼。并在釋放鎖之前對變量的修改刷新到住內存中。以此來保證可見性
有序性:java內存模型中,允許編譯器和處理器 對指令進行重排序。其會影響多線程并發執行的正確性。在java里可以通過volatile關鍵字,還有synchronized和lock來保證有序性。
synchronized和lock保證每個時刻只有一個線程執行同步代碼,使得線程串行化執行同步代碼,保證了有序性。volatile如何保證的講解在后面。
volatile
一個共享變量(類的成員變量、類的靜態成員變量)被volatile修飾后,就具備了兩層語義:保證了不同線程對這個變量進行操作時的可見性和禁止了指令重排序。
關于volatile保證可見性的原因我們上面已經講過了,現在來看看volatile通過禁止指令重排序來保證一定的有序性的意思:
1、當程序執行到volatile變量的讀操作或寫操作時,在其之前的操作的更改肯定全部已經進行,且結果對后面的操作可見。其后面的操作肯定還沒有進行
2、在進行指令優化時,不能將在volatile變量訪問的語句放在其后面執行,也不能把volatile變量后面的語句放在其前面執行。
volatile關鍵字的原理和實現機制:
在加入volatile關鍵字時,會多出一個lock前綴指令。lock前綴指令相當于一個內存屏障,其提供三個功能。
1、它會強制將對緩存的修改操作立即寫入主內存。
2、如果是寫操作,它會導致其他CPU中對應的緩存行無效
3、它確保指定重排序時不會把其后面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的后面。即在執行到內存屏障這句指令時,在它前面的操作已經全部完成。
volatile關鍵字能保證可見性和一定的有序性,那它能保證對變量的操作是原子性嗎?
答案是不能的。如常見的自增操作是不具備原子性的,它包括讀取變量的原始值,進行加一操作,寫入工作內存三個子操作。這就導致進行自增時可能發生子操作被分割執行。
如某個時刻變量i=10。
線程A對i進行自增操作,在讀取i的原始值后被阻塞,
然后線程B對i進行自增,去讀取i的原始值。
由于A沒有對i進行修改,所以B在主內存中讀取到的是原始值并進行加1。然后把11寫入主內存。然后A對i進行操作。由于已經讀取了i的值,此時A的工作內存中i的值還是10,A對i進行自增加一后,把11寫入主內存。兩個線程分別進行了一次自增操作,但是結果卻是11。
要注意的是:volatile無法保證對變量的任何操作都是原子性的。
使用volatile關鍵字時必須具備兩個條件:
1、對變量的寫操作不依賴于當前值。
2、該變量沒有包含在具有其他變量的不變式中。
即保證操作是原子性操作,才能保證使用volatile關鍵字的程序在并發時能夠正確執行。
ThreadLocal
首先ThreadLocal 是一個線程的局部變量(其實就是一個Map),ThreadLocal會為每個使用該變量的線程提供獨立的變量副本,所以每一個線程都可以獨立地改變自己的副本,將對象的可見范圍限制在同一個線程內,而不會影響其它線程所對應的副本。
這樣做其實就是以空間換時間的方式(與synchronized相反),以耗費內存為代價,單大大減少了線程同步(如synchronized)所帶來性能消耗以及減少了線程并發控制的復雜度。
ThreadLoca類中提供了幾個常用方法
public T get() { }---獲取ThreadLocal在當前線程中保存的變量副本
public void set(T value) { }---設置當前線程中變量的副本
public void remove() { }---移除當前線程中變量的副本
protected T initialValue() { }---protected修飾的方法。
ThreadLocal提供的只是一個淺拷貝,如果變量是一個引用類型,那么就要重寫該函數來實現深拷貝。建議在使用? ? ? ?ThreadLocal一開始時就重寫該函數
ThreadLocal的設計初衷就是為了避免多個線程去并發訪問同一個對象,盡管它是線程安全的。因此如果用普遍的方法,通過一個全局的線程安全的map來存儲多個線程的變量副本就違背了ThreadLocal的本意。在每個Thread中存放與它關聯的ThreadLocalMap是完全符合其設計思想的。當想對線程局部變量進行操作時,只要把Thread作為key來獲取Thread中的ThreadLocalMap即可。這種設計相比采用一個全局map的方法會占用很多內存空間,但其不需要額外采取鎖等線程同步方法而節省了時間上的消耗。
Synchronized卻正好相反,它用于在多個線程間通信時能夠獲得數據共享。即Synchronized用于線程間的數據共享,而ThreadLocal則用于線程間的數據隔離。所以ThreadLocal并不能代替synchronized,Synchronized的功能范圍更廣(同步機制)。
ThreadLocal中的內存泄露問題
如果ThreadLocal被設置為null后,并且沒有任何強引用指向它,根據垃圾回收的可達性分析算法,ThreadLocal將被回收。這樣的話,ThreadLocalMap中就會含有key為null的Entry,而且ThreadLocalMap是在Thread中的,只要線程遲遲不結束,這些無法訪問到的value就會形成內存泄露。為了解決這個問題,ThreadLocalMap中的getEntry()、set()和remove()函數都會清理key為null的Entry,以下面的getEntry()函數為例。
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];
}
}
要注意的是ThreadLocalMap的key是一個弱引用。在這里我們分析一下強引用key和弱引用key的差別
強引用key:ThreadLocal被設置為null,由于ThreadLocalMap持有ThreadLocal的強引用,如果不手動刪除,那么ThreadLocal將不會回收,產生內存泄漏。
弱引用key:ThreadLocal被設置為null,由于ThreadLocalMap持有ThreadLocal的弱引用,即便不手動刪除,ThreadLocal仍會被回收,ThreadLocalMap在之后調用set()、getEntry()和remove()函數時會清除所有key為null的Entry。
ThreadLocalMap僅僅含有這些被動措施來補救內存泄露問題,如果在之后沒有調用ThreadLocalMap的set()、getEntry()和remove()函數的話,那么仍然會存在內存泄漏問題。在使用線程池的情況下,如果不及時進行清理,內存泄漏問題事小,甚至還會產生程序邏輯上的問題。所以,為了安全地使用ThreadLocal,必須要像每次使用完鎖就解鎖一樣,在每次使用完ThreadLocal后都要調用remove()來清理無用的Entry。
總結:
1.threadLocal是用于解決多線程共享類的成員變量,原理:在每個線程中都存有一個本地ThreadMap,相當于存了一個對象的副本,key為threadlocal對象本身,value為需要存儲的對象值,這樣各個線程之間對于某個成員變量都有自己的副本,不會沖突。用空間去換時間
2.使用volatile關鍵字的時候,該變量一旦被修改,會立即寫入到主存中,同時會讓其他線程的工作內存中的緩存失效,這樣,其他線程在訪問該變量的時候會重新從主存中讀取可以獲得該變量最新的數據,從而保證的變量的可見性。而volatile首先保證前面的任務都完成,保證后面的任務在現有任務之后。
3.?volatile只能用于原子性的操作。如:i++,i=x;這種都不屬于原子性的操作,i++有三個步驟,先讀取內存中i的值,然后執行i+1操作,然后把結果寫回i,這樣的操作不屬于原子性的。
Java 任務調度 多線程
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。