最近遇到了些併發的問題,恰巧也有朋友問我相似的問題,無奈併發基礎知識過弱,只大概瞭解使用一些同步機制和併發工具包類,沒有造成一個完整的知識體系,並不能給出一個良好的解決方案。深知本身就是個弟弟,趁着週末有空,就趕忙把以前買的併發編程實戰拿起來,擦擦灰,惡補一下....java
在介紹併發前,咱們先來簡單的瞭解下計算機的發展歷史。早期的計算機是不包含操做系統的,他們可使用計算機上全部的資源,計算機從頭至尾也就只執行着一個程序。在這種裸機環境下,編寫和運行程序將變的很是麻煩,而且只運行一個程序對於計算機來講也是一種嚴重的浪費。爲了解決這個問題,操做系統閃亮登場,計算機每次都能運行多個程序,每一個程序都是一個單獨的進程。操做系統爲每個進程分配各類資源,好比:內存、文件句柄等。若是須要的話,不一樣的進程之間能夠經過通訊機制來交換數據。數據庫
操做系統的出現,主要給咱們解決了這幾個問題,資源利用率的提升,程序之間的公平性和便利性。編程
有些狀況下,程序必須等待某個外部操做完成才能繼續進行。好比當咱們向計算機複製數據的時候,此時只有io在工做,若是在等待複製的時間,計算機能夠運行其餘程序,無疑大大提升了資源的利用率。安全
操做系統常見的一種方式就是經過粗粒度的時間分片來使用戶和程序能共享計算機資源,而不是一個程序從頭運行到尾,而後再啓動下一個程序。想想,你能夠用着本身的我的pc,打着遊戲,聽着歌,和女友聊着天,計算機資源會來回切換,只不過由於速度很快,給咱們的感受就像是同時發生同樣,這一切都要歸功於操做系統的調配。多線程
通常來講,在計算多個任務時,應該編寫多個程序,每一個程序在執行一個任務時並在須要時進行通訊,這比只編寫一個程序來計算全部任務更容易實現。併發
線程的出現和進程的出現是一個道理的,只不過一個調配的是一個進程內的資源問題,另外一個是調配一臺計算機之間的資源。進程容許存在多個線程,而且線程之間會共享進程範圍內的資源(內存和文件句柄),但每一個線程都有本身的程序計數器、棧等,並且同一個程序的多個線程能夠同時被調度到多個cpu上運行。
線程被稱爲輕量級進程。在大多數操做系統中,線程都是最基本的調度單位。若是沒有統一的協同機制,線程將彼此獨立運行,因爲同一個進程上的全部線程都將共享進程的內存空間,它們將訪問相同的變量並在同一個堆上分配對象,這就須要一個更細粒度的數據共享機制,否則將形成不可預測的後果。框架
class ThreadSafeTest{ static int count; public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(1); for (int i=0;i<100;i++){ new Thread(new Runnable() { @Override public void run() { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } for (int x=0;x<100;x++){ count++; } } }).start(); } countDownLatch.countDown(); Thread.sleep(5000); System.out.println("count:"+count); } } 輸出結果:count:9635
運行結果:count:9955.咱們的期待結果是10000,而且咱們屢次的運行結果還可能不同。這個問題主要在於count++並非一個原子操做,它能夠分爲讀取count,count+1和計算結果寫回。若是在缺乏同步的狀況下,咱們沒法保證多線程狀況下結果的正確性.異步
當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。
從ThreadSafeTest例子咱們能夠清楚線程安全性多是很是複雜的,再沒有充足同步的狀況下,多個線程中的操做執行順序是不可預測的,可能會發生奇怪的結果。ide
public class NoStateCalculate implements Servlet { @Override public void service(ServletRequest req, ServletResponse res) throws ServletException, IOException { //解析數據樣本,計算結果calculate BigInteger calculate = calculate(req); Integer data= getData(calculate); res.getOutputStream().print(data); } }
這個servlet從請求中提取參數並計算結果calculate,而後去數據庫中查詢對應的數據data,最終將其寫入到輸出中。它是一個無狀態的類,它不包含任何域,也不包含其餘任何域的引用,全部的臨時狀態都存在於線程棧上的局部變量表中,而且只能由正在執行的線程訪問,線程之間不會相互影響,所以能夠說線程之間沒有共享狀態。因爲多線程訪問無狀態對象的行爲不會影響到其餘線程中操做的正確性,所以無狀態對象必定是線程安全的。工具
ThreadSafeTest例子並非一個線程安全的例子,緣由是將有100個線程同時調用count++,而count++又不是一個原子性的操做,其結果將多是不正確的。
競態條件:當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會發生競態條件(正確的結果依賴於運氣)。
複合操做:count++就是一組複合操做,要避免競態條件問題,就必須將操做原子化。
讓咱們對ThreadSafeTest進行改造,使用jdk提供的原子變量類AtomicInteger:
public class ThreadSafeTest { static AtomicInteger count =new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { final CountDownLatch countDownLatch = new CountDownLatch(1); for (int i=0;i<100;i++){ new Thread(new Runnable() { @Override public void run() { try { countDownLatch.await(); } catch (InterruptedException e) { e.printStackTrace(); } for (int x=0;x<100;x++){ count.incrementAndGet(); } } }).start(); } countDownLatch.countDown(); Thread.sleep(5000); System.out.println("count:"+count); } }
經過AtomicInteger能夠將單個整數變量的操做原子化,使其變成線程安全的。當共享狀態變量多於一個時,這種機制就不能解決問題了,應該經過鎖機制保證操做的原子性。
內置鎖:java提供了synchronized內置鎖來支持原子性。它能夠修飾在方法上,代碼塊上,其中同步代碼塊和普通方法的鎖就是方法調用所在的對象,而靜態方法的synchronized則以當前的Class對象做爲鎖。線程在進入同步代碼以前會自動得到鎖,退出同步代碼塊時自動釋放鎖。synchronized同時也是一種互斥鎖,最多隻有一個線程持有這種鎖,因此它能夠保證同步代碼操做的原子性。
重入鎖:當某個線程請求一個由其餘線程持有的鎖時,發出的請求就會阻塞。然而內置鎖是可重入的,所以若是某個線程試圖得到一個已經由他本身的持有的鎖,那麼這個請求就會成功。"重入"意味着獲取鎖的操做的粒度是線程,重入的一種實現方式就是爲每一個鎖關聯一個獲取計數值和一個全部者線程。當數值爲0時就表明鎖沒有被任何線程得到,當一個線程得到鎖時,計數器會加1,當同一個線程再次得到鎖時,將會在加1,以此來實現鎖的重入功能。
用鎖來保護狀態:
java的語法糖提供了一個內置鎖synchronized,它能夠很好地保證同步代碼塊只有一個線程執行。可是若是synchronized使用的不當,將會帶來嚴重的活躍性和性能問題。其對應的優化技術有不少,避免死鎖,減少鎖粒度,鎖分段等等均可有效的解決活躍性問題和性能問題(留到之後再介紹)。