多線程併發編程分享


-     基礎概念    -
javascript

1. 進程與線程java

  • 如今的操做系統都是多任務操做系統,容許多個進程在同一個CPU上運行。

  • 每一個進程都有獨立的代碼和數據空間,稱爲進程上下文

  • CPU從一個進程切換到另外一個進程所作的動做被成爲上下文切換,經過頻繁的上下文切換來讓這些進程看起來像是在同時運行同樣

  • 進程的運行須要較多的資源,操做系統可以同時運行的進城數量有限,而且進程間的切換和通訊也存在較大開銷。

  • 爲了能並並行的執行更多的任務,提高系統效率,才引入了線程概念。線程是CPU調度的最小單位,是進程的一部分,只能由進程建立,共享進程的資源和代碼
    c++

  • 以Java進程爲例,它至少有一個主線程(main方法所在的線程),經過主線程能夠建立更多的用戶線程或者守護線程,線程能夠有本身獨享的數據空間,同時線程間也共享進程的數據空間
    程序員

2.併發與並行web

  • 並行的概念:若是一個CPU有多個核心,並容許多個線程在不一樣的核心上同時執行,稱爲「多核並行」,這裏強調的是同時執行。算法

  • 併發的概念:好比在單個CPU上,經過必定的「調度算法」,把CPU運行時間劃分紅若干個時間片,再將時間片分配給各個線程執行,在一個時間片的線程代碼運行時,其它線程處於掛起等待的狀態,只不過CPU在作這些事情的時候很是地快速,所以讓多個任務看起來「像是」同時在執行,本質上同一時刻,CPU只能執行一個任務。typescript


-     線程狀態&狀態間轉換    -數據庫

1.線程狀態編程

  • 新建NEW:線程被新建立時的狀態,在堆區中被分配了內存緩存

  • 就緒RUNNABLE&READY:線程調用了它的start()方法,該線程進入就緒狀態,虛擬機會爲其建立方法調用棧和程序計數器,等待得到CPU的使用權

  • 運行RUNNING:線程獲取了CPU的使用權,執行程序代碼,只有就緒狀態纔有機會轉到運行狀態

  • 阻塞BLOCKED:位於對象鎖池的狀態,線程爲了等待某個對象的鎖,而暫時放棄CPU的使用權,且不參與CPU使用權的競爭。直到得到鎖,該線程才從新回到就緒狀態,從新參與CPU競爭,這涉及到「線程同步」

  • 等待WAITING:位於對象等待池的狀態,線程放棄CPU也放棄了鎖,這涉及到「線程通訊」

  • 計時等待TIME_WAITING:超時等待的狀態,它會放棄CPU可是不會放棄對象鎖

  • 終止TERMINATED&DEAD:代碼執行完畢、執行過程當中出現異常、受到外界干預而中斷執行,這些狀況均可以使線程終止

2.線程狀態間轉換圖

  • Thread3持有對象鎖,Thread1,2,4進入等待獲取鎖時的狀態是BLOCKED

  • Dopey線程調用sleepy.join()後,dopey線程處於WAITING狀態,會等待sleepy線程結束,sleepy線程因爲調用了sleep()方法,處於TIMED_WAITING狀態

  • 若是dopey線程調用sleepy.join(…)方法,dopey會進入TIMED_WAITING狀態,它會在超時時間內等待sleepy線程結束,若是超時了sleepy線程還未結束,dopey不會繼續等待,它會繼續運行

  • 調用了wait(…)方法以後會進入TIMED_WAITING狀態,超時等待


-     java對於線程的編程支持間轉換    -

1.Thread類經常使用方法

  • t.start() 啓動線程t,線程狀態有NEW變爲RUNNABLE,開始參與CPU競爭

  • t.checkAccess() 檢查當前線程是否有權限訪問線程t

  • t.isInterrupted() 檢查線程t是否要求被中斷

  • t.setPriority() 設置線程優先級:1-10,值越大,獲得執行的機會越高,通常比較少用

  • t.setDaemon(true) 設置線程爲後臺線程,代碼演示1

  • t.isAlive() 判斷線程t是否存活

  • t.join()/t.join(1000L) 當前線程掛起,等待t線程結束或者超時,代碼演示2

  • Thread.yield() 讓出CPU,若是有鎖,不會讓出鎖。轉爲RUNNABLE狀態,從新參與CPU的競爭

  • Thread.sleep(1000L) 讓出CPU,不讓鎖,睡眠1秒鐘以後轉爲RUNNABLE狀態,從新參與CPU競爭

  • Thread.currentThread() 獲取當前線程實例

  • Thread.interrupt() 給當前線程發送中斷信號

2.wait和sleep的差別和共同點,代碼演示3
  • wait方法是Object類的方法,是線程間通訊的重要手段之一,它必須在synchronized同步塊中使用;sleep方法是Thread類的靜態方法,能夠隨時使用

  • wait方法會釋放synchronized鎖,而sleep方法則不會

  • 由wait方法造成的阻塞,能夠經過針對同一個synchronized鎖做用域調用notify/notifyAll來喚醒,而sleep方法沒法被喚醒,只能定時醒來或被interrupt方法中斷

  • 共同點1:二者均可以讓程序阻塞指定的毫秒數

  • 共同點2:均可以經過interrupt方法打斷

3.sleep與yield,代碼示例4

  • 線程調用sleep方法後,會進入TIMED_WAITING狀態,在醒來以後會進入RUNNABLE狀態,而調用yield方法後,則是直接進入RUNNABLE狀態再次競爭CPU

  • 線程調用sleep方法後,其餘線程不管優先級高低,都有機會運行;而執行yield方法後,只會給那些相同或者更高優先級的線程運行的機會

  • sleep方法須要聲明InterruptedException,yield方法沒有聲明任何異常。


-    線程池    -

線程的建立和銷燬會消耗資源,在大量併發的狀況下,頻繁地建立和銷燬線程會嚴重下降系統的性能。所以,一般須要預先建立多個線程,並集中管理起來,造成一個線程池,用的時候拿來用,用完放回去。
  • 經常使用線程池:FixedThreadPool,CachedThreadPool,ScheduledThreadPool,代碼演示5

  • 主要關注的功能:shutDown方法;shutDownNow方法;execute(Runnable)向線程池提交一個任務,不須要返回結果;submit(task)向線程池提交一個任務,且須要返回結果,這裏涉及到Future編程模型,代碼演示6


-    線程安全    -

  • 怎麼理解線程安全?線程安全,本質上是指「共享資源」在多線程環境下的安全,不會由於多個線程併發的修改而出現數據破壞,丟失更新,死鎖等問題。

  • 爲何會出現線程不安全?我的的一些思考,讀操做是線程安全的,它不會改變值;寫操做也是線程安全的,這裏的寫操做是指對於內存或者硬盤上的值進行更改的那個動做,這個動做自己是具備原子性的。有不少人說,共享資源不安全是由於「併發的寫」,這裏我想說「寫」這個動做自己不會破壞資源的安全性。這裏要結合操做系統的工做特色來講明一下這個問題。

  • 各個線程從主內存中讀取數據到工做內存中,而後在工做內存中根據代碼指令對數據進行運算加工,最後寫回主內存中。

  • 引伸出線程安全要解決的三個問題

    • 原子性,某個線程對共享資源的一系列操做,不可被其餘線程中斷和干擾。

    • 可見性,當多個線程併發的讀寫某個共享資源時,每一個線程老是能讀取到該共享資源的最新數據。

    舉例://線程1執行的代碼int i = 0;i = 10;//線程2執行的代碼j = i;
      • 倘若執行線程1的是CPU1,執行線程2的是CPU2。由上面的分析可知,當線程1執行 i =10這句時,會先把i的初始值加載到CPU1的高速緩存中,而後賦值爲10,那麼在CPU1的高速緩存當中i的值變爲10了,卻沒有當即寫入到主存當中。

      • 此時線程2執行 j = i,它會先去主存讀取i的值並加載到CPU2的緩存當中,注意此時內存當中i的值仍是0,那麼就會使得j的值爲0,而不是10.這就是可見性問題,線程1對變量i修改了以後,線程2沒有當即看到線程1修改的值。

    • 有序性,單個線程內的操做必須是有序的。

      解釋一下什麼是指令重排序,通常來講,處理器爲了提升程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行前後順序同代碼中的順序一致,可是它會保證程序最終執行結果和代碼順序執行的結果是一致的。

    int a = 10; //語句1int r = 2//語句2a = a + 3; //語句3r = a*a; //語句4
      • 好比上面的代碼中,語句1和語句2誰先執行對最終的程序結果並無影響,那麼就有可能在執行過程當中,語句2先執行而語句1後執行。

      • 可是執行順序不多是 語句2—語句1—語句4—語句3,由於這樣會改變最終結果。

    雖然重排序不會影響單個線程內程序執行的結果,可是多線程呢?

    //線程1:context = loadContext(); //語句1inited = true; //語句2//線程2:while(!inited ){sleep()}doSomethingwithconfig(context);

    上面這段代碼在單線程看來,語句1和語句2沒有必然聯繫,那若是這時發生了指令重排序,語句2先執行,那這時線程2會認爲初始化已經完成,直接跳出循環,但其實線程1的初始化不必定完成了,這樣就會產生程序錯誤。



-    線程同步    -

線程同步指的是線程之間的協調和配合,是多線程環境下解決線程安全和效率的關鍵。主要包括四種經常使用方式來實現
  • 臨界區,表示同一時刻只容許一個線程執行的「代碼塊」被稱爲臨界區,要想進入臨界區則必須持有鎖

  • 互斥量,即咱們理解的鎖,只有擁有鎖的線程才被容許訪問共享資源

  • 自旋鎖:與互斥量相似,它不是經過休眠使進程阻塞,而是在獲取鎖以前一直處於忙等(自旋)阻塞狀態。用在如下狀況:鎖持有的時間短,並且線程並不但願在從新調度上花太多的成本,"原地打轉"。

  • 信號量,容許有限數量的線程在同一時刻訪問統一資源,當訪問線程達到上限時,其餘試圖訪問的線程將被阻塞

  • 事件,經過發送「通知」的方式來實現線程的同步


Java中對實現線程安全與線程同步提供哪些主要的能力

  • Volatile,被volatile修飾以後就具有了兩層語義:

    • 保證了不一樣線程對這個變量進行操做時的可見性,即一個線程修改了某個變量的值,這新值對其餘線程來講是當即可見的。

    • 禁止進行指令重排序,即對一個變量的寫操做先行發生於後面對這個變量的讀操做

看下面一段代碼://線程1boolean stop = false;while(!stop){doSomething();}//線程2stop = true;

這段代碼是一種典型的多線程寫法,線程1根據布爾值stop的值來決定是否跳出循環;而線程2則會決定是否將布爾值stop置爲true。若是線程2改變了stop的值,可是卻遲遲沒有寫入到主存中,那線程1其實還覺得stop=false,會一直循環下去。可是用volatile修飾以後就變得不同了:

  • 使用volatile關鍵字會強制將修改的值當即寫入主存;

  • 使用volatile關鍵字的話,當線程2進行修改時,會致使線程1的工做內存中緩存變量stop的緩存行無效(反映到硬件層的話,就是CPU的L1或者L2緩存中對應的緩存行無效);

  • 因爲線程1的工做內存中緩存變量stop的緩存行無效,因此線程1再次讀取變量stop的值時會去主存讀取。

基於上面的描述,咱們可能會問volatile這樣的能力是否是能保證原子性了呢?答案是否認的,代碼示例7

具體緣由我的理解以下:

java語言的指令集是一門基於棧的指令集架構。也就是說它的數值計算是基於棧的。好比計算inc++,翻譯成字節碼就會變成:

0: iconst_11: istore_12: iinc 1, 10:的做用是把1放到棧頂1:的做用是把剛纔放到棧頂的1存入棧幀的局部變量表2:的做用是對指令後面的1 ,1相加

由第0步能夠看到,當指令序列將操做數存入棧頂以後就再也不會從緩存中取數據了,那麼緩存行無效也就沒有什麼影響了。

  • Synchronized,用於標記一個方法或方法塊,經過給對象上「鎖」的方式,將本身的做用域變成一個臨界區,只有得到鎖的線程才能夠進入臨界區。每一個java對象在內存中都有一個對應的監視器monitor,它用來存儲「鎖」標記,記錄哪個線程擁有這個對象的「鎖」,又有哪些線程在競爭這個「鎖」。鎖,本質上是併發轉串行,所以它自然就能解決原子性,可見性,有序性問題。代碼示例8

  • CAS與atomic包

    Synchronized是一種獨佔鎖,悲觀鎖,等待鎖的線程處於BLOCKED狀態,影響性能;鎖的競爭會致使頻繁的上下文切換和調度延時,開銷較大;存在死鎖的風險等等。
    基於這些問題,咱們還有另一個方案,那就是CAS(Compare And Swap),其原理與咱們經常使用的數據庫樂觀鎖相似,即變量更新前檢查當前值是否符合預期,若是符合則用新值替換當前值,不然就循環重試,直到成功。當下主流CPU直接在指令層面上支持了CAS指令,好比atomic底層調用的compareAndSwapInt方法就是這樣一個native方法。所以,CAS的執行效率仍是比較高的。 CAS在使用上還須要注意幾點:
    • 經過版本號的方式,避免ABA問題

    • 循環開銷,衝突嚴重時過多地線程處於循環重試的狀態,將增長CPU的負擔

    • 只能保證一個共享變量的原子性操做,若是想要多個變量同時保證原子性操做,能夠考慮將這些變量放在一個對象中,而後使用AtomicReference類,這個類提供針對對象引用的原子性,從而保證對多個變量操做的原子性。代碼示例9

  • Lock自旋鎖

    • Java提供了Lock接口以及其實現類ReentLock;ReadWriteLock接口以及其實現類ReentrantReadWriteLock

    • 與synchronized鎖不一樣的是,線程在獲取Lock鎖的過程當中不會被阻塞,而是經過循環不斷的重試,直到當前持有該Lock鎖的線程釋放該鎖

    • Synchronized是關鍵字,由編譯器負責生成加鎖和解鎖操做,而ReentrantLock則是一個類,這個加鎖和解鎖的操做徹底在程序員手中,所以在寫代碼時,調用了lock方法以後必定要記得調用unlock來解鎖,最好放在finally塊中

    • 參見代碼示例10

  • Condition條件變量

    • Synchronized的同步機制要求全部線程等待同一對象的監視器「鎖」標記。而且在經過wait/notify/notifyAll方法進行線程間通訊時,只能隨機或者所有且無序的喚醒這些線程,並無辦法「有選擇」地決定要喚醒哪些線程,也沒法避免「非公平鎖」的問題

    • ReentrantLock容許開發者根據實際狀況,建立多個條件變量,全部取得lock的線程能夠根據不一樣的邏輯在對應的condition裏面waiting,每一個Condition對象擁有一個隊列,用於存放處於waiting狀態的線程

    • 這樣的一種設計,一樣可讓開發者根據實際狀況,決定喚醒哪些condition內部waiting的線程,同時還可以實現公平鎖。

    • 參見代碼示例11


-     做者介紹    -

chris
架構師一枚,早期就任於知名通訊公司,致力於通信軟件解決方案。以後就任於五百強諮詢公司,致力於爲大型車企提供數字化轉型方案。現就任於平安銀行信用卡中心,幫助平安銀行落地核心繫統的去IOE化改造。追求技術本質,目前主要方向是複雜系統的分佈式架構設計。

本文分享自微信公衆號 - 川聊架構(gh_44ec4115d261)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索