聊天室軟體原始碼的併發高,可能是什麼問題引發的?

雲豹科技程式設計師發表於2021-11-22

CPU為了對聊天室軟體原始碼進行優化,會對聊天室軟體原始碼的指令進行重排序,此時聊天室軟體原始碼的執行順序和程式碼的編寫順序不一定一致,這就可能會引起有序性問題。

有序性

有序性是指:按照聊天室軟體原始碼的既定順序執行。

說的通俗一點,就是聊天室軟體原始碼會按照指定的順序執行,例如,按照程式編寫的順序執行,先執行第一行程式碼,再執行第二行程式碼,然後是第三行程式碼,以此類推。如下圖所示。
在這裡插入圖片描述

指令重排序

編譯器或者直譯器為了優化聊天室軟體原始碼的執行效能,有時會改變程式的執行順序。但是,編譯器或者直譯器對程式的執行順序進行修改,可能會導致意想不到的問題!

在單執行緒下,指令重排序可以保證最終執行的結果與聊天室軟體原始碼順序執行的結果一致,但是在多執行緒下就會存在問題。

如果發生了指令重排序,則程式可能先執行第一行程式碼,再執行第三行程式碼,然後執行第二行程式碼,如下所示。

在這裡插入圖片描述

例如下面的三行程式碼。

int x = 1; int y = 2;int z = x + y;

CPU發生指令重排序時,能夠保證x=1和y = 2這兩行程式碼在z = x + y這行程式碼的上面,而x = 1和 y = 2的順序就不一定了。在單執行緒下不會出現問題,但是在多執行緒下就不一定了。

有序性問題

CPU為了對聊天室軟體原始碼進行優化,會對聊天室軟體原始碼的指令進行重排序,此時程式的執行順序和程式碼的編寫順序不一定一致,這就可能會引起有序性問題。

在Java程式中,一個經典的案例就是使用雙重檢查機制來建立單例物件。例如,在下面的程式碼中,在getInstance()方法中獲取物件例項時,首先判斷instance物件是否為空,如果為空,則鎖定當前類的class物件,並再次檢查instance是否為空,如果instance物件仍然為空,則為instance物件建立一個例項。

package io.binghe.concurrent.lab01;/**
 * @author binghe
 * @version 1.0.0
 * @description 測試單例
 */public class SingleInstance {
    private static SingleInstance instance;
    public static SingleInstance getInstance(){
        if(instance == null){
            synchronized (SingleInstance.class){
                if(instance == null){
                    instance = new SingleInstance();
                }
            }
        }
        return instance;
    }}

如果編譯器或者直譯器不會對上面的程式進行優化,整個程式碼的執行過程如下所示。

在這裡插入圖片描述

注意:為了讓大家更加明確流程圖的執行順序,我在上圖中標註了數字,以明確執行緒A和執行緒B執行的順序。

假設此時有執行緒A和執行緒B兩個執行緒同時呼叫getInstance()方法來獲取物件例項,兩個執行緒會同時發現instance物件為空,此時會同時對SingleInstance.class加鎖,而JVM會保證只有一個執行緒獲取到鎖,這裡我們假設是執行緒A獲取到鎖。則執行緒B由於未獲取到鎖而進行等待。接下來,執行緒A再次判斷instance物件為空,從而建立instance物件的例項,最後釋放鎖。此時,執行緒B被喚醒,執行緒B再次嘗試獲取鎖,獲取鎖成功後,執行緒B檢查此時的instance物件已經不再為空,執行緒B不再建立instance物件。

上面的一切看起來很完美,但是這一切的前提是編譯器或者直譯器沒有對聊天室軟體原始碼進行優化,也就是說CPU沒有對程式進行重排序。而實際上,這一切都只是我們自己覺得是這樣的。

在真正高併發環境下執行上面的程式碼獲取instance物件時,建立物件的new操作會因為編譯器或者直譯器對聊天室軟體原始碼的優化而出現問題。也就是說,問題的根源在於如下一行程式碼。

instance = new SingleInstance();

對於上面的一行程式碼來說,會有3個CPU指令與其對應。

1.分配記憶體空間。

2.初始化物件。

3.將instance引用指向記憶體空間。

正常執行的CPU指令順序為1—>2—>3,CPU對程式進行重排序後的執行順序可能為1—>3—>2。此時,就會出現問題。

當CPU對聊天室軟體原始碼進行重排序後的執行順序為1—>3—>2時,我們將執行緒A和執行緒B呼叫getInstance()方法獲取物件例項的兩種步驟總結如下所示。

【第一種步驟】

(1)假設執行緒A和執行緒B同時進入第一個if條件判斷。

(2)假設執行緒A首先獲取到synchronized鎖,進入synchronized程式碼塊,此時因為instance物件為null,所以,此時執行instance = new SingleInstance()語句。

(3)在執行instance = new SingleInstance()語句時,執行緒A會在JVM中開闢一塊空白的記憶體空間。

(4)執行緒A將instance引用指向空白的記憶體空間,在沒有進行物件初始化的時候,發生了執行緒切換,執行緒A釋放synchronized鎖,CPU切換到執行緒B上。

(5)執行緒B進入synchronized程式碼塊,讀取到執行緒A返回的instance物件,此時這個instance不為null,但是並未進行物件的初始化操作,是一個空物件。此時,執行緒B如果使用instance,就可能出現問題!!!

【第二種步驟】

(1)執行緒A先進入if條件判斷,

(2)執行緒A獲取synchronized鎖,並進行第二次if條件判斷,此時的instance為null,執行instance = new SingleInstance()語句。

(3)執行緒A在JVM中開闢一塊空白的記憶體空間。

(4)執行緒A將instance引用指向空白的記憶體空間,在沒有進行物件初始化的時候,發生了執行緒切換,CPU切換到執行緒B上。

(5)執行緒B進行第一次if判斷,發現instance物件不為null,但是此時的instance物件並未進行初始化操作,是一個空物件。如果執行緒B直接使用這個instance物件,就可能出現問題!!!

在第二種步驟中,即使發生執行緒切換時,執行緒A沒有釋放鎖,則執行緒B進行第一次if判斷時,發現instance已經不為null,直接返回instance,而無需嘗試獲取synchronized鎖。

我們可以將上述過程簡化成下圖所示。

在這裡插入圖片描述

總結

導致聊天室軟體原始碼併發程式設計產生各種詭異問題的根源有三個:快取導致的可見性問題、執行緒切換導致的原子性問題和編譯優化帶來的有序性問題。我們從根源上理解了這三個問題產生的原因,能夠幫助我們更好的編寫聊天室軟體原始碼。

本文轉載自網路,轉載僅為分享乾貨知識,如有侵權歡迎聯絡雲豹科技進行刪除處理
原文連結:https://my.oschina.net/u/4526289/blog/5319119


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69996194/viewspace-2843497/,如需轉載,請註明出處,否則將追究法律責任。

相關文章