併發編程的基本知識

1、概述

所謂併發編程是指在一臺處理器上「同時」處理多個任務。併發是在同一實體上的多個事件,多個事件在同一時間間隔發生。java

1.併發和多線程聯繫

併發與多線程之間的關係就是目的與手段之間的關係。併發的反面是串行。串行比如多個車輛行駛在一股車道上,它們只能「魚貫而行」。而併發比如多個車輛行駛在多股車道上,它們能夠「並駕齊驅」。併發的極致就是並行。多線程就是將本來多是串行的計算「改成」併發(並行)的一種手段、途徑或者模型。所以,有時咱們也稱多線程編程爲併發編程。多線程編程每每是其餘併發編程模型的基礎,因此多線程編程的重要性不言而喻。多線程能夠實現任務併發執行,也能夠實現任務串行有序執行。多線程僅是實現併發編程的一種有效手段,固然,目的與手段之間經常是一對多的關係,即實現併發的手段還有別的方式,諸如協程等。算法

2.併發與並行區別

併發和並行是十分容易混淆的概念。併發指的是多個任務交替進行,而並行則是指真正意義上的「同時進行」。實際上,若是系統內只有一個CPU,而使用多線程時,那麼真實系統環境下不能並行,只能經過切換時間片的方式交替進行,而成爲併發執行任務。真正的並行也只能出如今擁有多個CPU的系統中。編程

併發:與單位時間有關,在單位時間內能夠處理問題的能力。安全

並行:同一時刻,能夠同時處理事情的能力。bash

舉個例子,假設不考慮超線程技術,一個4核cpu在任何一個時刻處理的是4個線程,並行數爲4,而因爲時間片輪起色制,它在1秒內能夠支持處理100個線程,它在1秒內的併發數爲100。數據結構

2、目的&優勢

1.充分利用多核CPU的計算能力

併發編程的目標是充分的利用處理器的每個核,以達到最高的處理性能。該形式能夠將多核CPU的計算能力發揮到極致,性能獲得提高。多線程

2.方便進行業務拆分,提高應用性能

面對複雜業務模型,並行程序會比串行程序更適應業務需求,而併發編程更能吻合這種業務拆分。併發

3、問題&缺點

多線程技術有這麼多的好處,難道就沒有一點缺點麼,就在任何場景下就必定適用麼?很顯然不是。dom

1.線程安全

多線程改善了系統資源的利用率並提升了系統的處理能力。然而,併發執行也帶來了新的問題——死鎖。所謂死鎖是指多個線程因競爭資源而形成的一種僵局(互相等待),若無外力做用,這些進程都將沒法向前推動。在下面會單獨說明死鎖的問題以及如何避免。ide

2.頻繁的上下文切換

時間片是CPU分配給各個線程的時間,由於時間很是短,因此CPU不斷經過切換線程,讓咱們以爲多個線程是同時執行的,時間片通常是幾十毫秒。而每次切換時,須要保存當前的狀態起來,以便可以進行恢復先前狀態,而這個切換時很是損耗性能,過於頻繁反而沒法發揮出多線程編程的優點。一般減小上下文切換能夠採用無鎖併發編程,CAS算法,使用最少的線程和使用協程。

無鎖併發編程:能夠參照concurrentHashMap鎖分段的思想,不一樣的線程處理不一樣段的數據,這樣在多線程競爭的條件下,能夠減小上下文切換的時間。

CAS算法:利用Atomic下使用CAS算法來更新數據,使用了樂觀鎖,能夠有效的減小一部分沒必要要的鎖競爭帶來的上下文切換。

使用最少線程:避免建立不須要的線程,好比任務不多,可是建立了不少的線程,這樣會形成大量的線程都處於等待狀態。

協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。

因爲上下文切換也是個相對比較耗時的操做,因此在"java併發編程的藝術"一書中有過一個實驗,併發累加未必會比串行累加速度要快。 可使用Lmbench3測量上下文切換的時長 vmstat測量上下文切換次數。

4、死鎖

1.定義

死鎖是指多個線程因競爭資源而形成的一種僵局(互相等待),若無外力做用,這些線程都將沒法向前推動。

下面咱們經過一些實例來講明死鎖現象。

先看生活中的一個實例,2我的一塊兒吃飯可是隻有一雙筷子,2人輪流吃(同時擁有2只筷子才能吃)。某一個時候,一個拿了左筷子,一人拿了右筷子,2我的都同時佔用一個資源,等待另外一個資源,這個時候甲在等待乙吃完並釋放它佔有的筷子,同理,乙也在等待甲吃完並釋放它佔有的筷子,這樣就陷入了一個死循環,誰也沒法繼續吃飯。。。 在計算機系統中也存在相似的狀況。例如,某計算機系統中只有一臺打印機和一臺輸入設備,進程P1正佔用輸入設備,同時又提出使用打印機的請求,但此時打印機正被進程P2所佔用,而P2在未釋放打印機以前,又提出請求使用正被P1佔用着的輸入設備。這樣兩個進程相互無休止地等待下去,均沒法繼續執行,此時兩個進程陷入死鎖狀態。

2.產生的緣由

多個線程同時被阻塞,它們中的一個或者所有都在等待某個資源被釋放,而該資源又被其餘線程鎖定,從而致使每個線程都得等其它線程釋放其鎖定的資源,形成了全部線程都沒法正常結束。死鎖產生的四個必要條件:

(1)互斥使用,即當資源被一個線程佔用時,別的線程不能使。

(2)不可搶佔,資源請求者不能強制從資源佔有者手中奪取資源,資源只能由資源佔有者主動釋放。

(3)請求和保持,即當資源請求者在請求其餘的資源的同時保持對原有資源的佔有。

(4)循環等待,即存在一個等待隊列:P1佔有P2的資源,P2佔有P3的資源,P3佔有P1的資源。這樣就造成了一個等待環路。

當上述四個條件都成立的時候,便造成死鎖。固然,死鎖的狀況下若是打破上述任何一個條件,即可讓死鎖消失。

下面是產生死鎖的一個例子

public class DeadLock implements Runnable {  
    public int flag = 1;  
    //靜態對象是類的全部對象共享的  
    private static Object o1 = new Object(), o2 = new Object();  
    @Override  
    public void run() {  
        System.out.println("flag=" + flag);  
        if (flag == 1) {  
            synchronized (o1) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o2) {  
                    System.out.println("1");  
                }  
            }  
        }  
        if (flag == 0) {  
            synchronized (o2) {  
                try {  
                    Thread.sleep(500);  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
                synchronized (o1) {  
                    System.out.println("0");  
                }  
            }  
        }  
    }  
  
    public static void main(String[] args) {  
          
        DeadLock td1 = new DeadLock();  
        DeadLock td2 = new DeadLock();  
        td1.flag = 1;  
        td2.flag = 0;  
        //td1,td2都處於可執行狀態,但JVM線程調度先執行哪一個線程是不肯定的。  
        //td2的run()可能在td1的run()以前運行  
        new Thread(td1).start();  
        new Thread(td2).start();  
  
    }  
}  
複製代碼

當DeadLock類的對象flag==1時(td1)線程啓動,先鎖定o1,睡眠500毫秒;

而td1在睡眠的時候,另外一個flag==0的對象(td2)線程啓動,先鎖定o2,睡眠500毫秒;

td1睡眠結束後須要鎖定o2才能繼續執行,而此時o2已被td2鎖定;

td2睡眠結束後須要鎖定o1才能繼續執行,而此時o1已被td1鎖定;

td一、td2相互等待,都須要獲得對方鎖定的資源才能繼續執行,從而死鎖。

3.如何避免死鎖

在有些狀況下死鎖是能夠避免的。三種用於避免死鎖的技術:

(1)加鎖順序(線程按照必定的順序加鎖)

(2)加鎖時限(線程嘗試獲取鎖的時候加上必定的時限,超過期限則放棄對該鎖的請求,並釋放本身佔有的鎖)

(3)死鎖檢測

加鎖順序

當多個線程須要相同的一些鎖,可是按照不一樣的順序加鎖,死鎖就很容易發生。若是能確保全部的線程都是按照相同的順序得到鎖,那麼死鎖就不會發生。看下面這個例子:

Thread 1:
  lock A 
  lock B

Thread 2:
   wait for A
   lock C (when A locked)

Thread 3:
   wait for A
   wait for B
   wait for C
複製代碼

若是一個線程(好比線程3)須要一些鎖,那麼它必須按照肯定的順序獲取鎖。它只有得到了從順序上排在前面的鎖以後,才能獲取後面的鎖。

例如,線程2和線程3只有在獲取了鎖A以後才能嘗試獲取鎖C。由於線程1已經擁有了鎖A,因此線程2和3須要一直等到鎖A被釋放。在它們嘗試對B或C加鎖以前,必須成功地對A加了鎖。

按照順序加鎖是一種有效的死鎖預防機制。可是,這種方式須要事先知道全部可能會用到的鎖並對這些鎖作適當的排序,但總有些時候是沒法預知的。

加鎖時限

另一個能夠避免死鎖的方法是在嘗試獲取鎖的時候加一個超時時間,這也就意味着在嘗試獲取鎖的過程當中若超過了該時限,則該線程放棄對該鎖請求。即線程A沒有在給定的時限內成功得到全部須要的鎖,則會進行回退並釋放全部已經得到的鎖,而後等待一段隨機的時間再重試。這段隨機的等待時間讓其它線程有機會嘗試獲取線程A以前持有的鎖,而且讓該應用在沒有得到鎖的時候能夠繼續運行,不至於卡死(加鎖超時後能夠先繼續運行乾點其它事情,再回頭來重複以前加鎖的邏輯)。

如下例子,展現了兩個線程以不一樣的順序嘗試獲取相同的兩個鎖,在發生超時後回退並重試的場景:

Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked

Thread 1 lock attempt on B time out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.

Thread 2 lock attempt on A time out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.
複製代碼

在上面的例子中,線程2比線程1早200毫秒進行重試加鎖,所以它能夠先成功地獲取到兩個鎖。這時,線程1嘗試獲取鎖A而且處於等待狀態。當線程2結束時,線程1也能夠順利的得到這兩個鎖(除非線程2或者其它線程在線程1成功得到兩個鎖以前又得到其中的一些鎖)。

須要注意的是,因爲存在鎖的超時,因此咱們不能認爲這種場景就必定是出現了死鎖。也多是由於得到了鎖的線程須要很長的時間去完成它的任務,致使其它線程超時。

此外,若是有很是多的線程同一時間去競爭同一批資源,就算有超時和回退機制,仍是可能會致使這些線程重複地嘗試但卻始終得不到鎖。若是隻有兩個線程,而且重試的超時時間設定爲0到500毫秒之間,這種現象可能不會發生,可是若是是10個或20個線程狀況就不一樣了。由於這些線程等待相等的重試時間的機率就高的多(或者很是接近以致於會出現問題)。

死鎖檢測

死鎖檢測是一個更好的死鎖預防機制,它主要是針對那些不可能實現按序加鎖 & 鎖超時也不可行的場景。

每當一個線程得到了鎖,會在線程和鎖相關的數據結構中(map、graph等等)將其記下。除此以外,每當有線程請求鎖,也須要記錄在這個數據結構中。

當一個線程請求鎖失敗時,這個線程能夠遍歷鎖的關係圖看看是否有死鎖發生。例如,線程A請求鎖7,可是鎖7這個時候被線程B持有,這時線程A就能夠檢查一下線程B是否已經請求了線程A當前所持有的鎖。若是線程B確實有這樣的請求,那麼就是發生了死鎖(線程A擁有鎖1,請求鎖7;線程B擁有鎖7,請求鎖1)。

固然,死鎖通常要比兩個線程互相持有對方的鎖這種狀況要複雜的多。線程A等待線程B,線程B等待線程C,線程C等待線程D,線程D又在等待線程A。線程A爲了檢測死鎖,它須要遞進地檢測全部被B請求的鎖。從線程B所請求的鎖開始,線程A找到了線程C,而後又找到了線程D,發現線程D請求的鎖被線程A本身持有着。這是它就知道發生了死鎖。

下面是一幅關於四個線程(A、B、C、D)之間鎖佔有和請求的關係圖。像這樣的數據結構就能夠被用來檢測死鎖。

那麼當檢測出死鎖時,這些線程該作些什麼呢?

一個可行的作法是釋放全部鎖&回退,而且等待一段隨機的時間後重試。這個和簡單的加鎖超時相似,不同的是隻有死鎖已經發生了纔回退,而不會是由於加鎖的請求超時了。雖然有回退和等待,可是若是有大量的線程競爭同一批鎖,它們仍是會重複地死鎖(緣由同超時相似,不能從根本上減輕競爭)。

一個更好的方案是給這些線程設置優先級,讓一個(或幾個)線程回退,剩下的線程就像沒發生死鎖同樣繼續保持着它們須要的鎖。若是賦予這些線程的優先級是固定不變的,同一批線程老是會擁有更高的優先級。爲避免這個問題,能夠在死鎖發生的時候設置隨機的優先級。

相關文章
相關標籤/搜索