七週七併發之線程與鎖 頂 原 薦

1.概述

1.1併發還是並行(Concurrent or Parallel)

  • A concurrent program has multiple logical threads of control. These threads may or may not run in parallel.

  • A parallel program may or may not have more than one logical thread of control.

  • 併發是問題域中的概念:程序需要設計成能夠處理多個同時(幾乎同時)發生的事件;而並行則是方法域中的概念:通過將問題中的多個部分併發執行,來加速解決問題.

  • 併發是同一時間**應對(dealing with)多件事件的處理能力;並行是同一時間動手做(doing)**多件事情的能力

  • 可以同時處理多個問題,但是每次只做一件事,是併發.

  • 我妻子是一位教師。與衆多教師一樣,她極其善於處理多個任務。她雖然每次只能做一件事,但可以併發地處理多個任務。比如,在聽一位學生朗讀的時候,她可以暫停學生的朗讀,以維持課堂秩序,或者回答學生的問題。這是併發,但併不併行(因爲僅有她一個人,某一時刻只能進行一件事)。 但如果還有一位助教,則她們中一位可以聆聽朗讀,而同時另一位可以回答問題。這種方式既是併發,也是並行。 假設班級設計了自己的賀卡並要批量製作。一種方法是讓每位學生製作五枚賀卡。這種方法是並行,而(從整體看)不是併發,因爲這個過程整體來說只有一個任務。

  • 併發和並行經常被混淆的一個原因是,傳統的“線程與鎖”模型並沒有顯式支持並行。如果要用線程與鎖模型爲多核進行開發,唯一的選擇就是寫一個併發的程序,讓其並行地運行在多核上。

  • 併發程序通常是不確定的, 並行程序可能是確定的. 使用一門直接支持並行的編程語言可以寫出並行程序,而不會引入不入不 確定性.

1.2 並行架構

位級(bit-level)並行

  • 32位計算機比8位計算機運行速度更快,因爲並行,對於兩個32位數的加法,8位計算 機必須進行多次8位計算,而32位計算機可以一步完成,即並行地處理32位數的4字節。

指令級(instruction-level)並行

  • 現代CPU的並行度很高,其中使用的技術包括流水線、亂序執行和猜測執行等。入多核時代,我們必須面對的情況是:無論是表面上還是實質上,指令都不再串行 執行了。這個就是內存可見性的問題啊,由於cpu對指令的重排序!!重排序問題的引入

數據級(data)並行 數據級並行

  • 圖像處理就是一種適合進行數據級並行的場景。比如,爲了增加圖片亮度就需要增加每一個像 素的亮度。現代GPU(圖形處理器)也因圖像處理的特點而演化成了極其強大的數據並行處理器。

任務級(task-level)並行

  • 終於來到了大家所默認的並行形式——多處理器。從程序員的角度來看,多處理器架構最明 顯的分類特徵是其內存模型(共享內存模型或分佈式內存模型)。

  • 共享內存模型(通過內存通信)

  • 分佈式內存模型( 通過網絡通信 )

1.3 併發:不只是多核

  • 併發的世界,併發的軟件
  • 分佈式的世界,分佈式的軟件
  • 不可預測的世界,容錯性強的軟件
  • 複雜的世界,簡單的軟件
  • 在選對編程語言和工具的情況下,比起串行的等價解決方案,一個併發的解決方案會更簡潔清晰。
  • 如果解決方案有着與問題類似的併發結構,就會簡單許多:我們不需要創建一個複雜的線程來處理問題中的多個任務,只需要用多個簡單的線程分別處理不同的任務即可。

1.4 七個模型

線程與鎖

  • 線程與鎖模型有很多衆所周知的不足,但仍是其他模型的技術基礎,也是很多併發軟件開發的首選。

函數式編程

  • 函數式編程日漸重要的原因之一,是其對併發編程和並行編程提供了良好的支持。函數式編程消除了可變狀態,所以從根本上是線程安全的,而且易於並行執行。

Clojure之道——分離標識與狀態

  • 編程語言Clojure是一種指令式編程和函數式編程的混搭方案,在兩種編程方式上取得了微妙的平衡來發揮兩者的優勢。

actor

  • actor模型是一種適用性很廣的併發編程模型,適用於共享內存模型和分佈式內存模型,也適合解決地理分佈型問題,能提供強大的容錯性。

通信順序進程(Communicating Sequential Processes,CSP)

  • 表面上看,CSP模型與actor模型很相似,兩者都基於消息傳遞。不過CSP模型側重於傳遞信息的通道,而actor模型側重於通道兩端的實體,使用CSP模型的代碼會帶有明顯不同的風格。

數據級並行

  • 每個筆記本電腦裏都藏着一臺超級計算機——GPU。GPU利用了數據級並行,不僅可以快速進行圖像處理,也可以用於更廣闊的領域。如果要進行有限元分析、流體力學計算或其他的大量數字計算,GPU的性能將是不二選擇。

Lambda架構

  • 大數據時代的到來離不開並行——現在我們只需要增加計算資源,就能具有處理TB級數據的能力。Lambda架構綜合了MapReduce和流式處理的特點,是一種可以處理多種大數據問題的架構。

2.線程與鎖

2.1 簡單粗暴

  • 線程與鎖其實是是對底層硬件運行過程的形式化.這是他的最大優點也是最大缺點
  • 現在的優秀代碼很少直接使用底層服務:不應在產品代碼上直接使用Thread類等底層服務

2.2 第一天:互斥和內存模型

競態條件

內存可見性

class Counter {
    private int count = 0;
    public synchronized void increment() { ++count; } ➤
    public int getCount() { return count; }
}
  • 這段代碼沒有競態條件的bug 但是又內存可見性的bug 因爲getCount()沒有加鎖,調用getCount()可能獲得一個失效的值

死鎖

  • 哲學家進餐問題
class Philosopher extends Thread {
    private Chopstick left, right;
    private Random random;

    public Philosopher(Chopstick left, Chopstick right) {
        this.left = left; this.right = right;
                random = new Random();
    }

    public void run() {
        try {
            while(true) {
                    Thread.sleep(random.nextInt(1000)); // Think for a while
                synchronized(left) { // Grab left chopstick //
                    synchronized(right) { // Grab right chopstick // 15
                        Thread.sleep(random.nextInt(1000)); // Eat for a while
                    }
                }
            }
        } catch(InterruptedException e) {}
    }
}
  • 創建五個哲學家實例,這個程序可以愉快的運行很久,但到某個時刻一切會停下來:如果所有哲學家同時進餐,就會死鎖(相鄰的幾個同時準備進餐還不至於會死鎖)
  • 一個線程想使用多把鎖的時候就要考慮死鎖的可能,有一個簡單的規則還有避免死鎖:總是按照一個全局的固定的順序獲取多把鎖,其中一種實現如下:
class Philosopher extends Thread {
    private Chopstick first, second;
    private Random random;
    private int thinkCount;

    public Philosopher(Chopstick left, Chopstick right) {
        if(left.getId() < right.getId()) {
            first = left; second = right;
        } else {
            first = right; second = left;
        }
        random = new Random();
    }

    public void run() {
        try {
            while(true) {
                ++thinkCount;
                if (thinkCount % 10 == 0)
                    System.out.println("Philosopher " + this + " has thought " + thinkCount + " times");
                Thread.sleep(random.nextInt(1000));     // Think for a while
                synchronized(first) {                   // Grab first chopstick
                    synchronized(second) {                // Grab second chopstick
                        Thread.sleep(random.nextInt(1000)); // Eat for a while
                    }
                }
            }
        } catch(InterruptedException e) {}
    }
}
  • 程序解釋:當所有人同時決定進餐的時候,ABCD左手分別拿起1234號筷子(對於他們小的編號的筷子還是在左手),這和上面的程序沒啥不同,但是差別就在這個E,他左邊的筷子是大編號,所以他左手拿的是1,然而1被A拿了,所以他就一隻筷子都拿不到,所以D可以正常進餐,就不會死鎖

  • 侷限:獲取鎖的代碼寫的比較集中的話,有利於維護這個全局順序,但是對於規模比較大的程序,使用鎖的地方比較零散,各處都遵守這個順序就顯得不太實際.

  • 技巧:使用對象的散列值作爲鎖的全局順序

  • 優點:適用於所有java對象,不用爲鎖對象專門定義並維護一個順序,

  • 缺點:但是對象的散列值不能保證唯一性(雖然機率很小), 不是迫不得已不要使用

if(System.identityHashCode(left) < System.identityHashCode(right)) {
            first = left; second = right;
} else {
            first = right; second = left;
}

來自外星方法的危害

  private synchronized void updateProgress(int n) {
    for (ProgressListener listener: listeners) // listeners是累的一個field
      listener.onProgress(n);
  }
  • 上面的方法乍一看好像沒啥問題,但是這個方法調用了onProgress()方法,我們對這個方法一無所知, 要是他裏面還有一把鎖,就可能會死鎖
  • 解決方案:在遍歷前對listeners進行保護性複製(defensive copy),再針對這份副本進行遍歷

  private void updateProgress(int n) {
    ArrayList<ProgressListener> listenersCopy;
    synchronized(this) {
      listenersCopy = (ArrayList<ProgressListener>)listeners.clone();
    }
    for (ProgressListener listener: listenersCopy)
      listener.onProgress(n);
  }
  • 這個方案一石多鳥,不僅調用外星方法的時候不用加鎖,而且還大大減少了代碼持有鎖的時間(前面是對方法加synchronized,這裏是對語句塊)

避免危害的準則

  • 1.對共享變量的所有訪問都需要同步化(讀髒數據,競態條件)
  • 2.讀線程和寫線程都需要同步化(內存可見性)
  • 3.按照約定的全局順序獲取多把鎖(死鎖)
  • 4.當持有鎖的時候避免調用外星方法(你對外星方法一無所知,要是他裏面有鎖,就會死鎖)
  • 5.持有鎖的時間儘可能短

2.3第二天:超越內置鎖

  • ReentrantLock提供了顯式的lock和unlock
Lock lock = new ReentrantLock();
lock.lock();
try{
  //使用共享資源

} finally { //使用finally確保鎖釋放
  lock.unlock();
}

可中斷的鎖

  • 使用內置鎖,由於阻塞的線程無法被中斷,所以程序不可能從死鎖中恢復,可以用ReentrantLock代替內置鎖,使用它的lockInterruptibly 在下面的程序中使用Thread.interrupt()可以讓線程終止(這裏說的都是死鎖情況下)
    final ReentrantLock l1 = new ReentrantLock();
    final ReentrantLock l2 = new ReentrantLock();

    Thread t1 = new Thread() {
      public void run() {
        try {
          l1.lockInterruptibly();
          Thread.sleep(1000);
          l2.lockInterruptibly();
        } catch (InterruptedException e) { System.out.println("t1 interrupted"); }
      }
    };

超時

  • ReentrantLock突破了內置鎖的另一個限制:可以爲獲取鎖的操作設置超時時間,可以用這種方式來解決哲學家進餐問題
class Philosopher extends Thread {
  private ReentrantLock leftChopstick, rightChopstick;
  private Random random;
  private int thinkCount;

  public Philosopher(ReentrantLock leftChopstick, ReentrantLock rightChopstick) {
    this.leftChopstick = leftChopstick; this.rightChopstick = rightChopstick;
    random = new Random();
  }

  public void run() {
    try {
      while(true) {
        ++thinkCount;
        if (thinkCount % 10 == 0)
          System.out.println("Philosopher " + this + " has thought " + thinkCount + " times");
        Thread.sleep(random.nextInt(1000)); // Think for a while
        leftChopstick.lock();
        try {
          if (rightChopstick.tryLock(1000, TimeUnit.MILLISECONDS)) {
            // Got the right chopstick
            try {
              Thread.sleep(random.nextInt(1000)); // Eat for a while
            } finally { rightChopstick.unlock(); }
          } else {
            // Didn't get the right chopstick - give up and go back to thinking
            System.out.println("Philosopher " + this + " timed out");
          }
        } finally { leftChopstick.unlock(); }
      }
    } catch(InterruptedException e) {}
  }
}
  • 雖然上述代碼不會死鎖,但也不是一個足夠好的方案,後面有更好的方案
  • 1.這個方案不能避免死鎖,只能避免無盡的死鎖 只是提供了從死鎖中恢復的手段
  • 2.會受到活鎖現象,如果所有死鎖線程同時超時,它們極有可能再次陷入死鎖,雖然死鎖沒有永遠持續下去,但對資源的爭奪狀況沒有得到任何改善(可以用一些方法減少活鎖的機率,比如爲每個線程設置不同的超時時間)

交替鎖(hand-over-hand locking)

  • 交替鎖可以只鎖住鏈表的一部分,允許不涉及被鎖部分的其他線程自由訪問鏈表.插入新的鏈表節點時,需要將待插入位置兩邊的節點加鎖.首先鎖住鏈表的前兩個節點,如果這兩個節點之間不是待插入的位置,那麼就解鎖第一個,並鎖住第三個,以此類推,知道找到待插入位置並插入新的節點,最後解鎖兩邊的節點

  • 這種交替型的加鎖和解鎖順序無法用內置鎖實現,使用ReentrantLock可以

class ConcurrentSortedList {
  private class Node {
    int value;
    Node prev;
    Node next;
    ReentrantLock lock = new ReentrantLock();

    Node() {}

    Node(int value, Node prev, Node next) {
      this.value = value; this.prev = prev; this.next = next;
    }
  }

  private final Node head;
  private final Node tail;

  public ConcurrentSortedList() {
    head = new Node(); tail = new Node();
    head.next = tail; tail.prev = head;
  }

  //insert方法是有序的 遍歷列表直到找到第一個值小於等於新插入的值得節點,在這個位置插入
  public void insert(int value) {
    Node current = head;
    current.lock.lock();
    Node next = current.next;
    try {
      while (true) {
        next.lock.lock();
        try {
          if (next == tail || next.value < value) {
            Node node = new Node(value, current, next);
            next.prev = node;
            current.next = node;
              //!!!這裏return要在兩個finally都執行完後纔會執行啊!!!但只是finally裏的.不過要是return換成exit(0)就直接退出了

            return; 
          }
        } finally { current.lock.unlock(); }
        current = next;
        next = current.next;
      }
    } finally { next.lock.unlock(); }
  }

  public int size() {
    Node current = tail;
    int count = 0;

    while (current.prev != head) {
      ReentrantLock lock = current.lock;
      lock.lock();
      try {
        ++count;
        current = current.prev;
      } finally { lock.unlock(); }
    }

    return count;
  }

  public boolean isSorted() {
    Node current = head;
    while (current.next.next != tail) {
      current = current.next;
      if (current.value < current.next.value)
        return false;
    }
    return true;
  }
}

class LinkedList {
  public static void main(String[] args) throws InterruptedException {
    final ConcurrentSortedList list = new ConcurrentSortedList();
    final Random random = new Random();

    class TestThread extends Thread {
      public void run() {
        for (int i = 0; i < 10000; ++i)
          list.insert(random.nextInt());
      }
    }

    class CountingThread extends Thread {
      public void run() {
        while (!interrupted()) {
          System.out.print("\r" + list.size());
          System.out.flush();
        }
      }
    }

    Thread t1 = new TestThread();
    Thread t2 = new TestThread();
    Thread t3 = new CountingThread();
    //注意一下這裏的用法 這裏先join再interrupted的用法
    t1.start(); t2.start(); t3.start();
    t1.join(); t2.join();
    t3.interrupt();
 
    System.out.println("\r" + list.size());
 
    if (list.size() != 20000)
      System.out.println("*** Wrong size!");
 
    if (!list.isSorted())
      System.out.println("*** Not sorted!");
  }
}
  • 26行的 next.lock.lock();鎖住了頭,36行的 next.lock.lock();鎖住了下一個節點. if ( next == tail || next.value < value )判斷兩個節點之間是否是待插入位置,如果不是,在38行的finally解鎖 current.lock.unlock();並繼續遍歷,如果找到待插入位置,33~36行構造新節點並將其插入鏈表後返回.兩把鎖的解鎖操作在倆finally塊中進行
  • 這種方案可以讓多個線程併發的進行鏈表插入操作

條件變量

  • 併發編程經常需要等待某個事件的發生.比如隊列刪除元素前需要等待隊列非空等等
  • 按照下面的模式使用條件變量
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
  while (! « condition is true » ) {
    condition.await();
  }
  //使用共享資源
} finally { lock.unlock(); }
  • 爲何要在一個循環中循環調用await():當另一個線程調用了signal()或signalAll(),意味着對應的條件可能爲真,await()將原子地恢復運行並重新加鎖.從await()返回後需要重新檢查等待的條件是否爲真,必要的話可能再次調用await()並阻塞
  • 哲學家進餐問題新解決方法
class Philosopher extends Thread {

    private boolean eating;
    private Philosopher left;
    private Philosopher right;
    private ReentrantLock table;
    private Condition condition;
    private Random random;
    private int thinkCount;

    public Philosopher ( ReentrantLock table ) {
        eating = false;
        this.table = table;
        condition = table.newCondition();
        random = new Random();
    }

    public void setLeft ( Philosopher left ) {
        this.left = left;
    }

    public void setRight ( Philosopher right ) {
        this.right = right;
    }

    public void run () {
        try {
            while ( true ) {
                think();
                eat();
            }
        }
        catch ( InterruptedException e ) {
        }
    }

    private void think () throws InterruptedException {
        table.lock();
        try {
            eating = false;
            left.condition.signal();
            right.condition.signal();
        } finally {
            table.unlock();
        }
        ++thinkCount;
        if ( thinkCount % 10 == 0 ) {
            System.out.println( "Philosopher " + this + " has thought " + thinkCount + " times" );
        }
        Thread.sleep( 1000 );
    }

    private void eat () throws InterruptedException {
        table.lock();
        try {
            while ( left.eating || right.eating ) {
                condition.await();
            }
            eating = true;
        } finally {
            table.unlock();
        }
        Thread.sleep( 1000 );
    }
}
  • 現在沒有筷子類,現在僅當哲學家的左右鄰座都沒有進餐的時候,他纔可以進餐
  • 當一個哲學家飢餓的時候,他首先鎖住餐桌,這樣其他哲學家無法改變狀態(進餐/思考),然後查看左右的哲學家有沒有在進餐,沒有的話開始進餐並解鎖餐桌,否則調用await(),解鎖餐桌
  • 當一個哲學家進餐結束開始思考的時候,他首先鎖住餐桌並將eating設置爲false,然後通知左鄰右舍可以進餐了,最後解鎖餐桌.
  • 之前的解決方案經常只有一個哲學家能進餐,其他人都持有一根筷子在等,這個方案中當一個哲學家理論上可以進餐,他肯定可以進餐

原子變量

  • 原子變量是無鎖非阻塞算法的基礎
  • volatile是一種低級形式的同步,他的適用場景也越來越少,如果你要使用volatile,可以在atomic包中尋找更適合的工具

2.4 站在巨人的肩膀上

寫入時複製

  • 之前有用到保護性複製,Java標準庫提供了更優雅的現成的方案--CopyOnWriteArrayList,他不是在遍歷列表前進行復制,而是在列表被修改時進行
  • 先將當前容器進行Copy,複製出一個新的容器,然後新的容器裏添加元素,添加完元素之後,再將原容器的引用指向新的容器 所以CopyOnWrite容器也是一種讀寫分離的思想,讀和寫不同的容器。CopyOnWriteArrayList適合使用在讀操作遠遠大於寫操作的場景裏,比如緩存
  • 缺點: 1.內存佔用問題 2.數據一致性問題:CopyOnWrite容器只能保證數據的最終一致性,不能保證數據的實時一致性。所以如果你希望寫入的的數據,馬上能讀到,請不要使用CopyOnWrite容器。
//Downloader.java
  private CopyOnWriteArrayList<ProgressListener> listeners;


  public void addListener(ProgressListener listener) {
    listeners.add(listener);
  }
  public void removeListener(ProgressListener listener) {
    listeners.remove(listener);
  }
  private void updateProgress(int n) {
    for (ProgressListener listener: listeners)
      listener.onProgress(n);
  }

一個完整的程序

  • Q:wiki上出現頻率最高的詞

版本一的並行:生產者和消費者(串行的我略過了)

//生產者 將page加到隊尾
class Parser implements Runnable {
  private BlockingQueue<Page> queue;


  public Parser(BlockingQueue<Page> queue) {
    this.queue = queue;
  }
  
  public void run() {
    try {
      Iterable<Page> pages = new Pages(100000, "enwiki.xml");
      for (Page page: pages)
        queue.put(page);
    } catch (Exception e) { e.printStackTrace(); }
  }
}
//消費者
class Counter implements Runnable {
  private BlockingQueue<Page> queue;
  private Map<String, Integer> counts;
  
  public Counter(BlockingQueue<Page> queue,
                 Map<String, Integer> counts) {
    this.queue = queue;
    this.counts = counts;
  }

  public void run() {
    try {
      while(true) {
        Page page = queue.take();
        if (page.isPoisonPill())
          break;


        Iterable<String> words = new Words(page.getText());
        for (String word: words)
          countWord(word);
      }
    } catch (Exception e) { e.printStackTrace(); }
  }


  private void countWord(String word) {
    Integer currentCount = counts.get(word);
    if (currentCount == null)
      counts.put(word, 1);
    else
      counts.put(word, currentCount + 1);
  }
}

  • 最後創建兩個線程運行
  public static void main(String[] args) throws Exception {
    ArrayBlockingQueue<Page> queue = new ArrayBlockingQueue<Page>(100);
    HashMap<String, Integer> counts = new HashMap<String, Integer>();
 
    Thread counter = new Thread(new Counter(queue, counts));
    Thread parser = new Thread(new Parser(queue));
    long start = System.currentTimeMillis();
 
    counter.start();
    parser.start();
    parser.join();
    queue.put(new PoisonPill());
    counter.join();
    long end = System.currentTimeMillis();
    System.out.println("Elapsed time: " + (end - start) + "ms");
  }
  • 程序解釋:該程序由兩個線程在跑.一個讀取一個分析,性能還不是最高.這裏 ArrayBlockingQueue<Page> queue = new ArrayBlockingQueue<Page>(100);用了一個阻塞的併發隊列來存放讀取到的page.這個併發隊列很適合實現生產者消費者模式,提供了高效的併發方法put()和take(),這些方法會在必要時阻塞:當一個空隊列調用take(一個滿隊列調用put()),程序會阻塞直到隊列變爲非空(非滿)
  • 爲什麼要用阻塞隊列? concurrent包不僅提供了阻塞隊列,還提供了一種容量無限,操作不需等待,非阻塞的隊列ConcurrentLinkedQueue.爲何不用他?關鍵在與生產者和消費者幾乎不會保持相同的速度,當生產者速度快於消費者,生產者越來越大的時候,會撐爆內存.相比之下,阻塞隊列只允許生產者的速度在一定程度上超過消費者的速度,但不會超過很多.

第二個版本:多個消費者

  • 上個版本的解析文件花了10s,統計花了95s,一共花了95s.進一步優化就對統計過程進行並行化,建立多個消費者.(他這裏還是用一個count,不同的消費者都寫這一個count,所以要加鎖)
  private void countWord(String word) {
    lock.lock();
    try {
      Integer currentCount = counts.get(word);
      if (currentCount == null)  counts.put(word, 1);
      else  counts.put(word, currentCount + 1);
    } finally { lock.unlock(); }
  }
  • 運行多個消費者
    ExecutorService executor = Executors.newCachedThreadPool();
    for (int i = 0; i < NUM_COUNTERS; ++i)
      executor.execute(new Counter(queue, counts));
    Thread parser = new Thread(new Parser(queue));
    parser.start();
    parser.join();
    for (int i = 0; i < NUM_COUNTERS; ++i)
      queue.put(new PoisonPill());
    executor.shutdown();
  • 但是一運行發現比串行還慢一半.主要原因就是過多的線程嘗試訪問同一個共享資源,等待的時間比運行的時間還長.改用ConcurrentHashMap(使用了鎖分段)
  //改用 ConcurrentHashMap
  private void countWord(String word) {
    while (true) { //理解一下這裏的循環 如果下面的操作沒有成功的話,就重試
      Integer currentCount = counts.get(word);
      if (currentCount == null) {
        if (counts.putIfAbsent(word, 1) == null) //如果沒有與1關聯 則關聯,有原子性
          break;
      } else if (counts.replace(word, currentCount, currentCount + 1)) {
        break;
      }
    }
  • 這次的測速要好很多,但是沒有理論上的提速.因爲消費者對conuts有一些不必要的競爭,與其所有消費者都共享一個counts,不如每個消費者各自維護一個計數map,再對這些計數map進行合併
class Counter implements Runnable {

  private BlockingQueue<Page> queue;
  private ConcurrentMap<String, Integer> counts;
  private HashMap<String, Integer> localCounts;

  public Counter(BlockingQueue<Page> queue,
                 ConcurrentMap<String, Integer> counts) {
    this.queue = queue;
    this.counts = counts;
    localCounts = new HashMap<String, Integer>();
  }

  public void run() {
    try {
      while(true) {
        Page page = queue.take();
        if (page.isPoisonPill())
          break;
        Iterable<String> words = new Words(page.getText());
        for (String word: words)
          countWord(word);
      }
      //所以計數的那個可以是普通的map 他只在自己的線程裏
      mergeCounts();
    } catch (Exception e) { e.printStackTrace(); }
  }

  private void countWord(String word) {
    Integer currentCount = localCounts.get(word);
    if (currentCount == null)
      localCounts.put(word, 1);
    else
      localCounts.put(word, currentCount + 1);
  }

  private void mergeCounts() {
    for (Map.Entry<String, Integer> e: localCounts.entrySet()) {
      String word = e.getKey();
      Integer count = e.getValue();
      while (true) {
        Integer currentCount = counts.get(word);
        if (currentCount == null) {
          if (counts.putIfAbsent(word, count) == null)
            break;
        } else if (counts.replace(word, currentCount, currentCount + count)) {
          break;
        }
      }
    }
  }
}

第三天總結

  • 1.使用線程池,不要直接創建線程
  • 2.使用CopyOnWriteArrayList讓監聽器相關的代碼更簡單高效
  • 3.使用ArrayBlockingQueue讓生產者和消費者之間高效合作
  • 4.ConcurrentHashMap提供了更好的併發訪問

2.5 複習

  • 線程與鎖的缺點:沒有爲並行提供直接的支持,對於程序員,編程語言層面沒有提供足夠的幫助
  • 應用多線程的難點不在編程,而在於難以測試,多線程的bug很難重現.可維護性更讓人頭疼,如果不能對多線程問題進行可靠的測試,就無法對多線程進行可靠的重構.使用其他不那麼底層的模型會好一些.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章