天天看點

Java 鎖(一)樂觀鎖/悲觀鎖/公平鎖/非公平鎖/synchronized

作者:做好一個程式猿

樂觀鎖/悲觀鎖

樂觀鎖

樂觀鎖總是認為不存在并發問題,每次去取資料的時候,總認為不會有其他線程對資料進行修改,是以不會上鎖。但是在更新時會判斷其他線程在這之前有沒有對資料進行修改,一般會使用“資料版本機制”或“CAS操作”來實作。eg: 文檔線上編輯,git等都是用的樂觀鎖

悲觀鎖

悲觀鎖認為對于同一個資料的并發操作,一定會發生修改的,哪怕沒有修改,也會認為修改。是以對于同一份資料的并發操作,悲觀鎖采取加鎖的形式。悲觀的認為,不加鎖并發操作一定會出問題。比如Java裡面的同步原語synchronized關鍵字的實作就是悲觀鎖。

synchronized八種案例

package com.atguigu.juc.locks;


import java.util.concurrent.TimeUnit;

class Phone {
    public synchronized void sendEmail() {
        //暫停幾秒鐘線程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-------sendEmail");
    }

    public synchronized void sendSMS() {
        System.out.println("-------sendSMS");
    }

    public void hello() {
        System.out.println("-------hello");
    }
}

public class Lock8Demo {
    public static void main(String[] args){
        Phone phone = new Phone();//資源類1
        Phone phone2 = new Phone();//資源類2

        new Thread(() -> {
            phone.sendEmail();
        },"a").start();

        //暫停毫秒
        try { TimeUnit.MILLISECONDS.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            //phone.sendSMS();
            //phone.hello();
            phone.sendSMS();
        },"b").start();
    }
}
           

案例

  1. 一部手機标準通路有ab兩個線程,請問先列印郵件還是短信 (普通方法)
  2. sendEmail方法暫停3秒鐘,請問先列印郵件還是短信 (普通方法)
  3. 新增一個普通的hello方法,請問先列印郵件還是hello (普通方法)
  4. 有兩部手機,請問先列印郵件還是短信 (普通方法)
  5. 兩個靜态同步方法,同1部手機,請問先列印郵件還是短信
  6. 兩個靜态同步方法, 2部手機,請問先列印郵件還是短信
  7. 1個靜态同步方法,1個普通同步方法,同1部手機,請問先列印郵件還是短信
  8. 1個靜态同步方法,1個普通同步方法,2部手機,請問先列印郵件還是短信

說明

1-2

  • 一個對象裡面如果有多個synchronized方法,某一個時刻内,隻要一個線程去調用其中的一個synchronized方法了,其它的線程都隻能等待,換句話說,某一個時刻内,隻能有唯一的一個線程去通路這些synchronized方法。鎖的是目前對象this,被鎖定後,其它的線程都不能進入到目前對象的其它的synchronized方法

3-4

  • 加個普通方法後發現和同步鎖無關,hello
  • 換成兩個對象後,不是同一把鎖了,情況立刻變化。

5-6 都換成靜态同步方法後

三種 synchronized 鎖的内容有一些差别:

  • 對于普通同步方法,鎖的是目前執行個體對象,通常指this,具體的一部部手機,所有的普通同步方法用的都是同一把鎖,也就是說所得是執行個體對象本身。
  • 對于靜态同步方法,鎖的是目前類的Class對象,如Phone.class唯一的一個模闆
  • 對于同步方法塊,鎖的是 synchronized 括号内的對象

7-8

  • 當一個線程試圖通路同步代碼時它首先必須得到鎖,退出或抛出異常時必須釋放鎖。
  • 所有的普通同步方法用的都是同一把鎖——執行個體對象本身,就是new出來的具體執行個體對象本身,本類this,也就是說如果一個執行個體對象的普通同步方法擷取鎖後,該執行個體對象的其他普通同步方法必須等待擷取鎖的方法釋放鎖後才能擷取鎖。
  • 所有的靜态同步方法用的也是同一把鎖——類對象本身,就是我們說過的唯一模闆Class
  • 具體執行個體對象this和唯一模闆Class,這兩把鎖是兩個不同的對象,是以靜态同步方法與普通同步方法之間是不會有競态條件的,但是一旦一個靜态同步方法擷取鎖後,其他的靜态同步方法都必須等待該方法釋放鎖後才能擷取鎖。

從位元組碼角度分析synchronized

javap -c ***.class檔案反編譯,假如你需要更多資訊 javap -v ***.class檔案反編譯。

synchronized 同步代碼塊

首先是需要得到鎖才能執行同步代碼,當退出或者抛出異常時必須要釋放鎖,那麼它是如何來實作這個機制的呢?我們先看一段簡單的代碼:

package com.paddx.test.concurrent;
public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}           

檢視反編譯後結果:_ _

Java 鎖(一)樂觀鎖/悲觀鎖/公平鎖/非公平鎖/synchronized

反編譯結果

monitorenter:每個對象都是一個螢幕鎖(monitor)。當monitor被占用時就會處于鎖定狀态,線程執行monitorenter指令時嘗試擷取monitor的所有權,過程如下:

    1. 如果monitor的進入數為0,則該線程進入monitor,然後将進入數設定為1,該線程即為monitor的所有者;
    2. 如果線程已經占有該monitor,隻是重新進入,則進入monitor的進入數加1;
    3. 如果其他線程已經占用了monitor,則該線程進入阻塞狀态,直到monitor的進入數為0,再重新嘗試擷取monitor的所有權;

monitorexit:執行monitorexit的線程必須是objectref所對應的monitor的所有者。指令執行時,monitor的進入數減1,如果減1後進入數為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去擷取這個 monitor 的所有權。

monitorexit指令出現了兩次,第1次為同步正常退出釋放鎖;第2次為發生異常退出釋放鎖;防止出現鎖無法釋放,并發問題。

通過上面兩段描述,我們應該能很清楚的看出Synchronized的實作原理,Synchronized的語義底層是通過一個monitor的對象來完成,其實wait/notify等方法也依賴于monitor對象,這就是為什麼隻有在同步的塊或者方法中才能調用wait/notify等方法,否則會抛出java.lang.IllegalMonitorStateException的異常的原因。

一定是一個enter兩個exit嗎?

手動throw 一個異常的情況,在程式正常執行時也抛出一個異常,那麼無論在抛出異常的代碼之前的代碼正常還是異常,程式最後都會走異常處理的流程,就不需要正常的monitorexit處理,這樣位元組碼中就會隻有一個monitorexit

Java 鎖(一)樂觀鎖/悲觀鎖/公平鎖/非公平鎖/synchronized

synchronized 普通同步方法

package com.paddx.test.concurrent;

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}           

檢視反編譯後結果:

Java 鎖(一)樂觀鎖/悲觀鎖/公平鎖/非公平鎖/synchronized

從編譯的結果來看,方法的同步并沒有通過指令 monitorenter 和 monitorexit 來完成(理論上其實也可以通過這兩條指令來實作),不過相對于普通方法,其常量池中多了 ACC_SYNCHRONIZED 标示符。JVM就是根據該标示符來實作方法的同步的:

當方法調用時,調用指令将會檢查方法的 ACC_SYNCHRONIZED 通路标志是否被設定,如果設定了,執行線程将先擷取monitor,擷取成功之後才能執行方法體,方法執行完後再釋放monitor。

兩種同步方式本質上沒有差別,隻是方法的同步是一種隐式的方式來實作,無需通過位元組碼來完成。兩個指令的執行是JVM通過調用作業系統的互斥原語mutex來實作,被阻塞的線程會被挂起、等待重新排程,會導緻“使用者态和核心态”兩個态之間來回切換,對性能有較大影響。

synchronized 靜态同步方法

ACC_STATIC, ACC_SYNCHRONIZED通路标志區分該方法是否靜态同步方法

Java 鎖(一)樂觀鎖/悲觀鎖/公平鎖/非公平鎖/synchronized

反編譯synchronized鎖的是什麼

什麼是管程monitor

Java 鎖(一)樂觀鎖/悲觀鎖/公平鎖/非公平鎖/synchronized

為什麼任何一個對象都可以成為一個鎖

因為每個對象都内置了一個ObjectMonitor螢幕對象,這個螢幕是用c++寫的,底層依賴作業系統的互斥量mutex實作的加鎖解鎖。

Object.java → ObjectMonitor.java→ObjectMonitor.cpp→objectMonitor.hpp

Java 鎖(一)樂觀鎖/悲觀鎖/公平鎖/非公平鎖/synchronized

公平鎖/非公平鎖

公平鎖是指多個線程按照申請鎖的順序來擷取鎖。

非公平鎖是指多個線程擷取鎖的順序并不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先擷取鎖。有可能,會造成優先級反轉或者饑餓現象。

為什麼會有公平鎖/非公平鎖的設計為什麼預設非公平?

Java 鎖(一)樂觀鎖/悲觀鎖/公平鎖/非公平鎖/synchronized
  1. 恢複挂起的線程到真正鎖的擷取還是有時間差的,從開發人員來看這個時間微乎其微,但是從CPU的角度來看,這個時間差存在的還是很明顯的。是以非公平鎖能更充分的利用CPU 的時間片,盡量減少 CPU 空閑狀态時間。
  2. 使用多線程很重要的考量點是線程切換的開銷,當采用非公平鎖時,當1個線程請求鎖擷取同步狀态,然後釋放同步狀态,因為不需要考慮是否還有前驅節點,是以剛釋放鎖的線程在此刻再次擷取同步狀态的機率就變得非常大,是以就減少了線程的開銷。

什麼時候用公平?什麼時候用非公平?

如果為了更高的吞吐量,很顯然非公平鎖是比較合适的,因為節省很多線程切換時間,吞吐量自然就上去了;否則那就用公平鎖,大家公平使用。

可重入鎖

可重入鎖”的概念:自己可以再次擷取自己的内部鎖。比如一個線程獲得了某個對象的鎖,此時鎖還沒釋放,當再次擷取這個對象鎖的時候還可以擷取。對于Java ReetrantLock而言,從名字就可以看出是一個重入鎖,其名字是Reentrant Lock 重新進入鎖。

對于Synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖。

Java 鎖(一)樂觀鎖/悲觀鎖/公平鎖/非公平鎖/synchronized

繼續閱讀