高併發程序編程1

一.多線程相關的一些概念

1.同步和異步

        同步和異步通常用來形容一次方法的調用。同步方法調用一旦開始,調用者必須等待方法調用返回後,才能進行後續的操作。異步方法調用更像一個消息傳遞,一旦開始,方法調用就好立即返回,調用者就可以繼續後續的操作。異步方法通常在另一個線程中執行。整個過程,不會阻礙調用者的工作。對於調用者來說,異步調用似乎是一瞬間就完成的。如果異步調用需要返回結果,那麼當這個異步調用真實完成時,則會通知調用者。

 

2.併發(Concurrency)和並行(Parallelism)

          並行和併發時非常容易被混淆的概念。他媽都可以表示兩個或者多個任務一起執行,但是偏重點有些不同。併發偏重於多個任務交替執行,而多個任務之間有可能還是串行的。而並行時真正意義上的同時執行。

       嚴格意義上來說,並行的多個任務時真實的同時執行,而對於併發來說,這個過程只是交替的,一會兒運行任務A,一會兒執行任務B,系統會不停的在兩個之間切換。但對於外部觀察者來說,即使多個任務之間時串行併發的,也會造成多任務間時並行執行的錯覺。

      實際上,如果系統內只有一個cpu,而使用多進程或者多線程任務,那麼真實環境中這些任務不可能是真實並行的。畢竟一個cpu一次只能執行一條指令,這種情況霞多進程或者多線程就是併發的,而不是並行的(操作系統會不停的切換多個任務執行)。真實的並行也只可能出現在多核cpu系統中。

3.臨界區

       臨界區用來表示一種公共資源或者是共享數據,可以被多個線程使用。但是每一次,只能有一個線程使用它,一旦臨界區資源被佔用,其他線程要想使用這個資源,就必須等待。

     比如,在一個辦公室裏有一臺打印機。打印機一次只能執行一次任務。如果小王核小米同時需要打印文件,很顯然,如果小王先下了打印任務,打印機就會開始打印小王的文件。小米的任務只能等待小王打印完成後才能打印。這裏的打印機就是一個臨界區的例子。

    在並行程序中,臨界區資源是保護的對象,如果意外出現打印機同時執行兩個打印任務,那麼最可能的結果就是打印出來的文件就會是損壞的文件。它既不是小王需要的,也不是小米需要的。

4.阻塞(blocking)核非阻塞(Non-Blocking)

       阻塞和非阻塞通常用來形容多線程間的相互影響。比如一個線程佔用了臨界區資源,那麼其他所有需要這個資源的線程就必須在這個臨界區中等待。等待會導致線程掛起,這種情況就是阻塞。此時,如果佔用資源的線程一直不願意釋放資源,那麼其他所有阻塞在這個臨界區的線程都不能工作。

     非阻塞的意思與之相反,它強調沒有一個線程可以妨礙其他線程執行。所有的線程都會嘗試不斷向前執行。

5.死鎖(Deadlock)、飢餓(Starvation)、活鎖(Livelock)

    死鎖、飢餓和活鎖都屬於多線程的活躍性問題。如果發現上述這幾種情況,那麼相關線程可能就不再活躍,也就是說它可能很難再繼續往下執行了。

死鎖:就是各個線程彼此佔用對方需要的資源不釋放,卻在等待彼此佔用的資源。這樣就導致彼此執行不下去了。例如:線程a佔用資源a,等待資源b。線程b佔用資源b,等待資源a。誰都不願意釋放自己的資源,這種狀態將一直維持下去就是死鎖。

飢餓:是指某一個或者多個線程因爲雜種原因無法獲得所需要的資源,導致一直無法執行。比如它的線程優先級可能太低,而高優先級的線程不斷搶佔它需要的資源,導致低優先級線程無法工作。另外一種可能是,某一個線程一值佔着關鍵資源不放,導致其他需要這個資源的線程無法正常執行,這種情況也是飢餓的一種。與死鎖相比,飢餓還是有可能在未來一段時間內解決的。(比如高優先級的線程已經執行完)

二.併發級別

     由於臨界區的存在,多線程之間的併發必須受到控制。根據控制併發的策略,我們可以把併發的級別進行分類,大致上可以分爲阻塞,無飢餓,無障礙,無鎖,無等待這幾種。

1.阻塞

    一個線程是阻塞的,那麼在其他線程釋放資源之前,當前線程無法繼續執行。當我們使用synchronized關鍵字,或者重入鎖時,我們得到的就是阻塞的線程。

   無論時synchronized或者重入鎖,都會試圖在執行後續代碼前,得到臨界區的鎖。如果得不到,線程就會被掛起等待,直到佔有了所需資源爲止。

2.無飢餓(Starvation-Free)

     如果線程之間是由優先級的,那麼線程調度的時候總是會傾向於滿足高優先級的線程。也就是說,對於同一個資源的分配,是不公平的。如下圖所示,顯示了非公平於公平兩種情況(五角星代表高優先級線程)。對於非公平的鎖來說,系統允許高優先級的線程插隊。這樣由可能導致低優先級產生飢餓。但如果鎖是公平的,滿足先來後到,那麼飢餓就不會產生。不管新來的線程要學會多高。想獲得資源,就必須乖乖排隊。那麼所有的線程都有機會執行。、

3無障礙(Obstruction-Free)

    無障礙是一種最弱的非阻塞調度。兩個線程如果是無障礙的執行,那麼他們不會因爲臨界區的問題導致一方被掛起。換言之,大家都可以大搖大擺的進入臨界區了。那麼如果大家一起修改共享數據,把數據改壞了可怎麼辦?對於無障礙的線程來說,一旦檢測到這種情況,它就會立即對自己所做的修改進行回滾,確保數據安全。但如果沒有數據競爭發生,那麼線程就可以順利完成自己的工作,走出臨界區。

如果說阻塞的控制方式是保管策略。也就是說,系統認爲兩個線程之間很有可能發生百姓的衝突,因此,以保護共享數據爲第一優先級。相對來說,非阻塞的調度就是一種樂觀的策略。它認爲多個線程之間很有可能不會發生衝突,或者說這種概率不大。因此大家都應該無障礙的執行,但是一旦檢測到衝突,就應該進行回滾。 

    從這個策略中也可以看到,無障礙的多線程程序並不一定能順暢的運行。因爲當臨界區中存在嚴重的衝突時,所有的線程可能都會不斷的回滾自己的操作,而沒有一個線程可以走出臨界區。這種情況會影響系統的正常執行。所以,我們可能會非常希望在這一堆線程中,至少可以有一個線程能夠在有限的時間內完成自己的操作。而退出臨界區。至少這樣可以保證系統不會在臨界區中進行無限的等待。

     一種可行的無障礙實現可以依賴一個“一致性標記”來實現。線程在操作之前,先讀取並保存這個標記,在操作完成後,再次讀取,檢查這個標記是否被更改過,如果兩者一致,則說明資源訪問沒有衝突。如果不一致,則說明資源可能在操作過程中與其他寫線程衝突,需要重試操作。而任何對資源有修改操作的線程,在修改數據前,都需要更新這個一致性標記,表示數據不再安全。

4.無鎖(Lock-Free)

   無鎖的並行都是無障礙的。在無鎖的情況下。所有的線程都能嘗試對臨界區進行訪問,但不同的是,無鎖的併發保證必然有一個線程能夠在有限步內完成操作離開臨界區。 

    在無鎖的調用中,一個典型的特點是可能會包含一個無窮循環。在這個循環中,線程會不斷嘗試修改共享變量。如果沒有衝突,修改成功,那麼程序退出,否則繼續嘗試修改。但無論如何,無鎖的並行總能保證有一個線程是可以勝出的,不至於全軍覆沒。至於臨界區中競爭失敗的線程,它們則不斷重試,直到自己獲勝。如果運氣不好,總是嘗試不成功,則會程序類似飢餓的現象,線程會停止不前。

5.無等待(wait-Free)

     無鎖只要求有一個線程可以在有限步內完成操作,而無等待則在無鎖的基礎上更進一步進行擴展。它要求所有的線程都必須在有限步內完成,這樣就不會引起飢餓問題。如果限制這個步驟上限,還可以進一步分解爲有界無等待和線程數無關的無等待幾種。它們之間的區別只是對循環次數的限制不同.

    一種典型的無等待結構就是RCU(Read-Copy-Update) .它的基本思想是,對數據的讀可以不加控制。因此,所有的讀線程都是無等待的,它們既不會被鎖定等待也不會引起任何衝突。但是在寫數據的時候,先取得原始數據的副本,接着只修改副本數據(這就是爲什麼讀可以不加控制),修改完成後,在合適的時機回寫數據。

 三。Java內存模型(JMM)

       由於併發程序要比串行程序複雜很多,其中一個重要原因是併發程序下數據訪問的一致性和安全性將會受到嚴重挑戰。如何保證一個線程可以看到正確的數據呢?對於串行程序來說,根本小菜一碟,如果你讀取一個變量,這個變量的值是1,那麼你讀到的一定是1,就這麼簡單的問題在並行程序中居然變得複雜起來。事實上,如果不加控制地任由線程胡亂並行,即使原本是1的數值,你也有可能讀到2.因此,我們需要在深入瞭解並行機制的前提下,再定義一種規則,保證多個線程間可以有效地,正確的協同工作。而JMM也就是爲此而生的。

     JMM的關鍵技術點都是圍繞着多線程的原子性,可見性和有序性來建立的。

  1.原子性

       原子性就是一個操作時不可中斷的。即使是在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程干擾。

2.可見性

    可見性是指當一個線程修改了某個共享變量的值,其他線程是否能夠立即知道這個修改。顯然,對於串行程序來說,可見性問題是不存在的。因爲你在任何一個操作步驟中修改了某個變量,那麼在後續的步驟中,讀取這個變量的值,一定是修改後的新值。

3.有序性

  對於一個線程的執行代碼來說,我們總是習慣的認爲代碼的執行是從先往後,依次執行的。這麼理解也不能說完全錯誤,因爲就一個線程內而言,確實會表現成這樣。但是,在併發是,程序的執行可能就會出現亂序。實際上jvm爲了優化CPU處理性能,代碼底層彙編指令可能會做指令重排,在併發中可能會帶來亂序問題。

     哪些指令不能重排:Happen-Before規則

   程序順序原則:一個線程內保證語義的串行性

  volatile規則:volatile變量的寫,先發生於讀,這保證了volatile變量的可見性

  鎖規則:解鎖(unlock)必然發生於隨後的加鎖(lock)前

 傳遞性:A先於B,B先於C,那麼A必然先於C

 線程的start()方法先於他的每一個動作

 線程的所有操作先於線程的終結

 線程的中斷(interrupt)先於被中斷的代碼

 對象的構造函數執行,結束先於finalize()方法

 

          volatile不能保證原子性,確保可見性和有序性 

   

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章