java併發編程分析

在Java併發編程中,常常遇到多個線程訪問同一個 共享資源 ,這時候做爲開發者必須考慮如何維護數據一致性,這就是Java鎖機制(線程同步)的來源java

Java提供了多種多線程鎖機制的實現方式,常見的有:編程

  1. synchronized緩存

  2. ReentrantLock安全

  3. Semaphorebash

  4. AtomicInteger等多線程

每種機制都有優缺點與各自的適用場景,必須熟練掌握他們的特色才能在Java多線程應用開發時駕輕就熟。併發

4種Java線程鎖

1.synchronizedapp

在Java中synchronized關鍵字被經常使用於維護數據一致性。函數

synchronized機制是給共享資源上鎖,只有拿到鎖的線程才能夠訪問共享資源,這樣就能夠強制使得對共享資源的訪問都是順序的。高併發

Java開發人員都認識synchronized,使用它來實現多線程的同步操做是很是簡單的,只要在須要同步的對方的方法、類或代碼塊中加入該關鍵字,它可以保證在同一個時刻最多隻有一個線程執行同一個對象的同步代碼,可保證修飾的代碼在執行過程當中不會被其餘線程干擾。

使用synchronized修飾的代碼具備原子性和可見性,在須要進程同步的程序中使用的頻率很是高,能夠知足通常的進程同步要求。

  1. 原子性:原子,即一個不可再被分割的顆粒。在Java中原子性指的是一個或多個操做要麼所有執行成功要麼所有執行失敗。


  2. 有序性:程序執行的順序按照代碼的前後順序執行。(處理器可能會對指令進行重排序)


  3. 可見性:當多個線程訪問同一個變量時,若是其中一個線程對其做了修改,其餘線程能當即獲取到最新的值。


synchronized實現的機理依賴於軟件層面上的JVM,所以其性能會隨着Java版本的不斷升級而提升。到了Java1.6,synchronized進行了不少的優化,有適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向鎖等,效率有了本質上的提升。在以後推出的Java1.7與1.8中,均對該關鍵字的實現機理作了優化。須要說明的是,當線程經過synchronized等待鎖時是不能被Thread.interrupt()中斷的,所以程序設計時必須檢查確保合理,不然可能會形成線程死鎖的尷尬境地。最後,儘管Java實現的鎖機制有不少種,而且有些鎖機制性能也比synchronized高,但仍是強烈推薦在多線程應用程序中使用該關鍵字,由於實現方便,後續工做由JVM來完成,可靠性高。只有在肯定鎖機制是當前多線程程序的性能瓶頸時,才考慮使用其餘機制,如ReentrantLock等。

2.ReentrantLock

可重入鎖,顧名思義,這個鎖能夠被線程屢次重複進入進行獲取操做。ReentantLock繼承接口Lock並實現了接口中定義的方法,除了能完成synchronized所能完成的全部工做外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等避免多線程死鎖的方法。Lock實現的機理依賴於特殊的CPU指定,能夠認爲不受JVM的約束,並能夠經過其餘語言平臺來完成底層的實現。在併發量較小的多線程應用程序中,ReentrantLock與synchronized性能相差無幾,但在高併發量的條件下,synchronized性能會迅速降低幾十倍,而ReentrantLock的性能卻能依然維持一個水準。所以建議在高併發量狀況下使用ReentrantLock。

ReentrantLock引入兩個概念:公平鎖與非公平鎖

公平鎖指的是鎖的分配機制是公平的,一般先對鎖提出獲取請求的線程會先被分配到鎖。反之,JVM按隨機、就近原則分配鎖的機制則稱爲不公平鎖。

ReentrantLock在構造函數中提供了是否公平鎖的初始化方式,默認爲非公平鎖。這是由於,非公平鎖實際執行的效率要遠遠超出公平鎖,除非程序有特殊須要,不然最經常使用非公平鎖的分配機制。ReentrantLock經過方法lock()與unlock()來進行加鎖與解鎖操做,與synchronized會被JVM自動解鎖機制不一樣,ReentrantLock加鎖後須要手動進行解鎖。爲了不程序出現異常而沒法正常解鎖的狀況,使用ReentrantLock必須在finally控制塊中進行解鎖操做。一般使用方式以下所示:

Lock lock = new ReentrantLock();

try {

lock.lock();

//...進行任務操做5 }

finally {

lock.unlock();

}


3.Semaphore

      上述兩種鎖機制類型都是「互斥鎖」,學過操做系統的都知道,互斥是進程同步關係的一種特殊狀況,至關於只存在一個臨界資源,所以同時最多隻能給一個線程提供服務。可是,在實際複雜的多線程應用程序中,可能存在多個臨界資源,這時候咱們能夠藉助Semaphore信號量來完成多個臨界資源的訪問。Semaphore基本能完成ReentrantLock的全部工做,使用方法也與之相似,經過acquire()與release()方法來得到和釋放臨界資源。經實測,Semaphone.acquire()方法默認爲可響應中斷鎖,與ReentrantLock.lockInterruptibly()做用效果一致,也就是說在等待臨界資源的過程當中能夠被Thread.interrupt()方法中斷。此外,Semaphore也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名tryAcquire與tryLock不一樣,其使用方法與ReentrantLock幾乎一致。Semaphore也提供了公平與非公平鎖的機制,也可在構造函數中進行設定。Semaphore的鎖釋放操做也由手動進行,所以與ReentrantLock同樣,爲避免線程因拋出異常而沒法正常釋放鎖的狀況發生,釋放鎖的操做也必須在finally代碼塊中完成

4.AtomicInteger

         首先說明,此處AtomicInteger是一系列相同類的表明之一,常見的還有AtomicLong、AtomicLong等,他們的實現原理相同,區別在與運算對象類型的不一樣。咱們知道,在多線程程序中,諸如++i 或 i++等運算不具備原子性,是不安全的線程操做之一。一般咱們會使用synchronized將該操做變成一個原子操做,但JVM爲此類操做特地提供了一些同步類,使得使用更方便,且使程序運行效率變得更高。經過相關資料顯示,一般AtomicInteger的性能是ReentantLock的好幾倍。

Java線程鎖總結

1.synchronized

在資源競爭不是很激烈的狀況下,偶爾會有同步的情形下,synchronized是很合適的。緣由在於,編譯程序一般會盡量的進行優化synchronize,另外可讀性很是好。

2.ReentrantLock

在資源競爭不激烈的情形下,性能稍微比synchronized差點點。可是當同步很是激烈的時候,synchronized的性能一會兒能降低好幾十倍,而ReentrantLock確還能維持常態。

高併發量狀況下使用ReentrantLock。

3.Atomic

不激烈狀況下,性能比synchronized略遜,而激烈的時候,也能維持常態。激烈的時候,Atomic的性能會優於ReentrantLock一倍左右。

可是其有一個缺點,就是隻能同步一個值,一段代碼中只能出現一個Atomic的變量,多於一個同步無效,由於他不能在多個Atomic之間同步。

因此,咱們寫同步的時候,優先考慮synchronized,若是有特殊須要,再進一步優化。ReentrantLock和Atomic若是用的很差,不只不能提升性能,還可能帶來災難。

Thread 類的經常使用函數及功能:

1.sleep():

sleep是Thread的靜態方法,使當前的正在執行線程處於停滯狀態,sleep()使線程進入堵塞狀態,同時不會釋放所資源,sleep可以使優先級低的線程獲得執行的機會,固然也可讓同優先級和高優先級的線程有執行的機會。

2.wait():

wait使當前線程處於等待狀態,會釋放當前的鎖資源,使用wait()的時候要注意: 

wait()、notify()、notifyAll()都必須在synchronized中執行,不然會拋出異常 

wait()、notify()、notifyAll()都是屬於超類Object的方法 

一個對象只有一個鎖(對象鎖和類鎖仍是有區別的) 

wait()和sleep()區別: 

a).wait()能夠不指定時間,sleep()必須指定時間

 b).wait()會釋放當前鎖資源,sleep()不可以釋放鎖資源 

c).wait()是來自Object類中,sleep()是來自Thread類

3.join():

join是讓當前線程等待調用join 的線程運行完run方法,能夠指定時間,若指定了時間,則等待指定時間,即使調用join的線程沒運行完run方法,當前線程也會繼續往下運行;若未指定時間,則當前線程一直等待,直到join的線程運行完run方法。

4.yield(): 

yield也是Thread的靜態方法,yield的本質就是將當前線程從新放入搶佔CPU時間的」隊列「中,當前線程願意讓出CPU的使用權,可讓其餘線程繼續執行,可是線程調度器可能會中止當前線程繼續執行,也可能會讓該線程繼續執行。 而且與線程優先級並沒有關係,優先級高的不必定先執行。線程的優先級將該線程的重要性傳遞給線程調度器,調度器將傾向於讓優先權最高的線程先執行.而後這並不意味值優先權較低的線程將得不到執行。優先級較低的線程僅僅是執行的頻率較低 。

5.stop(): 

能夠中止正在執行得線程,這樣的方法不安全,不建議使用。他會解除線程獲取的全部鎖定,若是線程處於一種不連貫的狀態,其餘線程有可能在那種狀態下檢查和修改他們,很難找到問題的所在。 

6.suspend(): 

容易發生死鎖,調用suspend()的時候,目標線程會中止,可是卻仍然有以前獲取的鎖定。此時,其餘線程都不能有訪問線程鎖定的資源,除非被"掛起"的線程恢復運行。須要在Thread類中有一個標誌 若標誌指出線程應該掛起,便用wait()命其進入等待狀態。若標誌指出線程應當恢復,則用一個notify()從新啓動線程。  

7.interrupt():

interrupt()不會中斷一個正在運行的線程。這一方法實際上完成的是:在線程受到阻塞時拋出一箇中斷信號,線程就得以退出阻塞的狀態。 若是線程被Object.wait,Thread.join,Thread.sleep三種方法之一阻塞,那麼,它將接收到一箇中斷異(InterruptedException),從而提前地終結被阻塞狀態。 若是線程沒有被阻塞,這時調用interrupt()將不起做用。

8.setPriority():

線程分配時間片的多少就決定線程使用處理的多少,恰好對應線程優先級別這個概念。能夠經過int priority(),裏邊能夠填1-10,默認爲5,10最高。

Java中的volatile

      volatile是Java提供的一種輕量級的同步機制,在併發編程中,它也扮演着比較重要的角色。同synchronized相比(synchronized一般稱爲重量級鎖),volatile更輕量級,相比使用synchronized所帶來的龐大開銷,使用 synchronized 雖然能夠解決多線程安全問題,但弊端也很明顯:加鎖後多個線程須要判斷鎖,較爲消耗資源。

volatile 關鍵字做用

  • 內存可見性
  • 禁止指令重排

所謂可見性,是指當一條線程修改了共享變量的值,新值對於其餘線程來講是能夠當即得知的。java虛擬機有本身的內存模型(Java Memory Model,JMM),JMM能夠屏蔽掉各類硬件和操做系統的內存訪問差別,以實現讓java程序在各類平臺下都能達到一致的內存訪問效果。JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見,JMM定義了線程和主內存之間的抽象關係:共享變量存儲在主內存(Main Memory)中,每一個線程都有一個私有的本地內存(Local Memory),本地內存保存了被該線程使用到的主內存的副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不能直接讀寫主內存中的變量。這三者之間的交互關係以下:



 須要注意的是,JMM是個抽象的內存模型,因此所謂的本地內存,主內存都是抽象概念,並不必定就真實的對應cpu緩存和物理內存。Java 中存在一種原則——先行發生原則(happens-before)。其表示兩個事件結果之間的關係:若是一個事件發生在另外一個事件之間,其結果必須體現。volatile 的內存可見性就體現了該原則:對於一個 volatile 變量的寫操做先行發生於後面對這個變量的讀操做。將一個共享變量聲明爲volatile後,會有如下效應:

     1.當寫一個volatile變量時,JMM會把該線程對應的本地內存中的變量強制刷新到主內存 中 去;

     2.這個寫會操做會致使其餘線程中的緩存無效。

可是須要注意的是,咱們一直在拿volatile和synchronized作對比,僅僅是由於這兩個關鍵字在某些內存語義上有共通之處,volatile並不能徹底替代synchronized,它依然是個輕量級鎖,在不少場景下,volatile並不能勝任。

例如:當多個線程都對某一 volatile 變量(int a=0)進行 count++ 操做時,因爲 count++ 操做並非原子性操做,當線程 A 執行 count++ 後,A 工做內存其副本的值爲 1,但線程執行時間到了,主內存的值仍爲 0 ;線程 B又來執行 count++後並將值更新到主內存,主內存此時的值爲 1;而後線程 A 繼續執行將值更新到主內存爲 1,它並不知道線程 B 對變量進行了修改,也就是沒有判斷主內存的值是否發生改變,故最終結果爲 1,但理論上 count++ 兩次,值應該爲 2。 

因此要使用 volatile 的內存可見性特性的話得知足兩個條件:

 能確保只有單一的線程對共享變量的只進行修改。 

變量不須要和其餘狀態變量共同參與不變的約束條件。

所謂重排序,是指編譯器和處理器爲了優化程序性能而對指令序列進行排序的一種手段。可是重排序也須要遵照必定規則:

  1.重排序操做不會對存在數據依賴關係的操做進行重排序。

    好比:a=1;b=a; 這個指令序列,因爲第二個操做依賴於第一個操做,因此在編譯時和處理器運行時這兩個操做不會被重排序。

  2.重排序是爲了優化性能,可是無論怎麼重排序,單線程下程序的執行結果不能被改變

    好比:a=1;b=2;c=a+b這三個操做,第一步(a=1)和第二步(b=2)因爲不存在數據依賴關係,因此可能會發生重排序,可是c=a+b這個操做是不會被重排序的,由於須要保證最終的結果必定是c=a+b=3。

  重排序在單線程模式下是必定會保證最終結果的正確性,可是在多線程環境下,問題就出來了。指令重排雖然說能夠優化程序的執行效率,但在多線程問題上會影響結果。那麼有什麼解決辦法呢?答案是內存屏障。

內存屏障是一種屏障指令,使 CPU 或編譯器對屏障指令以前和以後發出的內存操做執行一個排序的約束。  

四種類型:LoadLoad 屏障、StoreStore 屏障、LoadStore 屏障、StoreLoad 屏障。(Load 表明讀取指令、Store 表明寫入操做) 

 在 volatile 變量上的體現:

(JVM 執行操做) 在每一個 volatile 寫入操做前插入 StoreStore 屏障; 

在寫操做後插入 StoreLoad 屏障; 

在讀操做前插入 LoadLoad 屏障;

 在讀操做後插入 LoadStore 屏障;

簡單總結下,volatile是一種輕量級的同步機制,它主要有兩個特性:一是保證共享變量對全部線程的可見性;二是禁止指令重排序優化。同時須要注意的是,volatile對於單個的共享變量的讀/寫具備原子性,可是像num++這種複合操做,volatile沒法保證其原子性。

可重入鎖與不可重入鎖

所謂重入鎖,指的是以線程爲單位,當一個線程獲取對象鎖以後,這個線程能夠再次獲取本對象上的鎖,而其餘的線程是不能夠的。

synchronized 和 ReentrantLock 都是可重入鎖。

可重入鎖的意義在於防止死鎖。

實現原理是經過爲每一個鎖關聯一個請求計數器和一個佔有它的線程。當計數爲0時,認爲鎖是未被佔有的;線程請求一個未被佔有的鎖時,JVM將記錄鎖的佔有者,而且將請求計數器置爲1 。

若是同一個線程再次請求這個鎖,計數將遞增;

每次佔用線程退出同步塊,計數器值將遞減。直到計數器爲0,鎖被釋放。

關於父類和子類的鎖的重入:子類覆寫了父類的synchonized方法,而後調用父類中的方法,此時若是沒有重入的鎖,那麼這段代碼將產生死鎖(很好理解吧)。

例子:

好比說A類中有個方法public synchronized methodA1(){

methodA2();

}

並且public synchronized methodA2(){

//具體操做

}

也是A類中的同步方法,噹噹前線程調用A類的對象methodA1同步方法,若是其餘線程沒有獲取A類的對象鎖,那麼當前線程就得到當前A類對象的鎖,而後執行methodA1同步方法,方法體中調用methodA2同步方法,當前線程可以再次獲取A類對象的鎖,而其餘線程是不能夠的,這就是可重入鎖。

不可重入鎖:

public class Lock
{    
    private boolean isLocked = false;    
    public synchronized void lock() throws InterruptedException
    {        
        while(isLocked){                
            wait();
        }
        isLocked = true;
    }
    public synchronized void unlock()
    {
        isLocked = false;
        notify();
    }
}複製代碼

使用該鎖:

public class Count{
    Lock lock = new Lock();
    public void print(){
        lock.lock();
        doAdd();
        lock.unlock();
    }
    public void doAdd(){
        lock.lock();        //do something
        lock.unlock();
    }
}複製代碼

當前線程執行print()方法首先獲取lock,接下來執行doAdd()方法就沒法執行doAdd()中的邏輯,必須先釋放鎖。這個例子很好的說明了不可重入鎖。

相關文章
相關標籤/搜索