天道不一定酬所有勤
但是,天道只酬勤

英雄联盟歌曲:深入理解Java并發編程(一):到底什么是線程安全

英雄联盟充值记录 www.frhrb.icu 本文是搞點事情!死磕Java并發編程。中的一篇試讀文章,更多文章,請參見:深入理解Java并發編程

什么是線程安全

線程安全,維基百科中的解釋是:

線程安全是編程中的術語,指某個函數、函數庫在并發環境中被調用時,能夠正確地處理多個線程之間的共享變量,使程序功能正確完成。

我們把這個定義拆解一下,我們需要弄清楚這么幾點: 1、并發 2、多線程 3、共享變量

并發

提到線程安全,必須要提及的一個詞那就是并發,如果沒有并發的話,那么也就不存在線程安全問題了。

什么是并發

并發(Concurrent),在操作系統中,是指一個時間段中有幾個程序都處于已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機上運行。

那么,操作系統視如何實現這種并發的呢?

現在我們用到操作系統,無論是Windows、Linux還是MacOS等其實都是多用戶多任務分時操作系統。使用這些操作系統的用戶是可以“同時”干多件事的。

但是實際上,對于單CPU的計算機來說,在CPU中,同一時間是只能干一件事兒的。為了看起來像是“同時干多件事”,分時操作系統是把CPU的時間劃分成長短基本相同的時間區間,即”時間片”,通過操作系統的管理,把這些時間片依次輪流地分配給各個用戶使用。

如果某個作業在時間片結束之前,整個任務還沒有完成,那么該作業就被暫停下來,放棄CPU,等待下一輪循環再繼續做.此時CPU又分配給另一個作業去使用。

由于計算機的處理速度很快,只要時間片的間隔取得適當,那么一個用戶作業從用完分配給它的一個時間片到獲得下一個CPU時間片,中間有所”停頓”,但用戶察覺不出來,好像整個系統全由它”獨占”似的。

所以,在單CPU的計算機中,我們看起來“同時干多件事”,其實是通過CPU時間片技術,并發完成的。

提到并發,還有另外一個詞容易和他混淆,那就是并行。

并發與并行之間的關系

并行(Parallel),當系統有一個以上CPU時,當一個CPU執行一個進程時,另一個CPU可以執行另一個進程,兩個進程互不搶占CPU資源,可以同時進行,這種方式我們稱之為并行(Parallel)。

Erlang 之父 Joe Armstrong 用一張比較形象的圖解釋了并發與并行的區別:

concurrent vs parallel

并發是兩個隊伍交替使用一臺咖啡機。并行是兩個隊伍同時使用兩臺咖啡機。

映射到計算機系統中,上圖中的咖啡機就是CPU,兩個隊伍指的就是兩個進程。

多線程

進程和線程

理解了并發和并行之間的關系和區別后,我們再回到前面介紹的多任務分時操作系統,看看CPU是如何進行進程調度的。

為了看起來像是“同時干多件事”,分時操作系統是把CPU的時間劃分成長短基本相同的”時間片”,通過操作系統的管理,把這些時間片依次輪流地分配給各個用戶的各個任務使用。

在多任務處理系統中,CPU需要處理所有程序的操作,當用戶來回切換它們時,需要記錄這些程序執行到哪里。在操作系統中,CPU切換到另一個進程需要保存當前進程的狀態并恢復另一個進程的狀態:當前運行任務轉為就緒(或者掛起、刪除)狀態,另一個被選定的就緒任務成為當前任務。上下文切換就是這樣一個過程,他允許CPU記錄并恢復各種正在運行程序的狀態,使它能夠完成切換操作。

在上下文切換過程中,CPU會停止處理當前運行的程序,并保存當前程序運行的具體位置以便之后繼續運行。從這個角度來看,上下文切換有點像我們同時閱讀幾本書,在來回切換書本的同時我們需要記住每本書當前讀到的頁碼。在程序中,上下文切換過程中的“頁碼”信息是保存在進程控制塊(PCB)中的。PCB還經常被稱作“切換幀”(switchframe)?!耙陳搿斃畔⒒嵋恢北4嫻紺PU的內存中,直到他們被再次使用。

對于操作系統來說,一個任務就是一個進程(Process),比如打開一個瀏覽器就是啟動一個瀏覽器進程,打開一個記事本就啟動了一個記事本進程,打開兩個記事本就啟動了兩個記事本進程,打開一個Word就啟動了一個Word進程。

而在多個進程之間切換的時候,需要進行上下文切換。但是上下文切換勢必會耗費一些資源。于是人們考慮,能不能在一個進程中增加一些“子任務”,這樣減少上下文切換的成本。比如我們使用Word的時候,它可以同時進行打字、拼寫檢查、字數統計等,這些子任務之間共用同一個進程資源,但是他們之間的切換不需要進行上下文切換。

在一個進程內部,要同時干多件事,就需要同時運行多個“子任務”,我們把進程內的這些“子任務”稱為線程(Thread)。

隨著時間的慢慢發展,人們進一步的切分了進程和線程之間的職責。把進程當做資源分配的基本單元,把線程當做執行的基本單元,同一個進程的多個線程之間共享資源

拿我們比較熟悉的Java語言來說,Java程序是運行在JVM上面的,每一個JVM其實就是一個進程。所有的資源分配都是基于JVM進程來的。而在這個JVM進程中,又可以創建出很多線程,多個線程之間共享JVM資源,并且多個線程可以并發執行。

共享變量

所謂共享變量,指的是多個線程都可以操作的變量。

前面我們提到過,進程視分配資源的基本單位,線程是執行的基本單位。所以,多個線程之間是可以共享一部分進程中的數據的。在JVM中,Java堆和方法區的區域是多個線程共享的數據區域。也就是說,多個線程可以操作保存在堆或者方法區中的同一個數據。那么,換句話說,保存在堆和方法區中的變量就是Java中的共享變量。

那么,Java中哪些變量是存放在堆中,哪些變量是存放在方法區中,又有哪些變量是存放在棧中的呢?

類變量、成員變量和局部變量

Java中共有三種變量,分別是類變量、成員變量和局部變量。他們分別存放在JVM的方法區、堆內存和棧內存中。

/**
 * @author Hollis
 */
public class Variables {

    /**
     * 類變量
     */
    private static int a;

    /**
     * 成員變量
     */
    private int b;

    /**
     * 局部變量
     * @param c
     */
    public void test(int c){
        int d;
    }
}

上面定義的三個變量中,變量a就是類變量,變量b就是成員變量,而變量c和d是局部變量。

所以,變量a和b是共享變量,變量c和d是非共享變量。所以如果遇到多線程場景,對于變量a和b的操作是需要考慮線程安全的,而對于線程c和d的操作是不需要考慮線程安全的。

小結

在了解了一些基礎知識以后,我們再來回過頭看看線程安全的定義:

線程安全是編程中的術語,指某個函數、函數庫在并發環境中被調用時,能夠正確地處理多個線程之間的共享變量,使程序功能正確完成。

現在我們知道了什么是并發環境,什么是多個線程以及什么是共享變量。那么只要我們在編寫多線程的代碼的時候注意一下,保證程序功能可以正確的執行就行了。

那么問題來了,定義中說線程安全能夠正確地處理多個線程之間的共享變量,使程序功能正確完成。

多線程場景中存在哪些問題會導致無法正確的處理共享變量? 多線程場景中存在哪些問題會導致程序無法正確完成? 如何解決多線程場景中影響『正確』的這些問題? 解決這些問題的各個手段的實現原理又是什么?

以上問題會在后續文章中介紹。

(全文完) 歡迎關注『Java之道』微信公眾號
贊(48)
如未加特殊說明,此網站文章均為原創,轉載必須注明出處。英雄联盟充值记录 » 深入理解Java并發編程(一):到底什么是線程安全
分享到: 更多 (0)

評論 2

  • 昵稱 (必填)
  • 郵箱 (必填)
  • 網址
  1. #1

    你好,探討一下,在小節“類變量、成員變量和局部變量”中,寫道“變量a和b是共享變量,變量c和d是非共享變量”。
    但如果c和d是非基本類型(對象),且聲明為final,即可在此方法域內聲明的新線程進行共享,此時c和d也是線程共享的變量了。所以我理解局部變量,不一定是線程安全的吧?

    hh6plus7個月前 (02-23)回復
  2. #2

    說的好

    測試3周前 (08-29)回復

HollisChuang's Blog

聯系我關于我