淺談多線程

概述

最近遇到了些併發的問題,恰巧也有朋友問我相似的問題,無奈併發基礎知識過弱,只大概瞭解使用一些同步機制和併發工具包類,沒有造成一個完整的知識體系,並不能給出一個良好的解決方案。深知本身就是個弟弟,趁着週末有空,就趕忙把以前買的併發編程實戰拿起來,擦擦灰,惡補一下....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和計算結果寫回。若是在缺乏同步的狀況下,咱們沒法保證多線程狀況下結果的正確性.異步

  • 活躍性問題
    安全性的含義是永遠不會發生錯誤的事情,而活躍性的含義將是正確的事情最終會發生。當某個操做沒法繼續執行下去,就會發生活躍性的問題。在穿行程序中,無心中形成的無限循環就是活躍性問題之一。此外分別還有死鎖、飢餓以及活鎖問題。
    死鎖:線程A在等待線程B釋放其擁有的資源,而線程B在等待線程A釋放其擁有的資源,這樣僵持不下,那麼線程A、B就會永遠等下去。
    飢餓:最多見的飢餓問題就是CPU時鐘週期問題。若是在java程序中存在持有鎖時執行一些沒法結束的結構(無限循環或者是等待某個資源發生阻塞),那麼極可能將致使飢餓,由於其餘須要這個鎖的線程將沒法獲得它。
    活鎖:活鎖不會阻塞線程,但也不能繼續執行。假如程序不能正確的執行某個操做,由於事務回滾,並將其放到隊列的頭部。因爲這條事務回滾的消息被放回到隊列頭部,處理器將反覆調用,並返回相同的結果。
  • 性能問題
    性能問題和活躍性問題是密切相關的。活躍性意味着某件正確的事情最終會發生,可是咱們通常更但願正確的事情儘快的發生。性能問題包括多個方面:服務時間過長、響應不靈敏、吞吐率太低、資源消耗太高、和可伸縮性較差等。在多線程程序中,,還存在因爲使用多線程而引入的其餘問題。在多線程程序中,當線程調度器臨時掛起活躍線程並轉而運行另外一個線程時,就會頻繁的出現上下文切換操做,這種操做將帶來極大的開銷(保存和恢復執行上下文,丟失局部性,而且CPU時鐘週期將更多地花費在線程調度上而不是線程運行上)。而且,當多個線程共享數據時,必須使用同步機制,而這些機制每每會抑制某些編譯器優化,使內存緩衝區的數據無效,以及增長共享內存總線的同步流量。

線程安全性

  • 什麼是線程安全性?
當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱這個類是線程安全的。

從ThreadSafeTest例子咱們能夠清楚線程安全性多是很是複雜的,再沒有充足同步的狀況下,多個線程中的操做執行順序是不可預測的,可能會發生奇怪的結果。ide

  • 無狀態對象必定是線程安全的**
    相信你們都對servlet有過了解,它是一個框架,其做用大概就是接收請求,處理參數,分發請求和返回結果。servlet是線程安全的,由於它是無狀態的。咱們來自定義個servlet:
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使用的不當,將會帶來嚴重的活躍性和性能問題。其對應的優化技術有不少,避免死鎖,減少鎖粒度,鎖分段等等均可有效的解決活躍性問題和性能問題(留到之後再介紹)。

相關文章
相關標籤/搜索