JAVA鎖機制-可重入鎖,可中斷鎖,公平鎖,讀寫鎖,自旋鎖,

 

若是須要查看具體的synchronized和lock的實現原理,請參考:解決多線程安全問題-無非兩個方法synchronized和lock 具體原理(百度)html

   

    在併發編程中,常常遇到多個線程訪問同一個 共享資源 ,這時候做爲開發者必須考慮如何維護數據一致性,在java中synchronized關鍵字被經常使用於維護數據一致性。synchronized機制是給共享資源上鎖,只有拿到鎖的線程才能夠訪問共享資源,這樣就能夠強制使得對共享資源的訪問都是順序的,由於對於共享資源屬性訪問是必要也是必須的,下文會有具體示例演示。
    一.java中的鎖
    通常在java中所說的鎖就是指的內置鎖,每一個java對象均可以做爲一個實現同步的鎖,雖說在java中一切皆對象, 可是鎖必須是引用類型的,基本數據類型則不能夠 。每個引用類型的對象均可以隱式的扮演一個用於同步的鎖的角色,執行線程進入synchronized塊以前會自動得到鎖,不管是經過正常語句退出仍是執行過程當中拋出了異常,線程都會在放棄對synchronized塊的控制時自動釋放鎖。 得到鎖的惟一途徑就是進入這個內部鎖保護的同步塊或方法 。
    正如引言中所說,對共享資源的訪問必須是順序的,也就是說當多個線程對共享資源訪問的時候,只能有一個線程能夠得到該共享資源的鎖,當線程A嘗試獲取線程B的鎖時,線程A必須等待或者阻塞,直到線程B釋放該鎖爲止,不然線程A將一直等待下去,所以java內置鎖也稱做互斥鎖,也便是說鎖其實是一種互斥機制。
    根據使用方式的不一樣通常咱們會將鎖分爲對象鎖和類鎖,兩個鎖是有很大差異的,對象鎖是做用在實例方法或者一個對象實例上面的,而類鎖是做用在靜態方法或者Class對象上面的。一個類能夠有多個實例對象,所以一個類的對象鎖可能會有多個,可是每一個類只有一個Class對象,因此類鎖只有一個。 類鎖只是一個概念上的東西,並非真實存在的,它只是用來幫助咱們理解鎖定的是實例方法仍是靜態方法區別的 。
    在java中實現鎖機制不只僅限於使用synchronized關鍵字,還有JDK1.5以後提供的Lock,Lock不在本文討論範圍以內。一個synchronized塊包含兩個部分:鎖對象的引用,以及這個鎖保護的代碼塊。若是做用在實例方法上面,鎖就是該方法所在的當前對象,靜態synchronized方法會從Class對象上得到鎖。java

 

鎖的相關概念介紹

1.可重入鎖算法

若是鎖具有可重入性,則稱做爲可重入鎖。像synchronized和ReentrantLock都是可重入鎖,可重入性在我看來實際上代表了鎖的分配機制:基於線程的分配,而不是基於方法調用的分配。舉個簡單的例子,當一個線程執行到某個synchronized方法時,好比說method1,而在method1中會調用另一個synchronized方法method2,此時線程沒必要從新去申請鎖,而是能夠直接執行方法method2。編程

看下面這段代碼就明白了: 設計模式

class MyClass {
    public synchronized void method1() {
        method2();
    }
 
    public synchronized void method2() {
 
    }
} 

上述代碼中的兩個方法method1和method2都用synchronized修飾了,假如某一時刻,線程A執行到了method1,此時線程A獲取了這個對象的鎖,而因爲method2也是synchronized方法,假如synchronized不具有可重入性,此時線程A須要從新申請鎖。可是這就會形成一個問題,由於線程A已經持有了該對象的鎖,而又在申請獲取該對象的鎖,這樣就會線程A一直等待永遠不會獲取到的鎖。數組

而因爲synchronized和Lock都具有可重入性,因此不會發生上述現象。安全

2.可中斷鎖多線程

可中斷鎖:顧名思義,就是能夠相應中斷的鎖。併發

在Java中,synchronized就不是可中斷鎖,而Lock是可中斷鎖。ide

若是某一線程A正在執行鎖中的代碼,另外一線程B正在等待獲取該鎖,可能因爲等待時間過長,線程B不想等待了,想先處理其餘事情,咱們可讓它中斷本身或者在別的線程中中斷它,這種就是可中斷鎖。

在前面演示lockInterruptibly()的用法時已經體現了Lock的可中斷性。

3.公平鎖

公平鎖即儘可能以請求鎖的順序來獲取鎖。好比同是有多個線程在等待一個鎖,當這個鎖被釋放時,等待時間最久的線程(最早請求的線程)會得到該所,這種就是公平鎖。

非公平鎖即沒法保證鎖的獲取是按照請求鎖的順序進行的。這樣就可能致使某個或者一些線程永遠獲取不到鎖。

在Java中,synchronized就是非公平鎖,它沒法保證等待的線程獲取鎖的順序。

而對於ReentrantLock和ReentrantReadWriteLock,它默認狀況下是非公平鎖,可是能夠設置爲公平鎖。

看一下這2個類的源代碼就清楚了:

在ReentrantLock中定義了2個靜態內部類,一個是NotFairSync,一個是FairSync,分別用來實現非公平鎖和公平鎖。

咱們能夠在建立ReentrantLock對象時,經過如下方式來設置鎖的公平性:  

ReentrantLock lock = new ReentrantLock(true);

若是參數爲true表示爲公平鎖,爲fasle爲非公平鎖。默認狀況下,若是使用無參構造器,則是非公平鎖。

另外在ReentrantLock類中定義了不少方法,好比: 

isFair()        //判斷鎖是不是公平鎖

isLocked()    //判斷鎖是否被任何線程獲取了

isHeldByCurrentThread()   //判斷鎖是否被當前線程獲取了

hasQueuedThreads()   //判斷是否有線程在等待該鎖

 

在ReentrantReadWriteLock中也有相似的方法,一樣也能夠設置爲公平鎖和非公平鎖。不過要記住,ReentrantReadWriteLock並未實現Lock接口,它實現的是ReadWriteLock接口。

4.讀寫鎖

讀寫鎖將對一個資源(好比文件)的訪問分紅了2個鎖,一個讀鎖和一個寫鎖。

正由於有了讀寫鎖,才使得多個線程之間的讀操做不會發生衝突。

ReadWriteLock就是讀寫鎖,它是一個接口,ReentrantReadWriteLock實現了這個接口。

能夠經過readLock()獲取讀鎖,經過writeLock()獲取寫鎖。

 

五、自旋鎖
首先是一種鎖,與互斥鎖類似,基本做用是用於線程(進程)之間的同步。與普通鎖不一樣的是,一個線程A在得到普通鎖後,若是再有線程B試圖獲取鎖,那麼這個線程B將會掛起(阻塞);試想下,若是兩個線程資源競爭不是特別激烈,而處理器阻塞一個線程引發的線程上下文的切換的代價高於等待資源的代價的時候(鎖的已保持者保持鎖時間比較短),那麼線程B能夠不放棄CPU時間片,而是在「原地」忙等,直到鎖的持有者釋放了該鎖,這就是自旋鎖的原理,可見自旋鎖是一種非阻塞鎖。
2、自旋鎖可能引發的問題:
1.過多佔據CPU時間:若是鎖的當前持有者長時間不釋放該鎖,那麼等待者將長時間的佔據cpu時間片,致使CPU資源的浪費,所以能夠設定一個時間,當鎖持有者超過這個時間不釋放鎖時,等待者會放棄CPU時間片阻塞;
2.死鎖問題:試想一下,有一個線程連續兩次試圖得到自旋鎖(好比在遞歸程序中),第一次這個線程得到了該鎖,當第二次試圖加鎖的時候,檢測到鎖已被佔用(實際上是被本身佔用),那麼這時,線程會一直等待本身釋放該鎖,而不能繼續執行,這樣就引發了死鎖。所以遞歸程序使用自旋鎖應該遵循如下原則:遞歸程序決不能在持有自旋鎖時調用它本身,也決不能在遞歸調用時試圖得到相同的自旋鎖。

JAVA中一種自旋鎖的實現:   CAS是Compare And Set的縮寫

import java.util.concurrent.atomic.AtomicReference;  
class SpinLock {  
        //java中原子(CAS)操做  
    AtomicReference<Thread> owner = new AtomicReference<Thread>();//持有自旋鎖的線程對象  
    private int count;  
    public void lock() {  
        Thread cur = Thread.currentThread();  
        //lock函數將owner設置爲當前線程,而且預測原來的值爲空。unlock函數將owner設置爲null,而且預測值爲當前線程。當有第二個線程調用lock操做時因爲owner值不爲空,致使循環    
  
            //一直被執行,直至第一個線程調用unlock函數將owner設置爲null,第二個線程才能進入臨界區。  
        while (!owner.compareAndSet(null, cur)){  
        }  
    }  
    public void unLock() {  
        Thread cur = Thread.currentThread();  
            owner.compareAndSet(cur, null);  
        }  
    }  
}  
public class Test implements Runnable {  
    static int sum;  
    private SpinLock lock;  
      
    public Test(SpinLock lock) {  
        this.lock = lock;  
    }  
    public static void main(String[] args) throws InterruptedException {  
        SpinLock lock = new SpinLock();  
        for (int i = 0; i < 100; i++) {  
            Test test = new Test(lock);  
            Thread t = new Thread(test);  
            t.start();  
        }  
          
        Thread.currentThread().sleep(1000);  
        System.out.println(sum);  
    }  
      
    @Override  
    public void run() {  
        this.lock.lock();  
        sum++;  
        this.lock.unLock();  
    }  
}

 

 

1、公平鎖/非公平鎖

公平鎖是指多個線程按照申請鎖的順序來獲取鎖。
非公平鎖是指多個線程獲取鎖的順序並非按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能,會形成優先級反轉或者飢餓現象。
對於ReentrantLock而言,經過構造函數指定該鎖是不是公平鎖,默認是非公平鎖。非公平鎖的優勢在於吞吐量比公平鎖大。
對於Synchronized而言,也是一種非公平鎖。因爲其並不像ReentrantLock是經過AQS的來實現線程調度,因此並無任何辦法使其變成公平鎖。
2、可重入鎖

可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。
說的有點抽象,下面會有一個代碼的示例。
對於Java ReentrantLock而言, 他的名字就能夠看出是一個可重入鎖,其名字是Re entrant Lock從新進入鎖。
對於Synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可必定程度避免死鎖。

複製代碼
synchronized void setA() throws Exception{

    Thread.sleep(1000); 
    setB();

}

synchronized void setB() throws Exception{

    Thread.sleep(1000);

}
複製代碼

3、獨享鎖/共享鎖

獨享鎖是指該鎖一次只能被一個線程所持有。
共享鎖是指該鎖可被多個線程所持有。
對於Java ReentrantLock而言,其是獨享鎖。可是對於Lock的另外一個實現類ReadWriteLock,其讀鎖是共享鎖,其寫鎖是獨享鎖。
讀鎖的共享鎖可保證併發讀是很是高效的,讀寫,寫讀 ,寫寫的過程是互斥的。
獨享鎖與共享鎖也是經過AQS來實現的,經過實現不一樣的方法,來實現獨享或者共享。
對於Synchronized而言,固然是獨享鎖。
4、互斥鎖/讀寫鎖

上面講的獨享鎖/共享鎖就是一種廣義的說法,互斥鎖/讀寫鎖就是具體的實現。
互斥鎖在Java中的具體實現就是ReentrantLock
讀寫鎖在Java中的具體實現就是ReadWriteLock
5、樂觀鎖/悲觀鎖

樂觀鎖與悲觀鎖不是指具體的什麼類型的鎖,而是指看待併發同步的角度。
悲觀鎖認爲對於同一個數據的併發操做,必定是會發生修改的,哪怕沒有修改,也會認爲修改。所以對於同一個數據的併發操做,悲觀鎖採起加鎖的形式。悲觀的認爲,不加鎖的併發操做必定會出問題。
樂觀鎖則認爲對於同一個數據的併發操做,是不會發生修改的。在更新數據的時候,會採用嘗試更新,不斷從新的方式更新數據。樂觀的認爲,不加鎖的併發操做是沒有事情的。
從上面的描述咱們能夠看出,悲觀鎖適合寫操做很是多的場景,樂觀鎖適合讀操做很是多的場景,不加鎖會帶來大量的性能提高。
悲觀鎖在Java中的使用,就是利用各類鎖。
樂觀鎖在Java中的使用,是無鎖編程,經常採用的是CAS算法,典型的例子就是原子類,經過CAS自旋實現原子操做的更新。
6、分段鎖

分段鎖實際上是一種鎖的設計,並非具體的一種鎖,對於ConcurrentHashMap而言,其併發的實現就是經過分段鎖的形式來實現高效的併發操做。
咱們以ConcurrentHashMap來講一下分段鎖的含義以及設計思想,ConcurrentHashMap中的分段鎖稱爲Segment,它即相似於HashMap(JDK7與JDK8中HashMap的實現)的結構,即內部擁有一個Entry數組,數組中的每一個元素又是一個鏈表;同時又是一個ReentrantLock(Segment繼承了ReentrantLock)。
當須要put元素的時候,並非對整個hashmap進行加鎖,而是先經過hashcode來知道他要放在那一個分段中,而後對這個分段進行加鎖,因此當多線程put的時候,只要不是放在一個分段中,就實現了真正的並行的插入。
可是,在統計size的時候,可就是獲取hashmap全局信息的時候,就須要獲取全部的分段鎖才能統計。
分段鎖的設計目的是細化鎖的粒度,當操做不須要更新整個數組的時候,就僅僅針對數組中的一項進行加鎖操做。
7、偏向鎖/輕量級鎖/重量級鎖

這三種鎖是指鎖的狀態,而且是針對Synchronized。在Java 5經過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是經過對象監視器在對象頭中的字段來代表的。
偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。下降獲取鎖的代價。
輕量級鎖是指當鎖是偏向鎖的時候,被另外一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,提升性能。
重量級鎖是指當鎖爲輕量級鎖的時候,另外一個線程雖然是自旋,但自旋不會一直持續下去,當自旋必定次數的時候,尚未獲取到鎖,就會進入阻塞,該鎖膨脹爲重量級鎖。重量級鎖會讓其餘申請的線程進入阻塞,性能下降。
8、自旋鎖

在Java中,自旋鎖是指嘗試獲取鎖的線程不會當即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減小線程上下文切換的消耗,缺點是循環會消耗CPU。
線程自旋和適應性自旋

咱們知道,java線程實際上是映射在內核之上的,線程的掛起和恢復會極大的影響開銷。
而且jdk官方人員發現,不少線程在等待鎖的時候,在很短的一段時間就得到了鎖,因此它們在線程等待的時候,並不須要把線程掛起,而是讓他無目的的循環,通常設置10次。
這樣就避免了線程切換的開銷,極大的提高了性能。

而適應性自旋,是賦予了自旋一種學習能力,它並不固定自旋10次一下。
他能夠根據它前面線程的自旋狀況,從而調整它的自旋,甚至是不通過自旋而直接掛起。
 

 


    二.synchronized使用示例
    1.多窗口售票
    假設一個火車票售票系統,有若干個窗口同時售票,很顯然在這裏票是做爲多個窗口的共享資源存在的,因爲座位號是肯定的,所以票上面的號碼也是肯定的,咱們用多個線程來模擬多個窗口同時售票,首先在不使用synchronized關鍵字的狀況下測試一下售票狀況。
    先將票自己做爲一個共享資源放在單獨的線程中,這種做爲共享資源存在的線程很顯然應該是實現Runnable接口,咱們將票的總數num做爲一個入參傳入,每次生成一個票以後將num作減法運算,直至num爲0即中止,說明票已經售完了,而後開啓多個線程將票資源傳入。

   public class Ticket implements Runnable{
     private int num;//票數量
     private boolean flag=true;//若爲false則售票中止
     public Ticket(int num){
     this.num=num;
     }
     @Override
     public void run() {
     while(flag){
     ticket();
     }
     }
     private void ticket(){
     if(num<=0){
     flag=false;
     return;
     }
     try {
     Thread.sleep(20);//模擬延時操做
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     //輸出當前窗口號以及出票序列號
     System.out.println(Thread.currentThread().getName()+"售出票序列號:"+num--);
     }
    }
    public class MainTest {
     public static void main(String[] args) {
     Ticketticket = new Ticket(5);
     Threadwindow01 = new Thread(ticket, "窗口01");
     Threadwindow02 = new Thread(ticket, "窗口02");
     Threadwindow03 = new Thread(ticket, "窗口03");
     window01.start();
     window02.start();
     window03.start();
     }
    }

程序的輸出結果以下:

  

    窗口02售出票序列號:5
    窗口03售出票序列號:4
    窗口01售出票序列號:5
    窗口02售出票序列號:3
    窗口01售出票序列號:2
    窗口03售出票序列號:2
    窗口02售出票序列號:1
    窗口03售出票序列號:0
    窗口01售出票序列號:-1

     從上面程序運行結果能夠看出不但票的序號有重號並且出票數量也不對,這種售票系統比12306可要爛多了,人家在繁忙的時候只是刷不到票而已,而這裏的售票系統倒好了,出票比預計的多了並且會出現多我的爭搶作同一個座位的風險。若是是單個售票窗口是不會出現這種問題,多窗口同時售票就會出現爭搶共享資源所以紊亂的現象,解決該現象也很簡單,就是在ticket()方法前面加上synchronized關鍵字或者將ticket()方法的方法體徹底用synchronized塊包括起來。

 //方式一
    private synchronized void ticket(){
     if(num<=0){
     flag=false;
     return;
     }
     try {
     Thread.sleep(20);//模擬延時操做
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     System.out.println(Thread.currentThread().getName()+"售出票序列號:"+num--);
    }
    //方式二
    private void ticket(){
     synchronized (this) {
     if (num <= 0) {
     flag = false;
     return;
     }
     try {
     Thread.sleep(20);//模擬延時操做
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     System.out.println(Thread.currentThread().getName() + "售出票序列號:" + num--);
     }
    }

   再看一下加入synchronized關鍵字的程序運行結果:   

    窗口01售出票序列號:5
    窗口03售出票序列號:4
    窗口03售出票序列號:3
    窗口02售出票序列號:2
    窗口02售出票序列號:1

 

    從這裏能夠看出在實例方法上面加上synchronized關鍵字的實現效果跟對整個方法體加上synchronized效果是同樣的。 另一點須要注意加鎖的時機也很是重要 ,本示例中ticket()方法中有兩處操做容易出現紊亂,一個是在if語句模塊,一處是在num–,這兩處操做自己都不是原子類型的操做,可是在使用運行的時候須要這兩處當成一個總體操做,因此synchronized將整個方法體都包裹在了一塊兒。如若否則,假設num當前值是1,可是窗口01執行到了num–,整個操做還沒執行完成,只進行了賦值運算還沒進行自減運算,可是窗口02已經進入到了if語句模塊,此時num仍是等於1,等到窗口02執行到了輸出語句的時候,窗口01的num–也已經將自減運算執行完成,這時候窗口02就會輸出序列號0的票。再者若是將synchronized關鍵字加在了run方法上面,這時候的操做不會出現紊亂或者錯誤,可是這種加鎖方式無異於單窗口操做,當窗口01拿到鎖進入run()方法以後,必須等到flag爲false纔會將語句執行完成跳出循環,這時候的num就已經爲0了,也就是說票已經被售賣完了,這種方式摒棄了多線程操做,違背了最初的設計原則-多窗口售票。
    2.懶漢式單例模式
    建立單例模式有不少中實現方式,本文只討論懶漢式建立。在Android開發過程當中單例模式能夠說是最常使用的一種設計模式,由於它操做簡單還能夠有效減小內存溢出。下面是懶漢式建立單例模式一個示例:

(懶漢式與餓漢式的區別:Singleton 單例模式(懶漢方式和餓漢方式)

 public class Singleton {
     private static Singletoninstance;
     private Singleton() {
     }
     public static SingletongetInstance() {
     if (instance == null) {
     instance = new Singleton();
     }
     return instance;
     }
    }

 若是對於多窗口售票邏輯已經徹底明白了的話就能夠看出這裏的實現方式是有問題的,咱們能夠簡單的建立幾個線程來獲取單例輸出對象的hascode值。

    com.sunny.singleton.Singleton@15c330aa
    com.sunny.singleton.Singleton@15c330aa
    com.sunny.singleton.Singleton@41aff40f

    在多線程模式下發現會出現不一樣的對象,這種單例模式很顯然不是咱們想要的,那麼根據上面多窗口售票的邏輯咱們在getInstance()方法上面加上一個synchronized關鍵字,給該方法加上鎖,加上鎖以後能夠避免多線程模式下生成多個不一樣對象,可是一樣會帶來一個效率問題,由於無論哪一個線性進入getInstance()方法都會先得到鎖,而後再次釋放鎖,這是一個方面,另外一個方面就是隻有在第一次調用getInstance()方法的時候,也就是在if語句塊內纔會出現多線程併發問題,而咱們卻索性將整個方法都上鎖了。討論到這裏就引出了另一個問題,到底是synchronized方法好仍是synchronized代碼塊好呢? 有一個原則就是鎖的範圍越小越好 ,加鎖的目的就是將鎖進去的代碼做爲原子性操做,由於非原子操做都不是線程安全的,所以synchronized代碼塊應該是在開發過程當中優先考慮使用的加鎖方式。

  public static SingletongetInstance() {
     if (instance == null) {
     synchronized (Singleton.class) {
     instance = new Singleton();
     }
     }
     return instance;
    }

  這裏也會遇到相似上面的問題,多線程併發下回生成多個實例,如線程A和線程B都進入if語句塊,假設線程A先得到鎖,線程B則等待,當new一個實例後,線程A釋放鎖,線程B得到鎖後會再次執行new語句,一樣不能保證單例要求,那麼下面代碼再來一個null判斷,進行雙重檢查上鎖呢?

 public static SingletongetInstance() {
     if (instance == null) {
     synchronized (Singleton.class) {
     if(instance==null){
     instance = new Singleton();
     }
     }
     }
     return instance;
    }

   該模式就是雙重檢查上鎖實現的單例模式,這裏在代碼層面咱們已經 基本 保證了線程安全了,可是仍是有問題的, 雙重檢查鎖定的問題是:並不能保證它會在單處理器或多處理器計算機上順利運行。雙重檢查鎖定失敗的問題並不歸咎於 JVM 中的實現bug,而是歸咎於java平臺內存模型。內存模型容許所謂的「無序寫入」,這也是這些習語失敗的一個主要緣由。 更爲詳細的介紹能夠參考 Java單例模式中雙重檢查鎖的問題 。因此單例模式建立比較建議使用惡漢式建立或者靜態內部類方式建立。

    3.synchronized不具備繼承性
    咱們能夠經過一個簡單的demo驗證這個問題,在一個方法中順序的輸出一系列數字,而且輸出該數字所在的線程名稱,在父類中加上synchronized關鍵字,子類重寫父類方法測試一下加上synchronized關鍵字和不加關鍵字的區別便可。

 public class Parent {
     public synchronized void test() {
     for (int i = 0; i < 5; i++) {
     System.out.println("Parent " + Thread.currentThread().getName() + ":" + i);
     try {
     Thread.sleep(500);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     }
     }
    }

 子類繼承父類Parent,重寫test()方法.

 public class Child extends Parent {
     @Override
     public void test() {
     for (int i = 0; i < 5; i++) {
     System.out.println("Child " + Thread.currentThread().getName() + ":" + i);
     try {
     Thread.sleep(500);
     } catch (InterruptedException e) {
     e.printStackTrace();
     }
     }
     }
    }

 測試代碼以下:

   final Child c = new Child();
    new Thread() {
     public void run() {
     c.test();
     };
    }.start();
    new Thread() {
     public void run() {
     c.test();
     };
    }.start();

   輸出結果以下:

    Parent Thread-0:0  Child Thread-0:0
    Parent Thread-0:1  Child Thread-1:0
    Parent Thread-0:2  Child Thread-0:1
    Parent Thread-0:3  Child Thread-1:1
    Parent Thread-0:4  Child Thread-0:2
    Parent Thread-1:0  Child Thread-1:2
    Parent Thread-1:1  Child Thread-0:3
    Parent Thread-1:2  Child Thread-1:3
    Parent Thread-1:3  Child Thread-0:4
    Parent Thread-1:4  Child Thread-1:4

    經過輸出信息能夠知道,父類Parent中會將單個線程中序列號輸出完成纔會執行另外一個線程中代碼,可是子類Child中確是兩個線程交替輸出數字,因此synchronized不具備繼承性。

    4.死鎖示例
    死鎖是多線程開發中比較常見的一個問題。如有多個線程訪問多個資源時,相互之間存在競爭,就容易出現死鎖。下面就是一個死鎖的示例,當一個線程等待另外一個線程持有的鎖時,而另外一個線程也在等待該線程鎖持有的鎖,這時候兩個線程都會處於阻塞狀態,程序便出現死鎖。

package com.lock;
   class Thread01 extends Thread{
    private Object resource01;
    private Object resource02;
    public Thread01(Object resource01, Object resource02) {
    this.resource01 = resource01;
    this.resource02 = resource02;
    }
    @Override
    public void run() {
    synchronized(resource01){
    System.out.println("Thread01 locked resource01");
    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    synchronized (resource02) {
    System.out.println("Thread01 locked resource02");
    }
    }
    }
   }
    class Thread02 extends Thread{
    private Object resource01;
    private Object resource02;
    public Thread02(Object resource01, Object resource02) {
    this.resource01 = resource01;
    this.resource02 = resource02;
    
    }
    @Override
    public void run() {
    synchronized(resource02){
    System.out.println("Thread02 locked resource02");
    try {
    Thread.sleep(500);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    synchronized (resource01) {
    System.out.println("Thread02 locked resource01");
    }
    }
    }
   }
   public class deadlock {
    public static void main(String[] args) {
    final Object resource01="resource01";
    final Object resource02="resource02";
    Thread01 thread01=new Thread01(resource01, resource02);
    Thread02 thread02=new Thread02(resource01, resource02);
    thread01.start();
    thread02.start();
    }
   }

結果爲:

Thread02 locked resource02
Thread01 locked resource01

 

  執行上面的程序就會一直等待下去,出現死鎖。當線程Thread01得到resource01的鎖後,等待500ms,而後嘗試獲取resource02的鎖,可是此時resouce02鎖已經被Thread02持有,一樣Thread02也等待了500ms嘗試獲取resouce01鎖,可是該所已經被Thread01持有,這樣兩個線程都在等待對方全部的資源,形成了死鎖。

    三.其它
    關鍵字synchronized具備鎖重入功能,當一個線程已經持有一個對象鎖後,再次請求該對象鎖時是能夠獲得該對象的鎖的,這種方式是必須的,不然在一個synchronized方法內部就沒有辦法調用該對象的另一個synchronized方法了。鎖重入是經過爲每一個所關聯一個計數器和一個佔有它的線程,當計數器爲0時,認爲鎖是未被佔有的。線程請求一個未被佔有的鎖時,JVM會記錄鎖的佔有者,並將計數器設置爲1。若是同一個線程再次請求該鎖,計數器會遞增,每次佔有的線程退出同步代碼塊時計數器會遞減,直至減爲0時鎖纔會被釋放。
    在聲明一個對象做爲鎖的時候要注意字符串類型鎖對象,由於字符串有一個常量池,若是不一樣的線程持有的鎖是具備相同字符的字符串鎖時,兩個鎖實際上同一個鎖。

  

ReentrantLock特性 

輪詢鎖的和定時鎖

可輪詢和可定時的鎖請求是經過tryLock()方法實現的,和無條件獲取鎖不同. ReentrantLock能夠有靈活的容錯機制.死鎖的不少狀況是因爲順序鎖引發的, 不一樣線程在試圖得到鎖的時候阻塞,而且不釋放本身已經持有的鎖, 最後形成死鎖. tryLock()方法在試圖得到鎖的時候,若是該鎖已經被其它線程持有,則按照設置方式馬上返回,而不是一直阻塞等下去,同時在返回後釋放本身持有的鎖.能夠根據返回的結果進行重試或者取消,進而避免死鎖的發生.

公平性

ReentrantLock構造函數中提供公平性鎖和非公平鎖(默認)兩種選擇。所謂公平鎖,線程將按照他們發出請求的順序來獲取鎖,不容許插隊;但在非公平鎖上,則容許插隊:當一個線程發生獲取鎖的請求的時刻,若是這個鎖是可用的,那這個線程將跳過所在隊列裏等待線程並得到鎖。咱們通常但願全部鎖是非公平的。由於當執行加鎖操做時,公平性將講因爲線程掛起和恢復線程時開銷而極大的下降性能。考慮這麼一種狀況:A線程持有鎖,B線程請求這個鎖,所以B線程被掛起;A線程釋放這個鎖時,B線程將被喚醒,所以再次嘗試獲取鎖;與此同時,C線程也請求獲取這個鎖,那麼C線程極可能在B線程被徹底喚醒以前得到、使用以及釋放這個鎖。這是種共贏的局面,B獲取鎖的時刻(B被喚醒後才能獲取鎖)並無推遲,C更早地獲取了鎖,而且吞吐量也得到了提升。在大多數狀況下,非公平鎖的性能要高於公平鎖的性能。

可中斷獲鎖獲取操做

lockInterruptibly方法可以在獲取鎖的同時保持對中斷的響應,所以無需建立其它類型的不可中斷阻塞操做。

讀寫鎖ReadWriteLock

​ReentrantLock是一種標準的互斥鎖,每次最多隻有一個線程能持有鎖。讀寫鎖不同,暴露了兩個Lock對象,其中一個用於讀操做,而另一個用於寫操做。 

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading.
     */
    Lock readLock(); 

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing.
     */
    Lock writeLock();
}

 可選擇實現:

1.釋放優先
2.讀線程插隊
3.重入性
4.降級
5.升級

ReentrantReadWriteLock實現了ReadWriteLock接口,構造器提供了公平鎖和非公平鎖兩種建立方式。讀寫鎖適用於讀多寫少的狀況,能夠實現更好的併發性。

參考:Java併發編程之顯示鎖ReentrantLock和ReadWriteLock讀寫鎖

相關文章
相關標籤/搜索