1. 程式人生 > >Java併發程式設計實戰 01併發程式設計的Bug源頭

Java併發程式設計實戰 01併發程式設計的Bug源頭

摘要

編寫正確的併發程式對我來說是一件極其困難的事情,由於知識不足,只知道synchronized這個修飾符進行同步。
本文為學習極客時間:Java併發程式設計實戰 01的總結,文章取圖也是來自於該文章

併發Bug源頭

在計算機系統中,程式的執行速度為:CPU > 記憶體 > I/O裝置 ,為了平衡這三者的速度差異,計算機體系機構、作業系統、編譯程式都進行了優化:

1.CPU增加了快取,以均衡和記憶體的速度差異
2.作業系統增加了程序、執行緒,已分時複用CPU,以均衡 CPU 與 I/O 裝置的速度差異
3.編譯程式優化指令執行順序,使得快取能夠更加合理的利用。

但是這三者導致的問題為:可見性、原子性、有序性

源頭之一:CPU快取導致的可見性問題

一個執行緒對共享變數的修改,另外一個執行緒能夠立即看到,那麼就稱為可見性。
現在多核CPU時代中,每顆CPU都有自己的快取,CPU之間並不會共享快取;

如執行緒A從記憶體讀取變數V到CPU-1,操作完成後儲存在CPU-1快取中,還未寫到記憶體中。
此時執行緒B從記憶體讀取變數V到CPU-2中,而CPU-1快取中的變數V對執行緒B是不可見的
當執行緒A把更新後的變數V寫到記憶體中時,執行緒B才可以從記憶體中讀取到最新變數V的值

上述過程就是執行緒A修改變數V後,對執行緒B不可見,那麼就稱為可見性問題。

源頭之二:執行緒切換帶來的原子性問題

現代的作業系統都是基於執行緒來排程的,現在提到的“任務切換”都是指“執行緒切換”
Java併發程式都是基於多執行緒的,自然也會涉及到任務切換,在高階語言中,一條語句可能就需要多條CPU指令完成,例如在程式碼 count += 1

中,至少需要三條CPU指令。

指令1:把變數 count 從記憶體載入到CPU的暫存器中
指令2:在暫存器中把變數 count + 1
指令3:把變數 count 寫入到記憶體(快取機制導致可能寫入的是CPU快取而不是記憶體)

作業系統做任務切換,可以發生在任何一條CPU指令執行完,所以並不是高階語言中的一條語句,不要被 count += 1 這個操作矇蔽了雙眼。假設count = 0,執行緒A執行完 指令1 後 ,做任務切換到執行緒B執行了 指令1、指令2、指令3後,再做任務切換回執行緒A。我們會發現雖然兩個執行緒都執行了 count += 1 操作。但是得到的結果並不是2,而是1。

如果 count += 1 是一個不可分割的整體,執行緒的切換可以發生在 count += 1 之前或之後,但是不會發生在中間,就像個原子一樣。我們把一個或者多個操作在 CPU 執行的過程中不被中斷的特性稱為原子性

源頭之三:編譯優化帶來的有序性問題

有序性指的是程式按照程式碼的先後順序執行。編譯器為了優化效能,可能會改變程式中的語句執行先後順序。如:a = 1; b = 2;,編譯器可能會優化成:b = 2; a = 1。在這個例子中,編譯器優化了程式的執行先後順序,並不影響結果。但是有時候優化後會導致意想不到的Bug。
在單例模式的雙重檢查建立單例物件中。如下程式碼:

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

問題出現在了new Singletion()這行程式碼,我們以為的執行順序應該是這樣的:

指令1:分配一塊記憶體M
指令2:在記憶體M中例項化Singleton物件
指令3:instance變數指向記憶體地址M

但是實際優化後的執行路徑確實這樣的:

指令1:分配一塊記憶體M
指令2:instance變數指向記憶體地址M
指令3:在記憶體M中例項化Singleton物件

這樣的話看出來什麼問題了嗎?當執行緒A執行完了指令2後,切換到了執行緒B,
執行緒B判斷到 if (instance != null)。直接返回instance,但是此時的instance還是沒有被例項化的啊!所以這時候我們使用instance可能就會觸發空指標異常了。如圖:

總結

在寫併發程式的時候,需要時刻注意可見性、原子性、有序性的問題。在深刻理解這三個問題後,寫起併發程式也會少一點Bug啦~。記住了下面這段話:CPU快取會帶來可見性問題、執行緒切換帶來的原子性問題、編譯優化帶來的有序性問題。

參考文章:極客時間:Java併發程式設計實戰 01 | 可見性、原子性和有序性問題:併發程式設計Bug的源頭

個人部落格網址: https://colablog.cn/

如果我的文章幫助到您,可以關注我的微信公眾號,第一時間分享文章給您

相關推薦

Java併發程式設計實戰 01併發程式設計Bug源頭

摘要 編寫正確的併發程式對我來說是一件極其困難的事情,由於知識不足,只知道synchronized這個修飾符進行同步。 本文為學習極客時間:Java併發程式設計實戰 01的總結,文章取圖也是來自於該文章 併發Bug源頭 在計算機系統中,程式的執行速度為:CPU > 記憶體 > I/O裝置 ,為了平

Java併發程式設計實戰併發基礎構建模組

一、同步容器類 同步容器類包括Vector和Hashtable以及一些功能相似的類,這些同步的封裝器類是由Collections.synchronizedXxx等工廠方法建立的。這些類實現執行緒安全的方式是:將它們的狀態封裝起來(即設為私有,使得外部無法直接訪問,只能通過公

JAVA併發程式設計實戰》避免活躍性危險

文章目錄 死鎖 鎖順序死鎖 動態的鎖順序死鎖 在協作物件之間發生的死鎖 開放呼叫 資源死鎖 死鎖的避免和診斷 支援定時的鎖 使用執行緒轉儲資訊來分析死鎖 其他活躍性危

JAVA併發程式設計實戰》取消和關閉

文章目錄 引言 任務取消 中斷 中斷策略 響應中斷 示例:計時執行 通過Future來實現取消 處理不可中斷的阻塞 採用newTaskFor封裝非標準的取消 停止基於執行緒的服務

JAVA併發程式設計實戰》任務執行

文章目錄 線上程中執行任務 序列執行任務 顯式的為任務建立執行緒 無限制建立執行緒的不足 Executor框架 示例:基於Executor的Web伺服器 執行策略 執行緒池 Exe

JAVA併發程式設計實戰》基礎構建模組

文章目錄 同步容器類 同步容器類的問題 迭代器和ConcurrentModificationException 隱藏迭代器 併發容器 ConcurrentHashMap 額外的原子Map操作

JAVA併發程式設計實戰》物件的組合

文章目錄 設計執行緒安全的類 找出構成物件狀態的所有變數 示例 找出約束狀態變數的不變性條件 例項封閉 java監視器模式 示例:車輛追蹤 執行緒安全性的委託

java併發程式設計實戰】—–執行緒基本概念

轉自 http://cmsblogs.com/?p=1638 共享和可變 要編寫執行緒安全的程式碼,其核心在於對共享的和可變的狀態進行訪問。 “共享”就意味著變數可以被多個執行緒同時訪問。我們知道系統中的資源是有限的,不同的執行緒對資源都是具有著同等的使用權。有限、公平就意味著競爭

讀書筆記(java併發程式設計實戰——CompletionService)

原文請參考微信公眾號(歡迎關注公眾號:coding_song):https://mp.weixin.qq.com/s/R50Eh4kTDtA031i-yMUZAw    Callable&Future Callbale描述的是抽象的計算任務,有明確的起點,並且最終會結束

Java併發程式設計實戰 - 學習筆記

第2章 執行緒安全性 1. 基本概念 什麼是執行緒安全性?可以這樣理解:一個類在多執行緒環境下,無論執行時環境怎樣排程,無論多個執行緒之間的執行順序是什麼,且在主調程式碼中不需要進行任何額外的同步,如果該類都能呈現出預期的、正確的行為,那麼該類就是執行緒安全的。 既然這樣,那麼安

Java併發程式設計實戰:閉鎖CountDownLatch,柵欄CyclicBarrier與訊號量Semaphore

整體上對三個概念進行一個說明: CountDownLatch和CyclicBarrier都能夠實現執行緒之間的等待,只不過它們側重點不同: CountDownLatch是閉鎖,相當於一扇門:在閉鎖達到結束狀態之前,這扇門一直是關閉的,並且沒有任何執行緒能夠通過,當到達結束

Java併發程式設計實戰————Semaphore訊號量的使用淺析

引言 本篇部落格講解《Java併發程式設計實戰》中的同步工具類:訊號量 的使用和理解。 從概念、含義入手,突出重點,配以程式碼例項及講解,並以生活中的案例做類比加強記憶。 什麼是訊號量 Java中的同步工具類訊號量即計數訊號量(Counting Semaphore),是

Java併發程式設計實戰————物件的組合

引言 物件的組合,是《Java Concurrency in Practice》中第四章引入的課題。這並不是一個併發的概念。 為了可以將現有的執行緒安全元件組合為更大規模的元件或程式,而不是每次記憶體訪問都進行分析以確保程式是執行緒安全的。這一章將介紹一些組合模式,這些模式可以更容易的使

Java併發程式設計實戰————售票問題

引言 現有一個需求如下: 有10000張火車票,每張票都有一個編號,同時有10個視窗對外售票,如何確保車票的正常售賣? 程式一:使用List 問題的解決辦法都是從我們最最熟悉的角度思考。程式一,我們使用一個普通的List作為方案。 閱讀以下程式碼,觀察執行結果: publ

Java併發程式設計實戰————ThreadLocal

一、引言 ThreadLocal是Java幫助實現執行緒封閉性的典型手段。 作用:提供執行緒內的區域性變數,這種變數線上程的生命週期內起作用,減少同一個執行緒內多個函式或元件之間一些公共變數的傳遞複雜度。同時也用來維護執行緒中的變數不被其他執行緒干擾。 這個類能使執行緒中的某個值與儲

java併發程式設計實戰》之 物件共享

解決問題: 如何共享和釋出物件,從而使它們能夠安全地由多個執行緒同時訪問 寫多執行緒注意兩點 防止某個執行緒正在使用物件狀態時,而另一個執行緒同時在修改狀態。 確保當一個執行緒修改了物件狀態後,其他執行緒能夠看到狀態變化。(同步的記憶體可見性) 1.可見性

java併發程式設計實戰》之 執行緒安全性

1.執行緒安全性 當多個執行緒訪問某個類時,不管執行時環境採用何種排程方式或者這些執行緒將如何交替執行,並且在主調程式碼中不需要任何額外的同步或協同,這個類都能表現出正確的行為,那麼這個類就是執行緒安全的。 無狀態物件一定是執行緒安全的,何為無狀態,就是類中不包含任何域,也不包含各種其

java併發程式設計實戰》筆記(一) 結構化併發應用程式

下載地址 連結:https://pan.baidu.com/s/1i6FlscH 密碼:m21n 1.任務執行 任務是一組邏輯執行單元,執行緒是使得任務非同步執行的機制 不可取的所謂執行緒開啟的方式: 1.所有任務放在單個執行緒中序列執行 2.每一個任務都開啟一個執行緒,無限

Java併發程式設計實戰————ReentrantLock

可替代synchronized的手動鎖  ReentrantLock是Lock介面的一個實現,可以用於替代synchronized。 使用ReentrantLock可以完成類似synchronize

Java併發程式設計實戰》筆記3——執行緒池的使用

1、執行緒飢餓死鎖 線上程池中,如果任務依賴於其他任務,那麼可能發生死鎖。在單執行緒的Executor中,如果一個任務將另一個任務提交到同一個Executor,並且等待這個被提交任務的結果,那麼通常會引發死鎖。 如下面程式碼所示: public class Thread