悲觀鎖,樂觀鎖,自旋鎖,偏向鎖,輕量級鎖,CAS,版本號機制總結

線程同步的各種方式(悲觀鎖,樂觀鎖,自旋鎖,偏向鎖,輕量級鎖,CAS,版本號機制總結)

版權聲明:本文爲博主原創文章,未經博主允許不得轉載。 https://blog.csdn.net/zhangjingao/article/details/86516038

前言

  最近研究了好久的java的多線程和線程同步及鎖的實現方式,自我感覺小有所得,整理出來以後自己回顧,其中包括悲觀鎖,樂觀鎖以及兩者的實現方式,包括自旋鎖,自適應自旋鎖,偏向鎖,輕量級鎖,重量鎖,版本號機制,CAS操作。
  

目錄

  • 悲觀鎖
  • 樂觀鎖
  • 區別和聯繫
  • 悲觀鎖的實現方式
    • 自旋鎖
    • 自適應自旋鎖
    • 偏向鎖
    • 輕量級鎖
    • 重量級鎖
  • 樂觀鎖的實現方式
    • 版本號機制
    • CAS機制
        
        

悲觀鎖

  顧名思義,其實就是在獲取數據的時候假設有線程也在惦記那數據,但是要實現線程同步啊,所以就先獨佔資源,讓其他線程阻塞,先加鎖,然後獨佔資源,再處理數據,直到釋放鎖。這樣就實現了線程同步,它靠的是加鎖。
  應用: java的synchronized鎖,ReentranLock;數據庫的行鎖,表鎖,都是悲觀鎖思想的體現。

樂觀鎖

  看了悲觀鎖,那就很明顯就能明白樂觀鎖了,同樣的道理,樂觀鎖的思想就是它覺得沒人在用這個數據,很樂觀嘛,認爲沒線程在佔用資源,所以不會加鎖。那怎麼實現數據同步呢?就是在當它更新數據的時候會檢測下在此期間有沒有其他線程使用了該數據,可以使用版本號機制和CAS算法(compare and swap)實現。
  應用:java的java.util.concurrent.atomic包下的原子類(使用CAS實現),數據庫的write_condition機制。

聯繫和區別

  悲觀鎖和樂觀鎖並不是一種鎖實現方式,而是一種鎖的思想
  都實現了線程同步。
  悲觀鎖因爲獨佔資源,所以比較在讀多寫少的情況下,這樣比較影響性能,因爲數據都不變嘛,這樣的情況下還加了把鎖,但是在寫多的情況下,這個機制就非常適合。它適合寫多讀少的場景。
  樂觀鎖因爲不加鎖,實現同步使用的是操作系統的CAS操作,CAS操作又是一個耗費資源的操作,所以在樂觀鎖碰到寫多的時候就比較糟糕了。它適合讀多寫少的場景。

  兩者並不好說優劣,因爲存在即合理,兩者適用場景不同。我個人建議使用synchronized,在jdk1.6之後,引入了輕量級鎖,偏向鎖,在線程衝突小的時候,可以獲得和CAS差不多的性能,在線程衝突較多的時候,性能比CAS就好得多。
  
  

悲觀鎖的實現方式(這裏介紹synchronized機制)

自旋鎖

  自旋鎖,就是當線程在競爭鎖時,發現,哎,鎖被佔用了,那我能被阻塞嗎?如果被阻塞之後還會再次喚醒,這個過程中,操作系統要在用戶態和內核態之中進行切換,這個操作是很耗費cpu性能的,但是有時候很快鎖就被owner線程給釋放了,這樣就沒必要切換狀態。這個時候呢,自旋鎖就是在競爭不到鎖的時候,先不阻塞,進入自旋鎖,這個鎖在我理解中不是一種鎖,而是一種競爭鎖的狀態,在這個過程中,他就是做一些無意義的操作(如:幾次空循環),然後在這個過程中會再次競爭鎖,如果還沒有競爭到,則會阻塞。自旋鎖在鎖持有時間長,鎖競爭不激烈的情況下更能突出性能。
  優點:
  - 自旋鎖減少了線程在阻塞和喚醒之間切換的頻率,那麼就是降低了操作系統在用戶態和內核態的切換,降低了cpu的損耗,提高了線程在競爭期間的性能。
  缺點:
  - 在單核處理器中,並不存在所謂的並行,所以說當owner線程在佔據cpu時,由於時間片輪轉運行到等待線程時,等待線程在那自旋沒有用的,因爲owner線程此時沒有辦法釋放鎖,那麼此時自旋就會很多餘,當時現在的電腦基本都是4核,6核還有8核的,所以這個問題在以前存在,現在基本還好。
  - 自旋鎖的時間無法判斷,如果線程持有鎖的時間短,小於或者等於自旋的時間,那麼就很舒服,自旋過了剛好獲得了鎖,但是很多時候這個鎖持有時間長短就要看代碼設計了,所以無法預測,這就很難受。
  這些問題在1.6引入了自適應自旋鎖之後得到了優化。
  

自適應自旋鎖

  java在jdk1.6之後引入了自適應自旋鎖,主要是爲了解決自旋鎖自旋時間不合理問題的。對於之前的自旋鎖而言,自旋時間不好控制,而且分配也不合理,很可能owner線程很快就釋放鎖了,但是就因爲自旋時間不合理錯過了而導致cpu在用戶態和內核態切換。
  自適應自旋鎖是根據上一個線程競爭這個鎖所需要的時間來決定當前線程自旋的時間。對於操作系統來說,上一個線程競爭鎖所需要的時間極有可能是這次所需的時間,所以自旋一般控制在10-100之內。如果上一個線程很快就獲得了鎖,那麼就認爲很快就能獲得鎖,所以就允許自旋時間長一點,比如100個循環;如果上一個線程很久才獲得鎖,那麼就認爲很難獲得鎖,就讓自旋時間短一點或者直接阻塞,以免多餘浪費cpu。自旋後仍然失敗,那麼就阻塞當前線程。
  優點:
  - 優點很明顯,優化了自旋時間不合理的問題,動態分配自旋時間。
  缺點
  - 依舊沒有完全解決時間不合理問題
  

偏向鎖

  有些時候,在整個同步週期內是沒有競爭的,在這時,又只有一個線程在運行,這個時候沒有其他線程在和它爭搶cpu,那麼這個時候進行的加鎖,釋放鎖,重入鎖等等操作都是多餘且降低性能的。所以這個時候就進入了偏向鎖的狀態。
  在這個過程中,線程可以忽略這些鎖,不會進行鎖的操作,就是好像在偏向這個線程一樣,只有初始化的時候使用一次鎖的操作,也就是它整個過程只有進入偏向鎖這個狀態使用CAS切換了下狀態,其他時候任何的鎖和CAS都不會做,偏向鎖就是力取在無競爭的情況下將同步都去掉。如果此時再有其他線程競爭鎖,那麼偏向鎖會膨脹爲輕量級鎖
  什麼叫在整個同步週期內是沒有競爭的? 看下面的代碼


public class ThreadTemp {

    public static void main(String[] args) {


        NoCompete compete = new NoCompete(10,11);
        Thread thread1 = new Thread(compete,"線程1");
        Thread thread2 = new Thread(compete,"線程2");
        thread1.start();
        thread2.start();
    }

}

class NoCompete implements Runnable {

    private int a, b;

    NoCompete(int a, int b) {
        this.a = a;
        this.b = b;
    }

//    同步方法,無實際競爭
    private synchronized void count1() {
//        ....do something
        System.out.println("線程名稱:"+Thread.currentThread().getName()+": "+a+"+"+b+"= "+(a+b));
    }

//    同步方法,有實際競爭
    private synchronized void count2() {
        a++;
        b++;
        System.out.println("線程名稱:"+Thread.currentThread().getName()+": "+a+"+"+b+"= "+(a+b));
    }

    @Override
    public void run() {
        try {
            //睡1s,模仿做了一些業務邏輯,拉長業務線,容易看到搶佔cpu
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        count1();
    }
}


  因爲用到的是同一個對象,兩個線程都會競爭這一個對象鎖。但是count1和count2就不一樣了。
  在count1中從未動過數據,這就是沒有實際競爭,實際上沒有必要保證線程同步,完全可以放心的在兩個線程間切換cpu,分析證明很多同步代碼塊,很多一部分並沒有實際的競爭,並不需要線程同步。
  在count2中兩個線程對a,b進行了修改,這個時候不同的執行情況就會得到不同的執行效果,這個時候就是有實際競爭,就要保證線程同步,如果不保證線程同步,就會出現讀髒數據等異常情況,得不到實際的理想執行效果。

輕量級鎖

  輕量級鎖是相對於重量級鎖而言的,當沒有鎖競爭,但是有多個線程在使用鎖的時候,這個時候就是用CAS獲得輕量級鎖,注意!使用的是CAS操作,這是在沒有鎖競爭的情況下。這樣做的好處就是避免了使用傳統的重量級鎖互斥量mutex的操作
  在輕量級鎖過程中,不會阻塞後來線程,CPU可以在多個線程中交替運行。
  如果輕量級鎖遭遇了 鎖競爭,哎呦,那麼不僅需要CAS操作還需要互斥量,性能比之重量級鎖還差,所以這時候它會膨脹爲重量級鎖。

  

重量級鎖

  就是咱們作爲菜鳥的時候理解的那種鎖,可重可重的,哈哈。就是直接阻塞掉後來線程,owner沒有釋放鎖,其他線程就只能等待owner線程釋放鎖。內部實現靠的是操作系統的互斥量mutex來實現的

膨脹過程

  鎖的膨脹過程是不可逆的,只能越來越高級,這樣做的目的是加快獲得和釋放鎖的速度。
  偏向鎖->輕量級鎖->重量級鎖,自旋鎖(自適應自旋鎖)是用來降低阻塞和調用之間的切換造成的操作系統從用戶態到內核態而執行的一段過程。

鎖類別 特點
偏向鎖 無實際鎖競爭,且只有一個線程使用鎖。
輕量級鎖 無實際鎖競爭,多個線程交替得到鎖,允許短時間的鎖競爭。
重量級鎖 有實際競爭,且鎖競爭時間長


樂觀鎖的實現方式

  寫完悲觀鎖的實現方式,看看樂觀鎖的實現方式。樂觀鎖有兩種方式,一種版本號機制,一種是CAS操作。

版本號機制

  所謂版本號機制就是給所有未修改時期的數據一個版本號,當且僅當自己所拿的數據的版本號大於等於數據當前的版本號,那麼允許你進行操作。
  舉栗子:(遊戲場景)線程A(攻擊100血量)和線程B(攻擊100血量)對數據M(血量200)進行操作。
  數據M當前版本號是1,那麼A來了後拿到M(200血量)是1,B來了拿到M(200血量)也是1,然後A修改的時候檢測版本號相等可以修改,M血量減到了100,版本號變爲了2;這時候B來了也要修改,發現:哎,不對,版本號比M的小,被人動過了,就重新更新該數據,再次檢測版本號,直到修改成功。

CAS操作

  全稱是compare and swap,他的僞代碼是這樣的

void compareAndSwap (V v , object a, object b) {
	If (v == a) {
		v = b;
	}
}

  它有三個參數,一個數據地址V,一個原始值A,一個新值B,拿地址V上的值和A比較,如果相等,就將新值B賦值給V上的數據。
  這個操作並沒有引入鎖,但是實現了線程同步,所以也叫非阻塞同步(Non-blocking Synchronization))。

  但是他有幾個問題

  1. CAS的ABA問題:
      當A自增了又自減了回到了原來的值,這個時候CAS操作並不能檢測出來,但是在A被修改這段時間或許也做了其他操作,比如修改了其他數據的值等影響同步的操作。
    在jdk1.5之後,java提供了AtomicStampedReference類,其中的compareAndSwap方法既可以保證引用是否等於預期引用,還能保證當前標誌是否等於預期標誌,如果全部相等纔會將內存的值賦值爲新的值。
  2. 自旋耗費cpu
      在CAS執行時,如果數據沒有更新成功,就會陷入自旋,如果長期不成功,就會一直自旋直到成功,這給CPU增加了很大的運行開銷。
  3. 只能保證一個變量的原子操作
      因爲它就一個原始數據參數,所以只能比較一個值。從jdk1.5之後,可以通過封裝AtomicReference類對象來保證引用多個數據的原子一致性。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章