Java并發問題總結!
-----------------來自小馬哥的故事
Java記憶體模型
所有變量都存儲在主記憶體中。這裡的主記憶體隻是虛拟機記憶體的一部分,可以和實體主記憶體類比。每條線程都有自己的工作記憶體。工作記憶體可以和處理器高速緩存類比。工作記憶體中儲存了主記憶體中變量的拷貝,線程所有的操作隻能在工作記憶體中進行,不同線程不能通路對方的工作記憶體,隻能通過更新到主記憶體中的方式來傳遞線程間的變量值。
主記憶體與工作記憶體間的互動操作都具有原子性,包括
其中read和load之間,store和write之間必須按順序執行,但是不要求連續執行,即中間可以插入其他指令。
并發的三個問題
原子性
指的是不能被線程排程機制中斷的操作,它會在上下文切換之前執行完畢。由于read,load,store,write,use,assign都能夠保證原子性,故對一個基本類型變量的通路和指派可以看作原子操作。對于synchronized塊之間的操作也具有原子性。
x = 1; // 具有原子性
y = x; // 2個指令,use了x的值,再assign到y
x++; // 4個指令,use了x的值,生成常數1,x加1,再assign到x
複制
可見性
指的是當一個線程修改了共享變量值,其他線程能夠立即得知這個修改。
普通變量的修改首先發生在本線程的工作記憶體中,這會導緻各個工作記憶體的不一緻性。當一個線程結束後會将各自的工作記憶體同步回主記憶體,另一個線程讀取這個變量時會從主記憶體中讀取它的新值。
volatile變量也是同樣的過程,隻是它修改後立即同步回主記憶體,并通知其他工作記憶體中的此變量失效。如果其他線程需要使用此變量時,隻能從主記憶體中重新讀取它的新值。這就保證了多線程下的變量可見性。
synchronized同步塊也具有可見性。這是由于對一個變量unlock之前,必須先将它同步回主記憶體中。
final修飾的變量也具有可見性。當一個final變量被初始化後(構造器完成之前沒有将this引用傳遞出去),此變量在其他線程中可見。
有序性
指的是機器會對指令進行重排序來達到運作時的優化。這就導緻了代碼書寫上的先後順序不能在執行時得到保證,但是在單線程内看,程式執行的結果和按照串行執行的結果保持一緻。而使用多線程時執行結果就不能保證了。Java可以有兩種方法保證線程間的有序性
volatile可以防止指令随意的重排序,它的作用相當于一個記憶體屏障,也就是重排序時不能将記憶體屏障之後的指令排在它之前。
synchronized同步塊可以保證有序,是因為同一時刻隻允許一個線程對某個變量進行lock操作。是以多個線程隻能有序的進入同一個同步塊。
volatile關鍵字
volatile是最輕量級的同步機制,但是它隻保證了被修飾變量的可見性和有序性,而不能保證原子性,進而不能解決很多并發同步問題。
具有兩個特性
可見性,一旦某個線程中的volatile變量被修改,即store和write 指令執行後,所有線程都可以得到最新的變量值。注意這裡是指令執行,而不是Java的語句執行,一條非原子性的Java語句總是對應多條指令,是以這條語句所帶來的改變不具有可見性。 有序性,防止指令随意的重排序優化。通過在彙編代碼中加入lock操作,使變量的修改寫回主記憶體,即執行了store和write指令。這意味着寫之前的所有操作都必須執行完成,進而達到了記憶體屏障的作用。同時它還會使其他工作記憶體中的該變量無效,其他線程需要重新從主記憶體中讀取此變量。
應用場合
由于volatile能夠保證可見性和有序性,唯一不能保證原子性,是以如果一個操作本身具有原子性,那麼使用volatile修飾後就可以保證并發的同步性。應用場合有兩個
變量的指派不依賴于它的目前值或别的變量的目前值,即直接使用assign指令而沒有使用use指令,具有原子性 保證隻有一個線程對變量進行修改,而别的線程隻進行讀取,讀取值不一定是最新的,但修改不會出錯
synchronized關鍵字
可以解決所有并發問題,但是容易造成濫用而導緻并發性能不高。可以作為方法的修飾符,表示要進入方法時需要擷取本對象的鎖,也可以使用synchronized(object){...}對代碼塊加鎖,表示要進入代碼塊時需要擷取object的鎖。 一旦有一個線程擷取了鎖而進入了同步塊中,所有的其他線程都會進入等鎖池而阻塞。這有時會導緻不必要的阻塞時間。同時,Java線程是映射到作業系統原生線程上的,阻塞和喚醒都需要從使用者态轉換到核心态,要花費較多處理器時間。而volatile變量的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為它需要在本地代碼中插入許多記憶體屏障指令來保證處理器不發生亂序執行。
重入鎖ReentrantLock
ReentrantLock可以顯式的建立,鎖定和釋放,與synchronized的内建鎖複雜但是更靈活,尤其是進入同步塊後如果抛出異常,可以進行清理工作。另外還可以實作一些進階功能,包括等待可中斷(線程可以放棄等待而做其他事情),公平鎖(按照申請鎖的時間擷取鎖),多條件綁定(通過調用newCondition()方法添加多個Condition)。
Lock lock = new ReentrantLock();
lock.lock();
try {
...
} finally {
lock.unlock();
}
ReentrantLock lock = new ReentrantLock();
boolean captured = lock.tryLock();
try {
...
} finally {
if (captured) lock.unlock();
}
複制
Atomic類
加鎖屬于阻塞同步,即無論共享資料是否真的出現競争都會加鎖,這是悲觀的并發政策。而利用硬體指令還可以實作非阻塞同步,這是一種基于沖突檢測的樂觀并發政策。它可以先操作,如果沒有其他線程争用共享資料則操作成功,而如果産生了沖突,則不斷重試直到操作成功。這涉及一條處理器指令compare-and-swap(CAS),它具有原子性,表示變量符合舊值時才會用新值更新變量,否則不更新,最後都傳回舊值。 例如Java8中AtomicInteger類的incrementAndGet()方法,會用到sun/misc/Unsafe類中的getAndAddInt()方法,其中的compareAndSwapInt()就是CAS操作。下述代碼的意義是對于執行個體var1在偏移var2處的舊值為var5,如果即将要指派的時候發現擷取的值不符合var5(CAS指令操作),說明此時有其他線程已經修改了這個變量,于是繼續擷取新的var5,直到指派前擷取的值符合var5,則用var5+var4更新var5。
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
複制
不使用同步的情況
可重入代碼不需要同步,因為它不使用共享變量,且所有的狀态量都由參數傳入,是以在任何時刻中斷它再傳回後不會出現錯誤,這就保證了線程安全。
如果能保證共享變量隻在一個線程中可見,同樣也不需要同步,但是這樣的應用比較少見。
利用ThreadLocal可以根除了對變量的共享,它可以為使用相同變量的每個線程建立不同的存儲。每個線程使用的都是獨立的變量,當然不會有同步的問題。但是一個線程是不能通路到另一個線程的ThreadLocal變量,盡管隻建立過一個ThreadLocal變量的執行個體。一個線程隻能使用get和set改變本線程内該變量的值,不同線程中該變量值互不影響。具體實作中,每個Thread線程對象都由一個ThreadLocal.ThreadLocalMap,其鍵為ThreadLocal.ThreadLocalHashCode,值就是本地線程變量,各個線程的Map是獨立的。
鎖優化
自旋鎖
阻塞線程比較耗時,是因為挂起和恢複線程都需要轉換核心态,而鎖定共享資料往往隻持續很短的時間。是以有時隻需要讓線程執行一個忙循環(自旋)等待,但是不放棄處理器執行,就可以擷取鎖。前提是等待時間不能太長,自适應自旋鎖可以調整自旋的時間。
鎖消除
如果代碼上要求同步,但是經過逃逸分析發現不可能存在共享資料競争,因而可以将鎖進行消除。有些代碼的同步可能不是人為加入的,而是源碼自帶的。
鎖粗化
如果一系列加鎖和解鎖是對同一個對象連續進行的,就可以将同步範圍擴大到整個序列的外部,這樣就可以進行一次加鎖和解鎖了。
輕量級鎖
認為大部分鎖在同步時間内是不存在競争的。通過利用對象頭資訊Mark Word和CAS操作判斷對象是否加鎖,如果沒有,則直接進入同步塊執行,這個判斷過程就是輕量級鎖;否則說明的這個加鎖的對象有競争,輕量級鎖就要膨脹為重量級鎖,也就是使用互斥量的一般鎖。 解鎖也要使用CAS操作,将原來的Mark Word的記錄替換回來,如果替換成功說明在此期間沒有線程嘗試過擷取該鎖,進而解鎖完成;如果失敗,則一定有線程嘗試過擷取該鎖,是以在解鎖完成後還要喚醒等鎖的線程。
偏向鎖
在輕量級鎖的基礎上,如果沒有競争,線程将CAS操作也取消,且這個偏向鎖會偏向第一個獲得它的線程。如果執行過程中該鎖沒有被其他線程擷取,則持有偏向鎖的線程永遠不用同步。但是一旦有線程嘗試擷取該鎖時,偏向模式被撤銷,将鎖對象恢複為未鎖定或者輕量級鎖。
本文由 小馬哥 創作,采用 知識共享署名4.0 國際許可協定進行許可
本站文章除注明轉載/出處外,均為本站原創或翻譯,轉載前請務必署名