【從入門到放棄-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++ 不是一個原子操做,實際上會執行三步:spa

  • 一、獲取count的值
  • 二、將count加1
  • 三、將計算結果寫入count

所以在併發執行時,兩個線程同時讀,可能會讀取到相同的值,對相同的值加一,致使結果不符合預期,這種狀況就是線程不安全。線程

線程安全:當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且調用時不須要採用額外的同步操做,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。code

引起緣由

引起線程安全性問題的緣由主要是共享內存能夠被多個線程讀寫,由於讀取和修改時機存在不肯定性,致使有線程讀到了過時數據,並在髒數據的基礎上處理後寫回共享內存,產生了錯誤的結果。

竟態條件

在併發編程中,由於不恰當的執行時序而出現不正確的結果的狀況被稱爲竟態條件。

常見的靜態條件類型:

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

發佈與逸出

發佈:使對象可以在當前做用域以外的代碼中使用。如將該對象的引用保存到其它代碼能夠訪問的地方、在一個非私有的方法中返回該引用,將引用傳遞到其它類的方法中。如:

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_

閱讀原文

本文爲雲棲社區原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索