1. 程式人生 > >【從入門到放棄-Java】併發程式設計-執行緒安全

【從入門到放棄-Java】併發程式設計-執行緒安全

概述

併發程式設計,即多條執行緒在同一時間段內“同時”執行。

在多處理器系統已經普及的今天,多執行緒能發揮出其優勢,如:一個8核cpu的伺服器,如果只使用單執行緒的話,將有7個處理器被閒置,只能發揮出伺服器八分之一的能力(忽略其它資源佔用情況)。
同時,使用多執行緒,可以簡化我們對複雜任務的處理邏輯,降低業務模型的複雜程度。

因此併發程式設計對於提高伺服器的資源利用率、提高系統吞吐量、降低編碼難度等方面起著至關重要的作用。

以上是併發程式設計的優點,但是它同樣引入了一個很重要的問題:執行緒安全。

什麼是執行緒安全問題

執行緒在併發執行時,因為cpu的排程等原因,執行緒會交替執行。如下圖例子所示

public class SelfIncremental {
    private static int count;

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                count++;
                System.out.println(count);

            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i< 10000; i++) {
                count++;
                System.out.println(count);

            }
        });

        thread1.start();
        thread2.start();
    }
}

執行完畢後count的值並不是每次都能等於20000,會出現小於20000的情況,原因是thread1和thread2可能會交替執行。

如圖所示:

  • t1時刻: thread1 讀取到count=100
  • t2時刻: thread2 讀取到count=100
  • t3時刻: thread1 對count+1
  • t4時刻: thread2 對count+1
  • t5時刻: thread1 將101寫入count
  • t5時刻: thread2 將101寫入count

因為count++ 不是一個原子操作,實際上會執行三步:

  • 1、獲取count的值
  • 2、將count加1
  • 3、將計算結果寫入count

因此在併發執行時,兩個執行緒同時讀,可能會讀取到相同的值,對相同的值加一,導致結果不符合預期,這種情況就是執行緒不安全。

執行緒安全:當多個執行緒訪問某個類時,不管執行時環境採用何種排程方式或者這些執行緒將如何交替執行,並且呼叫時不需要採用額外的同步操作,這個類都能表現出正確的行為,那麼就稱這個類是執行緒安全的。

引發原因

引發執行緒安全性問題的原因主要是共享記憶體可以被多個執行緒讀寫,因為讀取和修改時機存在不確定性,導致有執行緒讀到了過期資料,並在髒資料的基礎上處理後寫回共享記憶體,產生了錯誤的結果。

竟態條件

在併發程式設計中,因為不恰當的執行時序而出現不正確的結果的情況被稱為竟態條件。

常見的靜態條件型別:

  • 先檢查後執行:首先觀察到某個條件為真。根據這個觀察結果採用相應的動作,但實際上在你觀察到這個結果和採用相應動作之間,觀察的結果可能發生改變變得無效,導致後續的所有操作都變得不可預期。(比如延遲初始化)
  • 讀取-修改-寫入:基於物件之前的狀態來定義物件狀態的轉換。但在讀取到結果和修改之間,物件可能已被更改。這樣就會基於錯誤的資料修改得出錯誤的結果並被寫入。(比如遞增操作)

釋出與逸出

釋出:使物件能夠在當前作用域之外的程式碼中使用。如將該物件的引用儲存到其它程式碼可以訪問的地方、在一個非私有的方法中返回該引用,將引用傳遞到其它類的方法中。如:

public static Student student;

public void init() { 
    student = new Student;
}

這裡 student物件就被髮布了。

逸出:當不該被髮布的物件被髮布了,就稱為逸出。如

private String name = "xxx";

public String getString() {
    return name;
}

這裡name原為private型別但是卻被getString方法釋出了,就可以被視為逸出。

如何避免

執行緒封閉

執行緒封閉的物件只能由一個執行緒擁有,物件被封閉在該執行緒中,並且只有這個物件能修改。

執行緒封閉即不共享資料,僅在單執行緒內訪問資料,這是實現執行緒安全最簡單的方式之一。
實現執行緒封閉可以通過:

  • Ad-hoc執行緒封閉:即維護執行緒封閉性的職責完全由成熟實現承擔。
  • 棧封閉:通過區域性變數才能訪問物件,該區域性變數被儲存在執行執行緒的棧中,其他執行緒無法訪問。
  • ThreadLocal類:將共享的全域性變數轉換為ThreadLocal物件,當執行緒終止後,這些值會被垃圾回收。

只讀共享

在沒有額外同步的情況下,共享的物件可以由多個執行緒併發訪問,但是任何執行緒都不能修改。共享的物件包括不可變物件和事實不可變物件。

不可變物件:如果某個物件在被建立後就不能修改,那麼這個物件就是不可變物件。不可變物件一定是執行緒安全的。

執行緒安全共享

執行緒安全的物件在其內部實現同步,因此多執行緒可以通過物件的公有介面來進行訪問而不需要自己做同步。

保護物件

被保護的物件只能通過持有特定的鎖來訪問。即通過加鎖機制,確保物件的可見性及原子性。

  • 內建鎖:即通過synchronized關鍵字同步程式碼塊。執行緒在進入同步程式碼塊之前會自動獲得鎖,並在退出同步程式碼塊時自動釋放鎖。內建鎖是一種互斥鎖。
  • 重入鎖:當執行緒檢視獲取一個已經持有的鎖時,就會給鎖的計數器加一,釋放鎖時計數器會減一。當計數器為0時,釋放鎖
  • volatile:訪問volatile變數時,不會加鎖,也不會阻塞執行緒執行。他只確保變數的可見性,是一種比synchronized更輕量級的同步機制。

總結

本文主要是記錄了學習《Java併發程式設計實站》前幾章中,併發程式設計相關的一些概念。簡單介紹了執行緒安全、鎖機制等,接下來 我們會深入JUC原始碼,來深刻學習併發程式設計相關知識。

備註:本文主要源自對《Java併發程式設計實戰》的學習筆記。

作者:aloof_ 

原文連結

本文為雲棲社群原創內容,未經