JUC併發編程原理精講(源碼分析)

来源:https://www.cnblogs.com/buchizicai/archive/2023/05/04/17278656.html
-Advertisement-
Play Games

併發編程是指在程式中使用多線程技術來實現並行處理的能力。多線程機制使得程式可以分解成互不幹擾的任務,從而提高了程式執行的效率。併發編程可以通過對線程的創建,管理和協作進行控制,以實現更加高效的併發執行。併發編程的優點包括:① 提高程式執行效率:通過多線程並行處理,程式的處理速度可以顯著提高。② 增強... ...


1. JUC前言知識

JUC即 java.util.concurrent

涉及三個包:

  • java.util.concurrent
  • java.util.concurrent.atomic
  • java.util.concurrent.locks

普通的線程代碼:

  • Thread
  • Runnable 沒有返回值、效率相比入 Callable 相對較低!
  • Callable 有返回值!【工作常用】

1.1 進程和線程

進程:是指一個記憶體中運行的程式,每個進程都有一個獨立的記憶體空間,一個應用程式可以同時運行多個進程。進程是資源分配的單位。

記憶:進程的英文為Process,Process也為過程,所以進程可以大概理解為程式執行的過程。

(進程也是程式的一次執行過程,是系統運行程式的基本單位; 系統運行一個程式即是一個進程從創建、運行到消亡的過程)

線程:進程中的一個執行單元,負責當前進程中程式的執行。一個進程中是可以有多個線程的。線程是CPU調度和執行的單位。

【java預設有兩個線程:main、GC】


舉例:打開word使用是一個進程,word會檢查你的拼寫,兩個線程:容災備份,語法檢查


進程與線程的區別

  • 進程:有獨立的記憶體空間,進程中的數據存放空間(堆空間和棧空間)是獨立的,至少有一個線程。
  • 線程:堆空間是共用的,棧空間是獨立的,線程消耗的資源比進程小的多

1.2 併發與並行

並行 :指兩個或多個事件在同一時刻發生(同時發生)【多個CPU同時執行多個線程】

併發 :指兩個或多個事件在同一個時間段內發生。(交替執行) 【一個CPU交替執行線程】


拓展

  1. 併發編程的本質是充分利用cpu資源

    java代碼查詢cpu核數:

    //查詢cpu核數
    //CPU 密集型,IO密集型
    System.out.println(Runtime.getRuntime().availableProcessors());
    
  2. java真的可以開啟線程嗎?不能,通過源碼可知底層開啟線程的start()方法是native修飾的,意思是調用操作系統C++的代碼

    //本地方法,底層的C++ java無法直接操作硬體
    private native void start0();
    

1.3 線程六種狀態

  1. NEW(新建)
    線程剛被創建,但是並未啟動。還沒調用start方法
  2. Runnable(可運行)
    線程可以在java虛擬機中運行的狀態,可能正在運行自己代碼,也可能沒有,這取決於操 作系統處理器
  3. Blocked(鎖阻塞)
    當一個線程試圖獲取一個對象鎖,而該對象鎖被其他的線程持有,則該線程進入Blocked狀 態;當該線程持有鎖時,該線程將變成Runnable狀態。
  4. Waiting(無限等待)
    一個線程在等待另一個線程執行一個(喚醒)動作時,該線程進入Waiting狀態。
    進入這個 狀態後是不能自動喚醒的,必須等待另一個線程調用notify或者notifyAll方法才能夠喚醒。
  5. Timed Waiting(計時等待)
    同waiting狀態,有幾個方法有超時參數,調用他們將進入Timed Waiting狀態。
    這一狀態 將一直保持到超時期滿或者接收到喚醒通知。帶有超時參數的常用方法有Thread.sleep 、 Object.wait
  6. Teminated(被終止)
    因為run方法正常退出而死亡,或者因為沒有捕獲的異常終止了run方法而死亡。

上源碼:

public enum State {
    /**
     * 新建
     */
    NEW,

    /**
     * 運行
     */
    RUNNABLE,

    /**
     * 阻塞
     */
    BLOCKED,

    /**
     * 等待,死死的等
     */
    WAITING,

    /**
     * 超時等待
     */
    TIMED_WAITING,

    /**
     * 停止
     */
    TERMINATED;
}

1.4 sleep與wait區別

只要是等待都需要拋出異常,中斷異常

  1. 來自不同的類

    • wait -> Object

    • sleep -> Thread

  2. 關於鎖的釋放

    • wait會釋放鎖
    • sleep睡覺了,抱著鎖睡覺,不會釋放!
  3. 使用的範圍是不同的

    • wait必須在同步代碼塊中
    • sleep可以在任何地方睡

1.5 解耦寫線程

學生寫法(多耦):

public class SaleTickerDemo01 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(ticket, "A").start();
        new Thread(ticket, "B").start();
		new Thread(ticket, "C").start();
    }
}

class Ticket implements Runnable{
    private int number = 50;

    public void run(){
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "買了第" + (number--) + "張票");
        }
    }
}

工作寫法(解耦):

public class SaleTickerDemo01 {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                ticket.sale();
            }
        }, "C").start();

    }
}

class Ticket {
    private int number = 50;

    public synchronized void sale() {
        if (number > 0) {
            System.out.println(Thread.currentThread().getName() + "買了第" + (number--) + "張票");
        }
    }
}

1.6 鎖基礎

較難,能理解就理解

1.6.1 鎖機制

通過使用synchronized關鍵字來實現鎖,這樣就能夠很好地解決線程之間爭搶資源的情況。那麼,synchronized底層到是如何實現的呢?

我們知道,使用synchronized,一定是和某個對象相關聯的,比如我們要對某一段代碼加鎖,那麼我們就需要提供一個對象來作為鎖本身:

public static void main(String[] args) {
    synchronized (Main.class) {
        //這裡使用的是Main類的Class對象作為鎖
    }
}

我們來看看,它變成位元組碼之後會用到哪些指令:

image

其中最關鍵的就是monitorenter指令了,可以看到之後也有monitorexit與之進行匹配(註意這裡有2個),monitorentermonitorexit分別對應加鎖和釋放鎖,在執行monitorenter之前需要嘗試獲取鎖,每個對象都有一個monitor監視器與之對應,而這裡正是去獲取對象監視器的所有權,一旦monitor所有權被某個線程持有,那麼其他線程將無法獲得(管程模型的一種實現)。

在代碼執行完成之後,我們可以看到,一共有兩個monitorexit在等著我們,那麼為什麼這裡會有兩個呢,按理說monitorentermonitorexit不應該一一對應嗎,這裡為什麼要釋放鎖兩次呢?

首先我們來看第一個,這裡在釋放鎖之後,會馬上進入到一個goto指令,跳轉到15行,而我們的15行對應的指令就是方法的返回指令,其實正常情況下只會執行第一個monitorexit釋放鎖,在釋放鎖之後就接著同步代碼塊後面的內容繼續向下執行了。而第二個,其實是用來處理異常的,可以看到,它的位置是在12行,如果程式運行發生異常,那麼就會執行第二個monitorexit,並且會繼續向下通過athrow指令拋出異常,而不是直接跳轉到15行正常運行下去。

image

實際上synchronized使用的鎖就是存儲在Java對象頭中的,我們知道,對象是存放在堆記憶體中的,而每個對象內部,都有一部分空間用於存儲對象頭信息,而對象頭信息中,則包含了Mark Word用於存放hashCode和對象的鎖信息,在不同狀態下,它存儲的數據結構有一些不同。image


1.6.2 重量級鎖

在JDK6之前,synchronized一直被稱為重量級鎖,monitor依賴於底層操作系統的Lock實現,Java的線程是映射到操作系統的原生線程上,切換成本較高。而在JDK6之後,鎖的實現得到了改進。我們先從最原始的重量級鎖開始:

我們說了,每個對象都有一個monitor與之關聯,在Java虛擬機(HotSpot)中,monitor是由ObjectMonitor實現的:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //記錄個數
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //處於wait狀態的線程,會被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
}

每個等待鎖的線程都會被封裝成ObjectWaiter對象,進入到如下機制:

image

ObjectWaiter首先會進入 Entry Set等著,當線程獲取到對象的monitor後進入 The Owner 區域並把monitor中的owner變數設置為當前線程,同時monitor中的計數器count加1,若線程調用wait()方法,將釋放當前持有的monitorowner變數恢復為nullcount自減1,同時該線程進入 WaitSet集合中等待被喚醒。若當前線程執行完畢也將釋放monitor並複位變數的值,以便其他線程進入獲取對象的monitor

雖然這樣的設計思路非常合理,但是在大多數應用上,每一個線程占用同步代碼塊的時間並不是很長,我們完全沒有必要將競爭中的線程掛起然後又喚醒,並且現代CPU基本都是多核心運行的,我們可以採用一種新的思路來實現鎖。

在JDK1.4.2時,引入了自旋鎖(JDK6之後預設開啟),它不會將處於等待狀態的線程掛起,而是通過無限迴圈的方式,不斷檢測是否能夠獲取鎖,由於單個線程占用鎖的時間非常短,所以說迴圈次數不會太多,可能很快就能夠拿到鎖並運行,這就是自旋鎖。當然,僅僅是在等待時間非常短的情況下,自旋鎖的表現會很好,但是如果等待時間太長,由於迴圈是需要處理器繼續運算的,所以這樣只會浪費處理器資源,因此自旋鎖的等待時間是有限制的,預設情況下為10次,如果失敗,那麼會進而採用重量級鎖機制。

image

在JDK6之後,自旋鎖得到了一次優化,自旋的次數限制不再是固定的,而是自適應變化的,比如在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,並且持有鎖的線程正在運行,那麼這次自旋也是有可能成功的,所以會允許自旋更多次。當然,如果某個鎖經常都自旋失敗,那麼有可能會不再採用自旋策略,而是直接使用重量級鎖。


1.6.3 輕量級鎖

從JDK 1.6開始,為了減少獲得鎖和釋放鎖帶來的性能消耗,就引入了輕量級鎖。

輕量級鎖的目標是,在無競爭情況下,減少重量級鎖產生的性能消耗(並不是為了代替重量級鎖,實際上就是賭同一時間只有一個線程在占用資源),包括系統調用引起的內核態與用戶態切換、線程阻塞造成的線程切換等。它不像是重量級鎖那樣,需要向操作系統申請互斥量。它的運作機制如下:

在即將開始執行同步代碼塊中的內容時,會首先檢查對象的Mark Word,查看鎖對象是否被其他線程占用,如果沒有任何線程占用,那麼會在當前線程中所處的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於複製並存儲對象目前的Mark Word信息(官方稱為Displaced Mark Word)。

接著,虛擬機將使用CAS操作將對象的Mark Word更新為輕量級鎖狀態(數據結構變為指向Lock Record的指針,指向的是當前的棧幀)

CAS(Compare And Swap)是一種無鎖演算法(我們之前在Springboot階段已經講解過了),它並不會為對象加鎖,而是在執行的時候,看看當前數據的值是不是我們預期的那樣,如果是,那就正常進行替換,如果不是,那麼就替換失敗。比如有兩個線程都需要修改變數i的值,預設為10,現在一個線程要將其修改為20,另一個要修改為30,如果他們都使用CAS演算法,那麼並不會加鎖訪問i,而是直接嘗試修改i的值,但是在修改時,需要確認i是不是10,如果是,表示其他線程還沒對其進行修改,如果不是,那麼說明其他線程已經將其修改,此時不能完成修改任務,修改失敗。

在CPU中,CAS操作使用的是cmpxchg指令,能夠從最底層硬體層面得到效率的提升。

如果CAS操作失敗了的話,那麼說明可能這時有線程已經進入這個同步代碼塊了,這時虛擬機會再次檢查對象的Mark Word,是否指向當前線程的棧幀,如果是,說明不是其他線程,而是當前線程已經有了這個對象的鎖,直接放心大膽進同步代碼塊即可。如果不是,那確實是被其他線程占用了。

這時,輕量級鎖一開始的想法就是錯的(這時有對象在競爭資源,已經賭輸了),所以說只能將鎖膨脹為重量級鎖,按照重量級鎖的操作執行(註意鎖的膨脹是不可逆的)image

所以,輕量級鎖 -> 失敗 -> 自適應自旋鎖 -> 失敗 -> 重量級鎖

解鎖過程同樣採用CAS演算法,如果對象的MarkWord仍然指向線程的鎖記錄,那麼就用CAS操作把對象的MarkWord和複製到棧幀中的Displaced Mark Word進行交換。如果替換失敗,說明其他線程嘗試過獲取該鎖,在釋放鎖的同時,需要喚醒被掛起的線程。


輕量級鎖的加鎖過程:

(1)當線程執行代碼進入同步塊時,若Mark Word為無鎖狀態,虛擬機先在當前線程的棧幀中建立一個名為Lock Record的空間,用於存儲當前對象的Mark Word的拷貝,官方稱之為“Dispalced Mark Word”

(2)複製對象頭中的Mark Word到鎖記錄中。

(3)複製成功後,虛擬機將用CAS操作將對象的Mark Word更新為執行Lock Record的指針,並將Lock Record里的owner指針指向對象的Mark Word。如果更新成功,則執行4,否則執行5。;

(4)如果更新成功,則這個線程擁有了這個鎖,並將鎖標誌設為00,表示處於輕量級鎖狀態

(5)如果更新失敗,虛擬機會檢查對象的Mark Word是否指向當前線程的棧幀,如果是則說明當前線程已經擁有這個鎖,可進入執行同步代碼。否則說明多個線程競爭,輕量級鎖就會膨脹為重量級鎖,Mark Word中存儲重量級鎖(互斥鎖)的指針,後面等待鎖的線程也要進入阻塞狀態。


1.6.4 偏向鎖

偏向鎖相比輕量級鎖更純粹,乾脆就把整個同步都消除掉,不需要再進行CAS操作了。它的出現主要是得益於人們發現某些情況下某個鎖頻繁地被同一個線程獲取,這種情況下,我們可以對輕量級鎖進一步優化:偏向鎖實際上就是專門為單個線程而生的,當某個線程第一次獲得鎖時,如果接下來都沒有其他線程獲取此鎖,那麼持有鎖的線程將不再需要進行同步操作。

通俗的講,偏向鎖就是在運行過程中,對象的鎖偏向某個線程。即在開啟偏向鎖機制的情況下,某個線程獲得鎖,當該線程下次再想要獲得鎖時,不需要再獲得鎖(即忽略synchronized關鍵詞),直接就可以執行同步代碼,比較適合競爭較少的情況。

可以從之前的MarkWord結構中看到,偏向鎖也會通過CAS操作記錄線程的ID,如果一直都是同一個線程獲取此鎖,那麼完全沒有必要在進行額外的CAS操作。當然,如果有其他線程來搶了,那麼偏向鎖會根據當前狀態,決定是否要恢復到未鎖定或是膨脹為輕量級鎖。

如果我們需要使用偏向鎖,可以添加-XX:+UseBiased參數來開啟

所以,最終的鎖等級為:未鎖定 < 偏向鎖 < 輕量級鎖 < 重量級鎖

值得註意的是,如果對象通過調用hashCode()方法計算過對象的一致性哈希值,那麼它是不支持偏向鎖的,會直接進入到輕量級鎖狀態,因為Hash是需要被保存的,而偏向鎖的Mark Word數據結構,無法保存Hash值;如果對象已經是偏向鎖狀態,再去調用hashCode()方法,那麼會直接將鎖升級為重量級鎖,並將哈希值存放在monitor(有預留位置保存)中。

image

偏向鎖的獲取流程:

(1)查看Mark Word中偏向鎖的標識以及鎖標誌位,若是否偏向鎖為1且鎖標誌位為01,則該鎖為可偏向狀態。

(2)若為可偏向狀態,則測試Mark Word中的線程ID是否與當前線程相同,若相同,則直接執行同步代碼,否則進入下一步。

(3)當前線程通過CAS操作競爭鎖,若競爭成功,則將Mark Word中線程ID設置為當前線程ID,然後執行同步代碼,若競爭失敗,進入下一步。

(4)當前線程通過CAS競爭鎖失敗的情況下,說明有競爭。當到達全局安全點時之前獲得偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然後被阻塞在安全點的線程繼續往下執行同步代碼。


偏向鎖的釋放流程:

偏向鎖只有遇到其他線程嘗試競爭偏向鎖時,持有偏向鎖狀態的線程才會釋放鎖,線程不會主動去釋放偏向鎖。偏向鎖的撤銷需要等待全局安全點(即沒有位元組碼正在執行),它會暫停擁有偏向鎖的線程,撤銷後偏向鎖恢復到未鎖定狀態或輕量級鎖狀態。


1.6.5 鎖消除和鎖粗化

鎖消除和鎖粗化都是在運行時的一些優化方案。

  1. 鎖消除是比如我們某段代碼雖然加了鎖,但是在運行時根本不可能出現各個線程之間資源爭奪的情況,這種情況下,完全不需要任何加鎖機制,所以鎖會被消除。
  2. 鎖粗化則是我們代碼中頻繁地出現互斥同步操作,比如在一個迴圈內部加鎖,這樣明顯是非常消耗性能的,所以虛擬機一旦檢測到這種操作,會將整個同步範圍進行擴展。

2. Lock鎖

2.0 Lock鎖和synchronized的區別

  1. Synchronized是內置Java關鍵字;Lock是一個Java類。
  2. Synchronized無法判斷獲取鎖的狀態;Lock可以判斷是否獲取到了鎖。(boolean b = lock.tryLock();)
  3. Synchronized會自動釋放鎖;Lock必須要手動釋放鎖,如果不釋放鎖,死鎖。
  4. Synchronized線程1獲得鎖阻塞時,線程2會一直等待下去;Lock鎖線程1獲得鎖阻塞時,線程2等待足夠長的時間後中斷等待,去做其他的事。
  5. Synchronized可重入鎖,不可以中斷的,非公平;Lock,可重入鎖,可以判斷鎖,非公平(可以自己設置)。
    lock.lockInterruptibly();方法:當兩個線程同時通過該方法想獲取某個鎖時,假若此時線程A獲取到了鎖,而線程B只有在等待,那麼對線程B調用threadB.interrupt()方法能夠中斷線程B的等待過程。
  6. Synchronized適合鎖少量的代碼同步問題;Lock適合鎖大量的同步代碼。

2.1 Lock介面的三個實現類

image

由jdk查詢可知,有三種類


2.2 ReentrantLock類

ReentrantLock鎖的對象是調用lock方法的實例對象

使用創建ReentrantLock對象代替傳統的Synchronized鎖

2.2.1 構造方法——公平鎖and非公平鎖

公平鎖:十分公平,不能插隊。
非公平鎖:十分不公平,可以插隊。(預設非公平鎖)

image

需要更改預設的非公平鎖為公平鎖,需要在創建對象的時候參數設置為true(預設是false)


2.2.2 ReentrantLock類的使用

class X {
    private final ReentrantLock lock = new ReentrantLock();
    // ...
    public void m() {
        lock.lock();  // block until condition holds
        try {
            //業務代碼 ... method body
        } finally {
            lock.unlock();
        }
    }
}

2.3 Condition介面

使用await和signal方法代替傳統的wait和notify方法

image

image


2.3.1 await() signal() 方法基本使用

就是在最原始的多線程synchronized寫法上修改了使用的方法

使用while的緣故還是:可能會出現虛假喚醒

image

image


2.3.2 Condition實現精準通知喚醒

使用ReentrantLock創建的對象lock來創建多個condition對象,每次等待和喚醒都可以指定 如:conditionA.await(); conditionB.signal();

舉例:

public class C {
    public static void main(String[] args) {
        Data3 data3 = new Data3();
        //A執行完,調用B,B執行完,調用C,C執行完,調用A
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data3.printA();
            }
        }, "A").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data3.printB();
            }
        }, "B").start();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                data3.printC();
            }
        }, "C").start();

    }
}

class Data3 {
    private Lock lock = new ReentrantLock();
    Condition conditionA = lock.newCondition();
    Condition conditionB = lock.newCondition();
    Condition conditionC = lock.newCondition();
    private char ch = 'A';

    public void printA() {
        lock.lock();
        try {
            while (ch != 'A') {
                //等待
                conditionA.await();
            }
            System.out.println(Thread.currentThread().getName() + "--->A");
            //喚醒
            ch = 'B';
            conditionB.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void printB() {
        lock.lock();
        try {
            while (ch != 'B') {
                //等待
                conditionB.await();
            }
            System.out.println(Thread.currentThread().getName() + "--->B");
            //喚醒
            ch = 'C';
            conditionC.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }

    }

    public void printC() {
        lock.lock();
        try {
            while (ch != 'C') {
                //等待
                conditionC.await();
            }
            System.out.println(Thread.currentThread().getName() + "--->C");
            //喚醒
            ch = 'A';
            conditionA.signal();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

結果:執行順序變成以此執行

A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C
A--->A
B--->B
C--->C

3. 生產者與消費者

3.1 傳統的synchronized寫法

synchronized+wait+notifyall

public class A {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

class Data {
    private int number = 0;

    public synchronized void increment() throws InterruptedException {
        if (number != 0) {
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "==>" + number);
        //通知其他線程,我+1完畢了
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        if (number == 0) {
            //等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "==>" + number);
        //通知其他線程,我-1完畢了
        this.notifyAll();
    }
}

輸出:發現有問題,出現了負數和大於1的數,這就是虛假喚醒問題(看下麵)

A==>1
C==>0
B==>1
A==>2
B==>3
C==>2
C==>1
C==>0
B==>1
A==>2
B==>3
C==>2
C==>1
C==>0
B==>1
A==>2
B==>3
D==>2
D==>1
D==>0
C==>-1
C==>-2
C==>-3
D==>-4
D==>-5
D==>-6
D==>-7
D==>-8
D==>-9
D==>-10
B==>-9
A==>-8
B==>-7
A==>-6
B==>-5
A==>-4
B==>-3
A==>-2

3.2 Lock寫法

ReentrantLock類 和 Condition介面:lock+await+signal

public class PC {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.decrement();
            }
        },"A").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.increment();
            }
        },"B").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.decrement();
            }
        },"C").start();
        new Thread(()->{
            for (int i = 0; i < 10; i++) {
                data.increment();
            }
        },"D").start();
    }
}
class Data {
    private int number = 0;
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public void increment() {
        lock.lock();
        try {
            if (number > 0) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            number++;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
    public void decrement() {
        lock.lock();
        try {
            if (number <= 0) {
                try {
                    condition.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            number--;
            System.out.println(Thread.currentThread().getName() + "=>" + number);
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

輸出:發現有問題,出現了負數,這就是虛假喚醒問題(看下麵)

B=>1
A=>0
B=>1
A=>0
C=>-1
B=>0
B=>1
A=>0
C=>-1
B=>0
B=>1
A=>0
C=>-1
D=>0
D=>1
B=>2
A=>1
A=>0
C=>-1
D=>0
D=>1
B=>2
A=>1
A=>0
C=>-1
D=>0
D=>1
B=>2
A=>1
A=>0
C=>-1
D=>0
D=>1
B=>2
C=>1
C=>0
D=>1
C=>0
D=>1
C=>0


3.2 虛假喚醒問題

3.2.1 問題描述

白話:一個if條件里有等待語句,兩個消費者都進入這個等待;當生產者生產了1數量的產品並喚醒消費者,此時之前處於等待的兩個消費者會被喚醒,然後進行消費;導致最後消費的產品出現負數,因為產品只有一個,而消費者消費了兩次。

虛假喚醒是一種現象,它只會出現在多線程環境中,指的是在多線程環境下,多個線程等待在同一個條件上,等到條件滿足時,所有等待的線程都被喚醒,但由於多個線程執行的順序不同,後面競爭到鎖的線程在獲得時間片時條件已經不再滿足,線程應該繼續睡眠但是卻繼續往下運行的一種現象。

舉例:

public class A {
    public static void main(String[] args) {
        Data data = new Data();
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "A").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.increment();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "B").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "C").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    data.decrement();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "D").start();
    }
}

class Data {
    private int number = 0;

    public synchronized void increment() throws InterruptedException {
        if (number != 0) {
            //等待
            this.wait();
        }
        number++;
        System.out.println(Thread.currentThread().getName() + "==>" + number);
        //通知其他線程,我+1完畢了
        this.notifyAll();
    }

    public synchronized void decrement() throws InterruptedException {
        if (number == 0) {
            //等待
            this.wait();
        }
        number--;
        System.out.println(Thread.currentThread().getName() + "==>" + number);
        //通知其他線程,我-1完畢了
        this.notifyAll();
    }
}

結果:出現了大於1的情況,以及小於0的情況

A==>1
C==>0
B==>1
A==>2
B==>3
C==>2
C==>1
C==>0
B==>1
A==>2
B==>3
C==>2
C==>1
C==>0
B==>1
A==>2
B==>3
D==>2
D==>1
D==>0
C==>-1
C==>-2
C==>-3
D==>-4
D==>-5
D==>-6
D==>-7
D==>-8
D==>-9
D==>-10
B==>-9
A==>-8
B==>-7
A==>-6
B==>-5
A==>-4
B==>-3
A==>-2

3.2.2 解決方法

image


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 為解決傳統高校科技管理工作中存在的信息失誤率高、傳遞速度緩慢等一系列缺陷,設計開發了基於Java EE的高校科技管理系統,為高校科技管理工作提供了極大的便利。同時還可以用於大創項目,政府類的創新類項目,科研類項目申報管理系統平臺,互聯網+項目申報系統。 ...
  • 雖然現在IDE很強大又很智能,但是平常隨意寫點練手的代碼的時候,直接在命令行中使用vim和java命令更為方便快捷,可以做到無滑鼠純鍵盤的操作。 首先保證將java相關指令添加到了環境變數中; 1.編譯class文件: javac -d ./ Test.java 編譯好的class文件會放置到環境當 ...
  • Java讀取資料庫表(二) application.properties db.driver.name=com.mysql.cj.jdbc.Driver db.url=jdbc:mysql://localhost:3306/easycrud?useUnicode=true&characterEnco ...
  • 在之前的文章中,我們已經對 `bean` 的準備工作進行了講解,包括 `bean` 定義和 `FactoryBean` 判斷等。在這個基礎上,我們可以更加深入地理解 `getBean` 方法的實現邏輯,併在後續的學習中更好地掌握`createBean` 方法的實現細節。 ...
  • 背景說明 最近在做財務相關的系統,對賬單核銷預付款從技術角度來看就是將兩個數組進行合併 對賬單核銷預付款前提條件: 對賬單總金額必須等於未核銷金額 數據示例 對賬單數據 | 單號 | 金額 | | | | | B0001 | 100 | | B0002 | 80 | | B0003 | 120 | ...
  • 軟體:idea 我是先建立了一個空白的項目,自己創建的src包和其下麵的包。 **問題一:**建立包之後發現格式為src.com.tjp.bean 沒辦法建立其他與bean同級的service test utils view 等。只允許繼續建立bean的子包。 解決: 這是因為idea自動會摺疊空白 ...
  • 教程簡介 Python由荷蘭數學和電腦科學研究學會的吉多·範羅蘇姆於1990年代初設計,作為一門叫做ABC語言的替代品。 Python提供了高效的高級數據結構,還能簡單有效地面向對象編程。Python語法和動態類型,以及解釋型語言的本質,使它成為多數平臺上寫腳本和快速開發應用的編程語言, [2] ...
  • 教程簡介 Windows 10入門教程 - 從簡單的步驟瞭解Windows 10,從基本到高級概念,包括概述,安裝,入門,GUI基礎知識,導航,開始菜單,任務欄,平板電腦模式,文件資源管理器,Cortana,通知,快速操作,雲,通用應用程式,多個桌面,用戶管理,安全性,家長控制,應用程式,Web瀏覽 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...