十四、併發

一、什麼是線程

 多線程程序在較低的層次上擴展了多任務的概念:一個程序同時執行多個任務。通常, 每一個任務稱爲一個線程(thread), 它是線程控制的簡稱。可以同時運行一個以上線程的程 序稱爲多線程程序(multithreaded)。

   那麼,多進程與多線程有哪些區別呢? 本質的區別在於每個進程擁有自己的一整套變 量, 而線程則共享數據。 這聽起來似乎有些風險, 的確也是這樣, 在本章稍後將可以看到這 個問題。然而,共享變量使線程之間的通信比進程之間的通信更有效、更容易。此外, 在有 些操作系統中,與進程相比較, 線程更“ 輕量級”, 創建、撤銷一個線程比啓動新進程的開 銷要小得多。 

 什麼是線程

sleep方法可以拋出一個 InterrruptedException 異常。

下面是在一個單獨的線程中執行一個任務的簡單過程:

1 ) 將任務代碼移到實現了 Runnable 接口的類的 run方法中。這個接口非常簡單,只有 一個方法:

public interface Runnable { void run(); } 由於 Runnable 是一個函數式接口,可以用 lambda 表達式建立一個實例: Runnable r = () -> { taskcode};

2 ) 由 Runnable 創建一個 Thread 對象: Thread t = new Thread(r);

3 ) 啓動線程: t.start();

package com;
import java.util.*;
import java.lang.*;
import java.io.*;
public class Welcome{
    class a implements Runnable{
    	public void run() {
    		System.out.println("a");
    	}
    }
    public void x() {
    	 Runnable i=new a();
 		Thread thread=new Thread(new a());
 		thread.start();
    }
    public static void main(String []args){
         new Welcome().x();
    	   
    }
}

二、中斷線程

  當線程的 run 方法執行方法體中最後一條語句後, 並經由執行 return 語句返冋時, 或者出現了在方法中沒有捕獲的異常時,線程將終止。然而,interrupt 方法可以用來請求終止線程。當對一個線程調用 interrupt 方法時,線程的中斷狀態將被置位。這是每一個線程都具有 的 boolean 標誌。每個線程都應該不時地檢査這個標誌, 以判斷線程是否被中斷。 要想弄清中斷狀態是否被置位,首先調用靜態的 Thread.currentThread方法獲得當前線 程,然後調用 islnterrupted方法:

while(!Thread.currentThread().isInterrupted()) {
 			
 		}

但是, 如果線程被阻塞, 就無法檢測中斷狀態。這是產生 InterruptedExceptioii 異常的地 方。當在一個被阻塞的線程(調用 sleep 或 wait) 上調用 interrupt方法時,阻塞調用將會被 Interrupted Exception 異常中斷。

沒有任何語言方面的需求要求一個被中斷的線程應該終止。中斷一個線程不過是引起它 的注意。被中斷的線程可以決定如何響應中斷。某些線程是如此重要以至於應該處理完異常 後, 繼續執行,而不理會中斷。但是,更普遍的情況是,線程將簡單地將中斷作爲一個終止 的請求:

Runnable r = () -> {
 try { 
while (!Thread.currentThread().islnterrupted0 && more work todo) 
{ do morework }
 } 
catch(InterruptedException e) 
{ // thread was interr叩ted during sleep or wait } 
finally
 { cleanup,if required } // exiting the run method terminates the thread } 

interrupt()不能中斷在運行中的線程,它只能改變中斷狀態而已。

interruped用法

如果在每次工作迭代之後都調用 sleep方法(或者其他的可中斷方法),islnterrupted 檢測 既沒有必要也沒有用處。如果在中斷狀態被置位時調用 sleep方法,它不會休眠。相反,它 將清除這一狀態(丨)並拋出 IntemiptedException。因此, 如果你的循環調用 sleep,不會檢 測中斷狀態。相反,要如下所示捕獲 InterruptedException 異常:

Runnable r = () -> {
 try { 
while more work todo) 
{ do morework 
  Thread.sleep();}
 } 
catch(InterruptedException e) 
{ // thread was interrupted during sleep or wait } 
finally
 { cleanup,if required } // exiting the run method terminates the thread } 

 有兩個非常類似的方法,interrupted 和 islnterrupted。Interrupted 方法是一個靜態 方法, 它檢測當前的線程是否被中斷。 而且, 調用 interrupted 方法會清除該線程的中斷狀態。另一方面,islnterrupted 方法是一個實例方法,可用來檢驗是否有線程被中斷。調 用這個方法不會改變中斷狀態。

在很多發佈的代碼中會發現 InterruptedException 異常被抑制在很低的層次上。有兩種合理 的選擇:

•在 catch 子句中調用 Thread.currentThread().interrupt() 來設置中斷狀態。於是,調用者 可以對其進行檢測。 

void mySubTask() {
try { sleep(delay); } 
catch (InterruptedException e) 
{ Thread.currentThread()-interrupt(); }

•或者,更好的選擇是,用 throws InterruptedException標記你的方法, 不採用 try語句 塊捕獲異常。於是,調用者(或者,最終的 run 方法)可以捕獲這一異常。 

void mySubTask() throws InterruptedException
{sleep(delay);}

三、線程狀態

線程可以有如下 6 種狀態:

•New (新創建)

•Runnable (可運行)

•Blocked (被阻塞)

•Waiting (等待)

•Timed waiting (計時等待)

•Terminated (被終止) 

 當用 new 操作符創建一個新線程時,如 new Thread(r),該線程還沒有開始運行。這意味 着它的狀態是 new。當一個線程處於新創建狀態時,程序還沒有開始運行線程中的代碼。在 線程運行之前還有一些基礎工作要做。

可運行線程

一旦調用 start 方法,線程處於 runnable 狀態。一個可運行的線桿可能正在運行也可能沒 有運行, 這取決於操作系統給線程提供運行的時間。

搶佔式調度系 統給每一個可運行線程一個時間片來執行任務。當時間片用完,操作系統剝奪該線程的運行 權, 並給另一個線程運行機會(見圖 14-4 )。當選擇下一個線程時, 操作系統考慮線程的優 先級。

被阻塞線程和等待線程

當線程處於被阻塞或等待狀態時,它暫時不活動。它不運行任何代碼且消耗最少的資 源。直到線程調度器重新激活它。細節取決於它是怎樣達到非活動狀態的。 

•當一個線程試圖獲取一個內部的對象鎖(而不是java.util.concurrent 庫中的鎖),而該 鎖被其他線程持有, 則該線程進人阻塞狀態(我們在 14.5.3 節討論java.util.concurrent 鎖,在 14.5.5 節討論內部對象鎖)。當所有其他線程釋放該鎖,並且線程調度器允許 本線程持有它的時候,該線程將變成非阻塞狀態。

■ 當線程等待另一個線程通知調度器一個條件時,它自己進入等待狀態。我們在第 14.5.4 節來討論條件。在調用Object.wait方法或 Thread.join方法, 或者是等待 java, util.concurrent 庫中的 Lock 或 Condition 時, 就會出現這種情況。實際上,被阻塞狀態 與等待狀態是有很大不同的。

•有幾個方法有一個超時參數。調用它們導致線程進人計時等待(timed waiting) 狀 態。這一狀態將一直保持到超時期滿或者接收到適當的通知。帶有超時參數的方法有 Thread.sleep 和 Object.wait、Thread.join、Lock.tryLock 以及 Condition.await 的計時版。

  圖 14-3 展示了線程可以具有的狀態以及從一個狀態到另一個狀態可能的轉換。當一個線 程被阻塞或等待時(或終止時),另一個線程被調度爲運行狀態。當一個線程被重新激活(例 如, 因爲超時期滿或成功地獲得了一個鎖),調度器檢查它是否具有比當前運行線程更高的優 先級。如果是這樣,調度器從當前運行線程中挑選一個, 剝奪其運行權,選擇一個新的線程 運行。

被終止的線程

線程因如下兩個原因之一而被終止:

•因爲run方法正常退出而自然死亡。
•因爲一個沒有捕獲的異常終止了 run方法而意外死亡

join的使用

四、線程屬性

下面將討論線程的各種屬性,其中包括:線程優先級、守護線程、線程組以及處理未捕 獲異常的處理器。

線程優先級

在 Java 程序設計語言中,每一個線程有一個優先級。默認情況下,一+線程繼承它的父 線程的優先級。可以用 setPriority 方法提高或降低任何一個線程的優先級。可以將優先級設 置爲在 MIN_PRIORITY (在 Thread類中定義爲 1 ) 與 MAX_PRIORITY (定義爲 10 ) 之間的 任何值。NORM_PRIORITY 被定義爲 5。

 Java 線程的優 先級被映射到宿主機平臺的優先級上,優先級個數也許更多,也許更少。

守護線程

t.setDaemon(true); 將線程轉換爲守護線程(daemon thread)。這樣一個線程沒有什麼神奇。守護線程的唯一用途 是爲其他線程提供服務。計時線程就是一個例子,它定時地發送“ 計時器嘀嗒” 信號給其他 線程或清空過時的高速緩存項的線程。當只剩下守護線程時, 虛擬機就退出了,由於如果只 剩下守護線程, 就沒必要繼續運行程序了。 守護線程有時會被初學者錯誤地使用, 他們不打算考慮關機(shutdown) 動作。但是, 這是很危險的。守護線程應該永遠不去訪問固有資源, 如文件、 數據庫,因爲它會在任何時 候甚至在一個操作的中間發生中斷。

未捕獲異常處理器

  線程的 run方法不能拋出任何受查異常, 但是,非受査異常會導致線程終止。在這種情 況下,線程就死亡了。

  但是,不需要任何 catch子句來處理可以被傳播的異常。相反,就在線程死亡之前, 異 常被傳遞到一個用於未捕獲異常的處理器。 該處理器必須屬於一個實現 Thread.UncaughtExceptionHandler 接口的類。這個接口只有— 個方法。

void uncaughtException(Thread t, Throwable e)

可以用 setUncaughtExceptionHandler方法爲任何線程安裝一個處理器。也可以用 Thread 類的靜態方法 setDefaultUncaughtExceptionHandler 爲所有線程安裝一個默認的處理器。替換 處理器可以使用日誌 API 發送未捕獲異常的報告到日誌文件。 如果不安裝默認的處理器, 默認的處理器爲空。但是, 如果不爲獨立的線程安裝處理 器,此時的處理器就是該線程的 ThreadGroup 對象。

註釋: 線程組是一個可以統一管理的線程集合。默認情況下,創建的所有線程屬於相同的線程組, 但是, 也可能會建立其他的組。現在引入了更好的特性用於線程集合的操作, 所以建議不要在自己的程序中使用線程組。

 

ThreadGroup 類實現 Thread.UncaughtExceptionHandler 接口。它的 uncaughtException方 法做如下操作:

1 ) 如果該線程組有父線程組, 那麼父線程組的 uncaughtException方法被調用。

2 ) 否則, 如果 Thread.getDefaultExceptionHandler方法返回一個非空的處理器, 則調用 該處理器。

3 ) 否則,如果 Throwable 是 ThreadDeath 的一個實例, 什麼都不做。

4 ) 否則,線程的名字以及 Throwable 的棧軌跡被輸出到 System.err 上。 這是你在程序中肯定看到過許多次的棧軌跡

 

yield方法

五、同步

  在大多數實際的多線程應用中, 兩個或兩個以上的線程需要共享對同一數據的存取。如果兩個線程存取相同的對象, 並且每一個線程都調用了一個修改該對象狀態的方法,將會發 生什麼呢? 可以想象,線程彼此踩了對方的腳。根據各線程訪問數據的次序,可能會產生i化 誤的對象。這樣一個情況通常稱爲競爭條件(race condition)。


鎖對象

  有兩種機制防止代碼塊受併發訪問的干擾。Java語言提供一個 synchronized關鍵字達 到這一目的,並且 Java SE 5.0引入了 ReentrantLock 類。synchronized 關鍵字自動提供一個 鎖以及相關的“ 條件。

 用 ReentrantLock 保護代碼塊的基本結構如下:

myLock.lock(); // a ReentrantLock object 
try { critical section } 
finally 
{ myLock.unlock();// make sure the lock is unlocked even if an exception isthrown } 

  這一結構確保任何時刻只有一個線程進人臨界區。一旦一個線程封鎖了鎖對象, 其他任 何線程都無法通過 lock語句。當其他線程調用 lock 時,它們被阻塞,直到第一個線程釋放 鎖對象。

  警告: 把解鎖操作括在 finally 子句之內是至關重要的。如果在臨界區的代碼拋出異常, 鎖必須被釋放。否則, 其他線程將永遠阻塞。

  如果使用鎖, 就不能使用帶資源的 try語句。首先, 解鎖方法名不是 close。不過, 即使將它重命名, 帶資源的 try語句也無法正常工作。它的首部希望聲明一個新變量。但 是如果使用一個鎖, 你可能想使用多個線程共享的那個變量(而不是新變量) 。 

  

同步,簡單地理解,就是協同步調,一個完成了,另一個才能開始。

異步,就是你說的不同步,就是互不干擾,各幹各的,多個線程可能同時進行

  鎖是可重入的, 因爲線程可以重複地獲得已經持有的鎖。鎖保持一個持有計數(hold count) 來跟蹤對 lock 方法的嵌套調用。線程在每一次調用 lock 都要調用 unlock 來釋放鎖。 由於這一特性, 被一個鎖保護的代碼可以調用另一個使用相同的鎖的方法  

  通常, 可能想要保護需若干個操作來更新或檢查共享對象的代碼塊。要確保這些操作完 成後, 另一個線程才能使用相同對象。 

條件對象

  通常, 線程進人臨界區,卻發現在某一條件滿足之後它才能執行。要使用一個條件對 象來管理那些已經獲得了一個鎖但是卻不能做有用工作的線程。在這一節裏, 我們介紹 Java 庫中條件對象的實現。

  等待獲得鎖的線程和調用 await 方法的線程存在本質上的不同。一旦一個線程調用 await 方法,它進人該條件的等待集。當鎖可用時,該線程不能馬上解除阻塞。相反,它處於阻塞 狀態,直到另一個線程調用同一條件上的 signalAll 方法時爲止。 這一調用重新激活因爲這一條件而等待的所有線程。當這些線程從等待集當中移出時, 它們再次成爲可運行的,調度器將再次激活它們。同時,它們將試圖重新進人該對象。一旦 鎖成爲可用的,它們中的某個將從 await 調用返回, 獲得該鎖並從被阻塞的地方繼續執行。 此時, 線程應該再次測試該條件。由於無法確保該條件被滿足— —signalAll 方法僅僅是通知正在等待的線程 :此時有可能已經滿足條件, 值得再次去檢測該條件。 

  另一個方法 signal, 則是隨機解除等待集中某個線程的阻塞狀態。這比解除所有線程的 阻塞更加有效,但也存在危險。如果隨機選擇的線程發現自己仍然不能運行, 那麼它再次被 阻塞。如果沒有其他線程再次調用 signal, 那麼系統就死鎖了。  當一個線程擁有某個條件的鎖時, 它僅僅可以在該條件上調用 await、signalAll 或 signal 方法。

例:

package synch;

import java.util.*;
import java.util.concurrent.locks.*;

/**
 * A bank with a number of bank accounts that uses locks for serializing access.
 * @version 1.30 2004-08-01
 * @author Cay Horstmann
 */
public class Bank
{
   private final double[] accounts;
   private Lock bankLock;
   private Condition sufficientFunds;

   /**
    * Constructs the bank.
    * @param n the number of accounts
    * @param initialBalance the initial balance for each account
    */
   public Bank(int n, double initialBalance)
   {
      accounts = new double[n];
      Arrays.fill(accounts, initialBalance);
      bankLock = new ReentrantLock();
      sufficientFunds = bankLock.newCondition();
   }

   /**
    * Transfers money from one account to another.
    * @param from the account to transfer from
    * @param to the account to transfer to
    * @param amount the amount to transfer
    */
   public void transfer(int from, int to, double amount) throws InterruptedException
   {
      bankLock.lock();
      try
      {
         while (accounts[from] < amount)
            sufficientFunds.await();
         System.out.print(Thread.currentThread());
         accounts[from] -= amount;
         System.out.printf(" %10.2f from %d to %d", amount, from, to);
         accounts[to] += amount;
         System.out.printf(" Total Balance: %10.2f%n", getTotalBalance());
         sufficientFunds.signalAll();
      }
      finally
      {
         bankLock.unlock();
      }
   }

   /**
    * Gets the sum of all account balances.
    * @return the total balance
    */
   public double getTotalBalance()
   {
      bankLock.lock();
      try
      {
         double sum = 0;

         for (double a : accounts)
            sum += a;

         return sum;
      }
      finally
      {
         bankLock.unlock();
      }
   }

   /**
    * Gets the number of accounts in the bank.
    * @return the number of accounts
    */
   public int size()
   {
      return accounts.length;
   }
}


package synch;

/**
 * This program shows how multiple threads can safely access a data structure.
 * @version 1.31 2015-06-21
 * @author Cay Horstmann
 */
public class SynchBankTest
{
   public static final int NACCOUNTS = 100;
   public static final double INITIAL_BALANCE = 1000;
   public static final double MAX_AMOUNT = 1000;
   public static final int DELAY = 10;
   
   public static void main(String[] args)
   {
      Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
      for (int i = 0; i < NACCOUNTS; i++)
      {
         int fromAccount = i;
         Runnable r = () -> {
            try
            {
               while (true)
               {
                  int toAccount = (int) (bank.size() * Math.random());
                  double amount = MAX_AMOUNT * Math.random();
                  bank.transfer(fromAccount, toAccount, amount);
                  Thread.sleep((int) (DELAY * Math.random()));
               }
            }
            catch (InterruptedException e)
            {
            }            
         };
         Thread t = new Thread(r);
         t.start();
      }
   }
}

synchronized關鍵字

在進一步深人之前,總結一下 有關鎖和條件的關鍵之處:

•鎖用來保護代碼片段, 任何時刻只能有一個線程執行被保護的代碼。

•鎖可以管理試圖進入被保護代碼段的線程。

•鎖可以擁有一個或多個相關的條件對象。

•每個條件對象管理那些已經進入被保護的代碼段但還不能運行的線程

  從 1.0 版開始,Java 中的每一個對象都有一個內部鎖。如果一個方法用 synchronized關鍵字聲明,那麼對象的鎖 將保護整個方法。也就是說,要調用該方法,線程必須獲得內部的對象鎖

例,下面兩段代碼等價:

public synchronized void method() { method body }

public void method(){

this.intrinsicLock.lock();

try{

}finally{
    this.intrinsicLock.unlock();
}

}

內部對象鎖只有一個相關條件。wait 方法添加一個線程到等待集中,notifyAU /notify方 法解除等待線程的阻塞狀態。換句話說,調用 wait 或 notityAll 等價於

intrinsicCondition.await(); intrinsicCondition.signalAll()

每一個對象有一個內部鎖, 並且該鎖有一個內部條件。由鎖來管理那些試圖進入 synchronized 方法的線程,由條件來管理那些調用 wait 的線程。 

 

將靜態方法聲明爲synchronized也是合法的。如果調用這種方法,該方法獲得相關的類對 象的內部鎖。例如,如果 Bank類有一個靜態同步的方法,那麼當該方法被調用時,Bank.class 對象的鎖被鎖住。因此,沒有其他線程可以調用同一個類的這個或任何其他的同步靜態方法。 內部鎖和條件存在一些侷限。包括:

•不能中斷一個正在試圖獲得鎖的線程。

•試圖獲得鎖時不能設定超時。

•每個鎖僅有單一的條件, 可能是不夠的。

    在代碼中應該使用哪一種? Lock 和 Condition 對象還是同步方法?下面是一些建議:

•最好既不使用 Lock/Condition 也不使用 synchronized 關鍵字。在許多情況下你可以使 用java.util.concurrent 包中的一種機制,它會爲你處理所有的加鎖。

•如果 synchronized 關鍵字適合你的程序,那麼請儘量使用它,這樣可以減少編寫的代 碼數量,減少出錯的機率。程序清單 14-8 給出了用同步方法實現的銀行實例。

•如果特別需要 Lock/Condition結構提供的獨有特性時,才使用Lock/Condition.

例:

package synch2;

/**
 * This program shows how multiple threads can safely access a data structure,
 * using synchronized methods.
 * @version 1.31 2015-06-21
 * @author Cay Horstmann
 */
public class SynchBankTest2
{
   public static final int NACCOUNTS = 100;
   public static final double INITIAL_BALANCE = 1000;
   public static final double MAX_AMOUNT = 1000;
   public static final int DELAY = 10;

   public static void main(String[] args)
   {
      Bank bank = new Bank(NACCOUNTS, INITIAL_BALANCE);
      for (int i = 0; i < NACCOUNTS; i++)
      {
         int fromAccount = i;
         Runnable r = () -> {
            try
            {
               while (true)
               {
                  int toAccount = (int) (bank.size() * Math.random());
                  double amount = MAX_AMOUNT * Math.random();
                  bank.transfer(fromAccount, toAccount, amount);
                  Thread.sleep((int) (DELAY * Math.random()));
               }
            }
            catch (InterruptedException e)
            {
            }
         };
         Thread t = new Thread(r);
         t.start();
      }
   }
}

同步堵塞

正如剛剛討論的,每一個 Java 對象有一個鎖。線程可以通過調用同步方法獲得鎖。還有 另一種機制可以獲得鎖,通過進入一個同步阻塞。當線程進入如下形式的阻塞: synchronized (obj) // this is the syntax for a synchronized block { critical section }

於是它獲得 Obj 的鎖。 有時會發現“ 特殊的” 鎖,例如:

public class Bank 
{ private double[] accounts; 
private Object lock = new Object() ; 
public void transfer(int from, int to, int amount)
 { 
synchronized (lock) // an ad-hoc lock
 { accounts[from] -= amount; accounts[to] += amount; } 
   System.out.print1n(.. } 
}

 

在此,lock 對象被創建僅僅是用來使用每個 Java 對象持有的鎖。 有時程序員使用一個對象的鎖來實現額外的原子操作, 實際上稱爲客戶端鎖定。如你所見,客戶端鎖定是非常脆弱 的,通常不推薦使用。

監視器概念

  用 Java 的術語來講,監視器具有如下特性:

•監視器是隻包含私有域的類。

•每個監視器類的對象有一個相關的鎖。

•使用該鎖對所有的方法進行加鎖。

  換句話說,如果客戶端調用 obj.meth0d(), 那麼 obj 對象的鎖是在方法調用開始時自動獲得,並且當方法返回時自動釋放該鎖。因爲所有 的域是私有的,這樣的安排可以確保一個線程在對對象操作時, 沒有其他線程能訪問 該域。

•該鎖可以有任意多個相關條件

然而, 在下述的 3 個方面 Java 對象不同於監視器, 從而使得線程的安全性下降:

•域不要求必須是 private。

•方法不要求必須是 synchronized。

•內部鎖對客戶是可用的。 

 

Volatile域

  有時,僅僅爲了讀寫一個或兩個實例域就使用同步, 顯得開銷過大了。“ 如果向一個變量寫入值, 而這個變量接下 來可能會被另一個線程讀取, 或者,從一個變量讀值, 而這個變量可能是之前被另一個 線程寫入的, 此時必須使用同步。

  volatile關鍵字爲實例域的同步訪問提供了一種免鎖機制。如果聲明一個域爲 volatile, 那麼編譯器和虛擬機就知道該域是可能被另一個線程併發更新的。

private volatile boolean done;

   Volatile 變量不能提供原子性。例如, 方法 public void flipDone() { done = !done; } // not atomic 不能確保翻轉域中的值。不能保證讀取、翻轉和寫入不被中斷。

volatile詳解

final變量

  還有一種情況可以安全地訪問一個共享域, 即這個域聲明爲 final 時。考慮以下聲明:

final Map<String, Double>accounts = new HashKap<>();

其他線程會在構造函數完成構造之後纔看到這個 accounts 變量。 如果不使用 final,就不能保證其他線程看到的是 accounts 更新後的值,它們可能都只是 看到 null, 而不是新構造的 HashMap。 當然,對這個映射表的操作並不是線程安全的。如果多個線程在讀寫這個映射表,仍然需要進行同步。

原子性

  假設對共享變量除了賦值之外並不完成其他操作,那麼可以將這些共享變量聲明爲 volatile。java.util.concurrent.atomic 包中有很多類使用了很高效的機器級指令(而不是使用 鎖)來保證其他操作的原子性。 例如, Atomiclnteger 類提供了方法 incrementAndGet 和 decrementAndGet, 它們分別以原子方式將一個整數自增或自減。例如,可以安全地生成一個 數值序列,如下所示: 

public static AtomicLong nextNumber=new AtomicLong();
long id=nextNumber.incrementAndGet();

incrementAndGet 方法以原子方式將 AtomicLong 自增, 並返回自增後的值。也就是說, 獲得值、增 1 並設置然後生成新值的操作不會中斷。可以保證即使是多個線程併發地訪問同一個實例,也會計算並返回正確的值。 

例,設希望跟蹤不同線程觀察的最大值:

AtomicLong largest=new AtomicLong();
          do {
        	  oldValue=largest.get();
        	  newValue=Math.max(oldvalue,observed);
          }while(!largest.compareAndSet(oldValue, newValue));

在 Java SE 8中,不再需要編寫這樣的循環樣板代碼。實際上,可以提供一個 lambda 表 達式更新變量,它會爲你完成更新。對於這個例子,我們可以調用:

largest.updateAndGet(x -> Math.max(x, observed)); 或

largest.accumulateAndCet(observed, Math::max); accumulateAndGet方法利用一個二元操作符來合併原子值和所提供的參數。 還有 getAndUpdate 和 getAndAccumulate 方法可以返回原值

死鎖

  有可能會因爲每一個線程要等待更多的錢款存人而導致所有線程都被阻塞。這樣的狀態 稱爲死鎖(deadlock)。

 當程序掛起時, 鍵入 CTRL+\, 將得到一個所有線程的列表。每一個線程有一個棧 蹤跡, 告訴你線程被阻塞的位置。

 還有一種很容易導致死鎖的情況: 在 SynchBankTest 程序中, 將 signalAll方法轉換 爲 signal, 會發現該程序最終會掛起(將 NACCOUNTS 設爲 10 可以更快地看到結果) 。 signalAll 通知所有等待增加資金的線程, 與此不同的是 signa丨方法僅僅對一個線程解鎖。遺憾的是,Java 編程語言中沒有任何東西可以避免或打破這種死鎖現象。必須仔細設計 程序,以確保不會出現死鎖。 

 線程局部變量

  前面幾節中, 我們討論了在線程間共享變量的風險。有時可能要避免共享變量, 使用 ThreadLocal 輔助類爲各個線程提供各自的實例。 例如,SimpleDateFormat 類不是線程安全的。 假設有一個靜態變量。

如果兩個線程都執行以下操作: String dateStamp = dateFormat.format(new Date()); 結果可能很混亂,因爲 dateFormat 使用的內部數據結構可能會被併發的訪問所破壞。當 然可以使用同步,但開銷很大; 或者也可以在需要時構造一個局部 SimpleDateFormat 對象, 不過這也太浪費了。

要爲每個線程構造一個實例,可以使用以下代碼:

public static final ThreadLocal<SimpleDateFormat> dateformat=
new ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-mm-dd"));

    要訪問具體的格式化方法,可以調用:

String dateStamp = dateFormat.get().format(new Date()); 在一個給定線程中首次調用 get 時, 會調用 initialValue方法。在此之後,get方法會返回 屬於當前線程的那個實例。

  在多個線程中生成隨機數也存在類似的問題。java. .util.Random 類是線程安全的。但是如 果多個線程需要等待一個共享的隨機數生成器, 這會很低效。 可以使用 ThreadLocal 輔助類爲各個線程提供一個單獨的生成器, 不過 Java SE 7 還另外 提供了一個便利類。只需要做以下調用:

int random = ThreadLocalRandom.current().nextlnt(upperBound):

ThreadLocalRandom.current()調用會返回特定於當前線程的 Random 類實例。

 

鎖測試與超時

  線程在調用 lock 方法來獲得另一個線程所持有的鎖的時候,很可能發生阻塞。應該更加 謹慎地申請鎖。tryLock方法試圖申請一個鎖, 在成功獲得鎖後返回 true, 否則, 立即返回 false, 而且線程可以立即離開去做其他事情:

if(mylock.tryLock()){
    try{}
    finally{mylock.unlock();}
}

  可以調用 tryLock 時,使用超時參數,像這樣:

if (myLock.tryLock(100, TineUnit.MILLISECONDS)) ...

TimeUnit 是一 枚舉類型,可以取的值包括 SECONDS、MILLISECONDS, MICROSECONDS 和 NANOSECONDS。 lock 方法不能被中斷。如果一個線程在等待獲得一個鎖時被中斷,中斷線程在獲得鎖之 前一直處於阻塞狀態。如果出現死鎖,那麼,lock 方法就無法終止。 然而, 如果調用帶有用超時參數的 tryLock, 那麼如果線程在等待期間被中斷,將拋出 InterruptedException 異常。這是一個非常有用的特性,因爲允許程序打破死鎖。 也可以調用 locklnterruptibly方法。它就相當於一個超時設爲無限的 tryLock 方法。 在等待一個條件時, 也可以提供一個超時: myCondition.await(100, TineUniBILLISECONDS)) 如果一個線程被另一個線程通過調用 signalAU 或 signal 激活, 或者超時時限已達到,或 者線程被中斷, 那麼 await 方法將返回。 如果等待的線程被中斷, await 方法將拋出一個 InterruptedException 異常。在你希望出 現這種情況時線程繼續等待(可能不太合理), 可以使用awaitUninterruptibly方法代替 await

讀寫鎖

  如果很多線程從一個數據結構讀取數據而很少線程修改其中數 據的話, 後者是十分有用的。在這種情況下, 允許對讀者線程共享訪問是合適的。當然,寫 者線程依然必須是互斥訪問的。 

  下面是使用讀 / 寫鎖的必要步驟:

1 ) 構造一個 ReentrantReadWriteLock 對象:

private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock():

2 ) 抽取讀鎖和寫鎖:

private Lock readLock = rwl.readLock(); private Lock writeLock = rwl.writeLock();

3 ) 對所有的獲取方法加讀鎖:

public double getTotalBalance()

{

    readLock.lock(); try { . . . } finally { readLock.unlock(); }

}

4 ) 對所有的修改方法加寫鎖:

public void transfer(. . .)

{

    writeLock.lock(); try { . . . } finally { writeLock.unlock(); }

}

爲什麼棄用stop和suspend方法

  stop方法天生就不安全,經驗證明 suspend方法 會經常導致死鎖.當線程要終止另一個線程時, 無法知道什麼時候調用stop方法是安全的, 什麼時候導致 對象被破壞。因此,該方法被棄用了。在希望停止線程的時候應該中斷線程, 被中斷的線程 會在安全的時候停止.

接下來, 看看 suspend方法有什麼問題。與 stop不同,suspend不會破壞對象。但是, 如果用 suspend 掛起一個持有一個鎖的線程, 那麼,該鎖在恢復之前是不可用的。如果調用 suspend方法的線程試圖獲得同一個鎖, 那麼程序死鎖: 被掛起的線程等着被恢復,而將其 掛起的線程等待獲得鎖.

六、阻塞隊列

  對於許多線程問題, 可以通過使用一個或多個隊列以優雅且安全的方式將其形式化。生 產者線程向隊列插人元素, 消費者線程則取出它們。使用隊列,可以安全地從一個線程向另 一個線程傳遞數據。例如,考慮銀行轉賬程序, 轉賬線程將轉賬指令對象插入一個隊列中, 而不是直接訪問銀行對象。另一個線程從隊列中取出指令執行轉賬。只有該線程可以訪問該 銀行對象的內部。因此不需要同步。(當然, 線程安全的隊列類的實現者不能不考慮鎖和條 件,但是, 那是他們的問題而不是你的問題。)

  當試圖向隊列添加元素而隊列已滿, 或是想從隊列移出元素而隊列爲空的時候, 阻塞隊 列(blocking queue) 導致線程阻塞。在協調多個線程之間的合作時,阻塞隊列是一個有用的工具。

    阻塞隊列方法分爲以下 3類, 這取決於當隊列滿或空時它們的響應方式。如果將隊列當 作線程管理工具來使用, 將要用到 put 和 take 方法。當試圖向滿的隊列中添加或從空的隊列 中移出元素時,add、 remove 和 element 操作拋出異常。當然,在一個多線程程序中, 隊列會 在任何時候空或滿, 因此,一定要使用 offer、poll 和 peek方法作爲替代。這些方法如果不能 完成任務,只是給出一個錯誤提示而不會拋出異常。

    註釋: poll和 peek 方法返回空來指示失敗。因此,向這些隊列中插入 null 值是非法的。 

     LinkedBlockingQueue 的容量是沒有上邊界的,但是,也可以選擇指定最大容量。LinkedBlockingDeque 是一個雙端 的版本。ArrayBlockingQueue 在構造時需要指定容量,並且有一個可選的參數來指定是否需 要公平性。若設置了公平參數, 則那麼等待了最長時間的線程會優先得到處理。通常,公平 性會降低性能,只有在確實非常需要時才使用它。 PriorityBlockingQueue 是一個帶優先級的隊列, 而不是先進先出隊列。元素按照它們的 優先級順序被移出。該隊列是沒有容量上限,但是,如果隊列是空的, 取元素的操作會阻 塞

  JavaSE 7增加了一個 TransferQueue 接口,允許生產者線程等待, 直到消費者準備就緒 可以接收一個元素。如果生產者調用 q.transfer(iteni); 這個調用會阻塞, 直到另一個線程將元素(item) 刪除。LinkedTransferQueue 類實現了這個接口。 

  例:

 

 

 

七、線程安全的集合

高效的映射、集和隊列

  java.util.concurrent 包提供了映射、 有序集和隊列的高效實現:ConcurrentHashMap、 ConcurrentSkipListMap > ConcurrentSkipListSet 和 ConcurrentLinkedQueue。 這些集合使用複雜的算法,通過允許併發地訪問數據結構的不同部分來使競爭極小化。 與大多數集合不同,size方法不必在常量時間內操作。確定這樣的集合當前的大小通常 需要遍歷。 

   而在迭代時,ConcurrentHashMap使用了不同於傳統集合的快速失敗迭代器(見之前的文章《JAVA API備忘---集合》)的另一種迭代方式,我們稱爲弱一致迭代器。在這種迭代方式中,當iterator被創建後集合再發生改變就不再是拋出ConcurrentModificationException,取而代之的是在改變時new新的數據從而不影響原有的數據,iterator完成後再將頭指針替換爲新的數據,這樣iterator線程可以使用原來老的數據,而寫線程也可以併發的完成改變,更重要的,這保證了多個線程併發執行的連續性和擴展性,是性能提升的關鍵。

  集合返回弱一致性(weakly consistent) 的迭代器。這意味着迭代器不一定能反映出它 們被構造之後的所有的修改,但是,它們不會將同一個值返回兩次,也不會拋出 Concurrent ModificationException 異常。 註釋:與之形成對照的是, 集合如果在迭代器構造之後發生改變,java.util 包中的迭代器 將拋出一個 ConcurrentModificationException 異常。  

   併發的散列映射表, 可高效地支持大量的讀者和一定數量的寫者。默認情況下,假定 可以有多達 16 個寫者線程同時執行。可以有更多的寫者線程,但是, 如果同一時間多於 16 個,其他線程將暫時被阻塞。可以指定更大數目的構造器,然而,恐怕沒有這種必要。併發散列映射將桶組織 爲樹, 而不是列表,鍵類型實現了 Comparable, 從而可以保證性能爲 O(log(n))。 

 

映射條目的原子更新

  顯然,下面的 代碼不是線程安全的: 

Long oldValue = map.get(word); 
Long newValue = oldValue == null ? 1: oldValue + 1 ; 
map.put(word, newValue); // Error-might not replace oldValue 

有些程序員很奇怪爲什麼原本線程安全的數據結構會允許非線程安全的操作。不 過有兩種完全不同的情況。如果多個線程修改一個普通的 HashMap,它們會破壞內部結 構(一個鏈表數組) 。有些鏈接可能丟失, 或者甚至會構成循環,使得這個數據結構不再 可用。對於 ConcurrentHashMap 絕對不會發生這種情況。在上面的例子中,get 和 put 代 碼不會破壞數據結構。不過,由於操作序列不是原子的,所以結果不可預知。 

 傳統的做法是使用 replace 操作,它會以原子方式用一個新值替換原值,前提是之前沒有 其他線程把原值替換爲其他值。必須一直這麼做, 直到 replace 成功。

do{
oldValue = map.get(word);
newValue = oldValue = null ? 1 : oldValue + 1; } 
while (!map.replace(word, oldValue, newValue);

調用 compute方法時可以提供 一個鍵和一個計算新值的函數。這個函數接收鍵和相關聯的值(如果沒有值,則爲 mill), 它 會計算新值。例如,可以如下更新一個整數計數器的映射:

map.compute(word, (k, v) -> v = null ? 1: v +1 )

另外還有 computelfPresent 和 computelfAbsent方法,它們分別只在已經有原值的情況下計 算新值,或者只有沒有原值的情況下計算新值.

首次增加一個鍵時通常需要做些特殊的處理。利用 merge 方法可以非常方便地做到這一 點。這個方法有一個參數表示鍵不存在時使用的初始值。否則, 就會調用你提供的函數來結 合原值與初始值。(與 compute 不同,這個函數不處理鍵。)
 map.merge(word, 1L, (existingValue, newValue) -> existingValue + newValue);

或者,更簡單地可以寫爲:

map.merge(word, 1L, Long::sum); 

如果傳入 compute 或 merge 的函數返回 null, 將從映射中刪除現有的條目.

 

對併發散列映射的批操作

Java SE 8 爲併發散列映射提供了批操作,即使有其他線程在處理映射,這些操作也能安 全地執行。批操作會遍歷映射,處理遍歷過程中找到的元素。無須凍結當前映射的快照。除非你恰好知道批操作運行時映射不會被修改,否則就要把結果看作是映射狀態的一個近似。 有 3 種不同的操作:

•搜索(search) 爲每個鍵或值提供一個函數,直到函數生成一個非 null 的結果。然後搜 索終止,返回這個函數的結果。

•歸約(reduce) 組合所有鍵或值, 這裏要使用所提供的一個累加函數。

•forEach 爲所有鍵或值提供一個函數

每個操作都有 4 個版本:

•operationKeys: 處理鍵。

•operatioriValues: 處理值。

•operation: 處理鍵和值。

•operatioriEntries: 處理 Map.Entry對象

  對於上述各個操作, 需要指定一個參數化閾值。如果映射包含的 元素多於這個閾值, 就會並行完成批操作。如果希望批操作在一個線程中運行,可以使用閾 值 Long.MAX_VALUE。如果希望用儘可能多的線程運行批操作,可以使用閾值 1。  

String result=map.search(threhold,(k,v)->v>1000?k:null);

//forEach方法有兩種形式。第一個只爲各個映射條目提供一個消費者函數, 例如:
map.forEach(threhold,(k,v)->System.out.println(k+"->"+v));
//第二種形式還有一個轉換器函數,這個函數要先提供,其結果會傳遞到消費者: 
map.forEach(threhold,(k,v)->k+"->"+v,System.out::println);

Long sum=map.reduceValues(threhold,Long::sum);

Integer maxlength=map.reduceKeys(threshold,String::length,Integer::sum);

如果映射爲空, 或者所有條目都被過濾掉, reduce 操作會返回 null。如果只有一 個元素, 則返回其轉換結果, 不會應用累加器

對於 int、 long 和 double 輸出還有相應的特殊化操作, 分別有後綴 Tolnt、ToLong和 ToDouble.

併發集視圖

  靜態 newKeySet方法會生成一個 Set<K>, 這實際上是 ConcurrentHashMap<K, Boolean〉 的一個包裝器。(所有映射值都爲 Boolean.TRUE, 不過因爲只是要把它用作一個集,所以並 不關心具體的值。
  

Set<String> words=ConcurrentHashMap.<String>newKeySet();

  這個集是可變的。 如果刪除這個集的元素,這個鍵(以及相應的值)會從映射中刪除。不過,不能向鍵集增加 元素,因爲沒有相應的值可以增加。Java SE 8 爲 ConcurrentHashMap增加了第二個 keySet方 法,包含一個默認值,可以在爲集增加元素時使用:

Set<String> words = map.keySet(1L);

words.add("java”); 如果 "Java”在 words 中不存在,現在它會有一個值 1.

寫數組的拷貝

  CopyOnWriteArrayList 和 CopyOnWriteArraySet 是線程安全的集合,其中所有的修改線 程對底層數組進行復制。如果在集合上進行迭代的線程數超過修改線程數, 這樣的安排是 很有用的。當構建一個迭代器的時候, 它包含一個對當前數組的引用。如果數組後來被修改 了,迭代器仍然引用舊數組, 但是,集合的數組已經被替換了。因而,舊的迭代器擁有一致 的(可能過時的)視圖,訪問它無須任何同步開銷。

並行數組算法

  在 Java SE 8中, Arrays類提供了大量並行化操作。靜態 Arrays.parallelSort 方法可以對 一個基本類型值或對象的數組排序。例如:

  parallelSetAll 方法會用由一個函數計算得到的值填充一個數組。這個函數接收元素索引, 然後計算相應位置上的值。 

Arrays.parallelSort(words);

最後還有一個 parallelPrefix 方法,它會用對應一個給定結合操作的前綴的累加結果替換 各個數組元素.

Arrays.parallelPrefix(values, (x, y)-> x * y) 之後,數組將包含: [1,1*2,1*2*3,...]

較早的線程安全集合

  Vector 和 Hashtable類就提供了線程安全的動態數組和散列表的 實現。現在這些類被棄用了, 取而代之的是 AnayList 和 HashMap類。這些類不是線程安全 的,而集合庫中提供了不同的機制。任何集合類都可以通過使用同步包裝器(synchronization wrapper) 變成線程安全的: 

List<E> list=Collections.synchronizedList(new ArrayList<>);
Map<K,V> synchHashMap=Collections.synchronizeMap(new HashMap<K,V>());

結果集合的方法使用鎖加以保護,提供了線程安全訪問

  如果在另一個線程可能進行修改時要對集合進行迭代,仍然需要使用“ 客戶端” 鎖定:

synchronized (synchHashMap)

{ Iterator<K> iter = synchHashMap.keySet().iterator(); while (iter.hasNextO) . . }

  如果使用“ foreach” 循環必須使用同樣的代碼, 因爲循環使用了迭代器。注意:如果在 迭代過程中,別的線程修改集合,迭代器會失效,拋出 ConcurrentModificationException異 常。同步仍然是需要的, 因此併發的修改可以被可靠地檢測出來。 最好使用java.Util.COnciirrent 包中定義的集合, 不使用同步包裝器中的。特別是, 假如它 們訪問的是不同的桶, 由於 ConcurrentHashMap 已經精心地實現了,多線程可以訪問它而且 不會彼此阻塞。有一個例外是經常被修改的數組列表。在那種情況下,同步的 ArrayList 可 以勝過 CopyOnWriteArrayList().

八、Collable與Future

  Runnable 封裝一個異步運行的任務,可以把它想象成爲一個沒有參數和返回值的異步方 法。Callable 與 Runnable 類似,但是有返回值。Callable 接口是一個參數化的類型, 只有一 個方法 call。類型參數是返回值的類型。例如, Callable<Integer> 表示一個最終返回 Integer 對象的異 步計算。 

  Future 保存異步計算的結果。可以啓動一個計算,將 Future 對象交給某個線程,然後忘 掉它。Future 對象的所有者在結果計算好之後就可以獲得它。 

Future接口的方法:

public interface Future<V> 
{ V get() throws .. 
  V get(long timeout, TimeUnit unit) throws .. 
  void cancel(boolean maylnterrupt); 
   boolean isCancelled(); 
   boolean isDone(); } 

FutureTask 包裝器是一種非常便利的機制, 可將 Callable轉換成 Future 和 Runnable, 它 同時實現二者的接口.

例:

package future;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;

/**
 * @version 1.01 2012-01-26
 * @author Cay Horstmann
 */
public class FutureTest
{
   public static void main(String[] args)
   {
      try (Scanner in = new Scanner(System.in))
      {
         System.out.print("Enter base directory (e.g. /usr/local/jdk5.0/src): ");
         String directory = in.nextLine();
         System.out.print("Enter keyword (e.g. volatile): ");
         String keyword = in.nextLine();
   
         MatchCounter counter = new MatchCounter(new File(directory), keyword);
         FutureTask<Integer> task = new FutureTask<>(counter);
         Thread t = new Thread(task);
         t.start();
         try
         {
            System.out.println(task.get() + " matching files.");
         }
         catch (ExecutionException e)
         {
            e.printStackTrace();
         }
         catch (InterruptedException e)
         {
         }
      }
   }
}

/**
 * This task counts the files in a directory and its subdirectories that contain a given keyword.
 */
class MatchCounter implements Callable<Integer>
{
   private File directory;
   private String keyword;

   /**
    * Constructs a MatchCounter.
    * @param directory the directory in which to start the search
    * @param keyword the keyword to look for
    */
   public MatchCounter(File directory, String keyword)
   {
      this.directory = directory;
      this.keyword = keyword;
   }

   public Integer call()
   {
      int count = 0;
      try
      {
         File[] files = directory.listFiles();
         List<Future<Integer>> results = new ArrayList<>();

         for (File file : files)
            if (file.isDirectory())
            {
               MatchCounter counter = new MatchCounter(file, keyword);
               FutureTask<Integer> task = new FutureTask<>(counter);
               results.add(task);
               Thread t = new Thread(task);
               t.start();
            }
            else
            {
               if (search(file)) count++;
            }

         for (Future<Integer> result : results)
            try
            {
               count += result.get();
            }
            catch (ExecutionException e)
            {
               e.printStackTrace();
            }
      }
      catch (InterruptedException e)
      {
      }
      return count;
   }

   /**
    * Searches a file for a given keyword.
    * @param file the file to search
    * @return true if the keyword is contained in the file
    */
   public boolean search(File file)
   {
      try
      {
         try (Scanner in = new Scanner(file, "UTF-8"))
         {
            boolean found = false;
            while (!found && in.hasNextLine())
            {
               String line = in.nextLine();
               if (line.contains(keyword)) found = true;
            }
            return found;
         }
      }
      catch (IOException e)
      {
         return false;
      }
   }
}

九、執行器

  構建一個新的線程是有一定代價的, 因爲涉及與操作系統的交互。如果程序中創建了大 量的生命期很短的線程,應該使用線程池(thread pool)。一個線程池中包含許多準備運行的 空閒線程。將 Runnable 對象交給線程池, 就會有一個線程調用 run方法。當 run 方法退出 時,線程不會死亡,而是在池中準備爲下一個請求提供服務。

    另一個使用線程池的理由是減少併發線程的數目。創建大量線程會大大降低性能甚至使 虛擬機崩潰。如果有一個會創建許多線程的算法, 應該使用一個線程數“ 固定的” 線程池以 限制併發線程的總數。 執行器(Executor) 類有許多靜態工廠方法用來構建線程池

線程池

  newCachedThreadPoo丨方法構建了一個線程池, 對於每個任務, 如果有空閒線程可用,立即讓它執行 任務,如果沒有可用的空閒線程, 則創建一個新線程。newFixedThreadPool 方法構建一個具 有固定大小的線程池。如果提交的任務數多於空閒的線程數, 那麼把得不到服務的任務放 置到隊列中。當其他任務完成以後再運行它們。newSingleThreadExecutor 是一個退化了的大小爲 1 的線程池: 由一個線程執行提交的任務,一個接着一個。這 3 個方法返回實現了 ExecutorService 接口的 ThreadPoolExecutor 類的對象。 

  可用下面的方法之一將一個 Runnable 對象或 Callable 對象提交給 ExecutorService:

Future<?> submit(Runnable task)

Future<T> submit(Runnable task, T result)

Future<T> submit(Callable<T> task) 該池會在方便的時候儘早執行提交的任務。調用 submit 時,會得到一個 Future 對象, 可 用來查詢該任務的狀態。 

候得到它。 當用完一個線程池的時候, 調用 shutdown。該方法啓動該池的關閉序列。被關閉的執 行器不再接受新的任務。當所有任務都完成以後,線程池中的線程死亡。另一種方法是調用 shutdownNow。該池取消尚未開始的所有任務並試圖中斷正在運行的線程。 下面總結了在使用連接池時應該做的事:

1) 調用 Executors 類中靜態的方法 newCachedThreadPool 或 newFixedThreadPool。

2) 調用 submit 提交 Runnable 或 Callable對象。

3 ) 如果想要取消一個任務, 或如果提交 Callable 對象, 那就要保存好返回的 Future 對象。

4 ) 當不再提交任何任務時,調用 shutdown。

package threadPool;

import java.io.*;
import java.util.*;
import java.util.concurrent.*;

/**
 * @version 1.02 2015-06-21
 * @author Cay Horstmann
 */
public class ThreadPoolTest
{
   public static void main(String[] args) throws Exception
   {
      try (Scanner in = new Scanner(System.in))
      {
         System.out.print("Enter base directory (e.g. /usr/local/jdk5.0/src): ");
         String directory = in.nextLine();
         System.out.print("Enter keyword (e.g. volatile): ");
         String keyword = in.nextLine();
   
         ExecutorService pool = Executors.newCachedThreadPool();
   
         MatchCounter counter = new MatchCounter(new File(directory), keyword, pool);
         Future<Integer> result = pool.submit(counter);
   
         try
         {
            System.out.println(result.get() + " matching files.");
         }
         catch (ExecutionException e)
         {
            e.printStackTrace();
         }
         catch (InterruptedException e)
         {
         }
         pool.shutdown();
   
         int largestPoolSize = ((ThreadPoolExecutor) pool).getLargestPoolSize();
         System.out.println("largest pool size=" + largestPoolSize);
      }
   }
}

/**
 * This task counts the files in a directory and its subdirectories that contain a given keyword.
 */
class MatchCounter implements Callable<Integer>
{
   private File directory;
   private String keyword;
   private ExecutorService pool;
   private int count;

   /**
    * Constructs a MatchCounter.
    * @param directory the directory in which to start the search
    * @param keyword the keyword to look for
    * @param pool the thread pool for submitting subtasks
    */
   public MatchCounter(File directory, String keyword, ExecutorService pool)
   {
      this.directory = directory;
      this.keyword = keyword;
      this.pool = pool;
   }

   public Integer call()
   {
      count = 0;
      try
      {
         File[] files = directory.listFiles();
         List<Future<Integer>> results = new ArrayList<>();

         for (File file : files)
            if (file.isDirectory())
            {
               MatchCounter counter = new MatchCounter(file, keyword, pool);
               Future<Integer> result = pool.submit(counter);
               results.add(result);
            }
            else
            {
               if (search(file)) count++;
            }

         for (Future<Integer> result : results)
            try
            {
               count += result.get();
            }
            catch (ExecutionException e)
            {
               e.printStackTrace();
            }
      }
      catch (InterruptedException e)
      {
      }
      return count;
   }

   /**
    * Searches a file for a given keyword.
    * @param file the file to search
    * @return true if the keyword is contained in the file
    */
   public boolean search(File file)
   {
      try
      {
         try (Scanner in = new Scanner(file, "UTF-8"))
         {
            boolean found = false;
            while (!found && in.hasNextLine())
            {
               String line = in.nextLine();
               if (line.contains(keyword)) found = true;
            }         
            return found;
         }
      }
      catch (IOException e)
      {
         return false;
      }
   }
}

  出於信息方面的考慮, 這個程序打印出執行中池中最大的線程數。 但是不能通過 ExecutorService 這個接口得到這一信息。因此, 必須將該pool 對象強制轉換爲 ThreadPoolExecutor 類對象。 

預定執行

     ScheduledExecutorService 接口具有爲預定執行(Scheduled Execution) 或 重 復 執 行 任 務而設計的方法。它是一種允許使用線程池機制的java.util.Timer 的泛化。Executors 類的 newScheduledThreadPool 和 newSingleThreadScheduledExecutor方法將返回實現了 Scheduled-ExecutorService 接口的對象。 可以預定 Runnable 或 Callable 在初始的延遲之後只運行一次。也可以預定一個 Runnable 對象週期性地運行。

  控制任務組

  有 時, 使用執行器有更有實際意義的原因, 控制一組相關任務。invokeAny方法提交所有對象到一個 Callable 對象的集合中,並返回某個已經完成了的 任務的結果。無法知道返回的究竟是哪個任務的結果, 也許是最先完成的那個任務的結果。 對於搜索問題, 如果你願意接受任何一種解決方案的話,你就可以使用這個方法。

  

List<Callable<T>>tasks=...;
list<Future<T>> results=executor.invokeAll(tasks);
for(Future<T>:results){
    processFurther(result.get(i));
}

這個方法的缺點是如果第一個任務恰巧花去了很多時間,則可能不得不進行等待。將結 果按可獲得的順序保存起來更有實際意義。可以用 ExecutorCompletionService 來進行排列。 用常規的方法獲得一個執行器。然後, 構建一個 ExecutorCompletionService, 提交任務 給完成服務。更有效:

ExecutorCompletionService<T> service = new ExecutorCompletionServiceo(executor);
for (Callable<T> task : tasks) 
   service,submit(task);

for (int i = 0; i < tasks.size();i++) 
  processFurther(service.take().get()); 

Fork-Join框架

  要採用框架可用的一種方式完成這種遞歸計算, 需要提供一個擴展 RecursiveTask() 的 類(如果計算會生成一個類型爲 T 的結果)或者提供一個擴展 RecursiveAction 的類(如果不 生成任何結果)。再覆蓋 compute方法來生成並調用子任務,然後合併其結果.

例:

package forkJoin;

import java.util.concurrent.*;
import java.util.function.*;

/**
 * This program demonstrates the fork-join framework.
 * @version 1.01 2015-06-21
 * @author Cay Horstmann
 */
public class ForkJoinTest
{
   public static void main(String[] args)
   {
      final int SIZE = 10000000;
      double[] numbers = new double[SIZE];
      for (int i = 0; i < SIZE; i++) numbers[i] = Math.random();
      Counter counter = new Counter(numbers, 0, numbers.length, x -> x > 0.5);
      ForkJoinPool pool = new ForkJoinPool();
      pool.invoke(counter);
      System.out.println(counter.join());
   }
}

class Counter extends RecursiveTask<Integer>
{
   public static final int THRESHOLD = 1000;
   private double[] values;
   private int from;
   private int to;
   private DoublePredicate filter;

   public Counter(double[] values, int from, int to, DoublePredicate filter)
   {
      this.values = values;
      this.from = from;
      this.to = to;
      this.filter = filter;
   }

   protected Integer compute()
   {
      if (to - from < THRESHOLD)
      {
         int count = 0;
         for (int i = from; i < to; i++)
         {
            if (filter.test(values[i])) count++;
         }
         return count;
      }
      else
      {
         int mid = (from + to) / 2;
         Counter first = new Counter(values, from, mid, filter);
         Counter second = new Counter(values, mid, to, filter);
         invokeAll(first, second);
         return first.join() + second.join();
      }
   }
}

  可完成Future

待完成。。。。。

十、同步器

  java.util.concurrent 包包含了幾個能幫助人們管理相互合作的線程集的類見表 14-5。這 些機制具有爲線程之間的共用集結點模式(common rendezvous patterns) 提供的“ 預置功能” ( canned functionality)。 如果有一個相互合作的線程集滿足這些行爲模式之一, 那麼應該直接 重用合適的庫類而不要試圖提供手工的鎖與條件的集合。

信號量

  概念上講,一個信號量管理許多的許可證(permit)。爲了通過信號量,線程通過調用 acquire請求許可。其實沒有實際的許可對象, 信號量僅維護一個計數。許可的數目是固定 的,由此限制了通過的線程數量。其他線程可以通過調用 release 釋放許可。而且,許可不是二 必須由獲取它的線程釋放。事實上,任何線程都可以釋放任意數目的許可,這可能會增加許可數目以至於超出初始數目。 

倒計時門栓

  一個倒計時門栓(CountDownLatch) 讓一個線程集等待直到計數變爲 0。倒計時門栓是 一次性的。一旦計數爲 0, 就不能再重用了。 一個有用的特例是計數值爲 1 的門栓。實現一個只能通過一次的門。線程在門外等候直 到另一個線程將計數器值置爲 舉例來講, 假定一個線程集需要一些初始的數據來完成工作。工作器線程被啓動並在門 外等候。另一個線程準備數據。當數據準備好的時候, 調用 cmmtDown, 所有工作器線程就 可以繼續運行了。 然後,可以使用第二個門栓檢査什麼時候所有工作器線程完成工作。用線程數初始化門 栓。每個工作器線程在結束前將門栓計數減 1。另一個獲取工作結果的線程在門外等待,一 旦所有工作器線程終止該線程繼續運行。

障柵

  CyclicBarrier 類實現了一個集結點(rendezvous) 稱爲障柵(barrier)。考慮大量線程運行 在一次計算的不同部分的情形。當所有部分都準備好時,需要把結果組合在一起。當一個線 程完成了它的那部分任務後, 我們讓它運行到障柵處。一旦所有的線程都到達了這個障柵, 障柵就撤銷,線程就可以繼續運行。 下面是其細節。首先, 構造一個障柵,並給出參與的線程數:

CyclicBarrier barrier = new CydicBarrier(nthreads);

每一個線程做一些工作,完成後在障柵上調用 await :

 public void run(){ doWork(); bamer.await();
await 方法有一個可選的超時參數: barrier.await(100, TineUnit.MILLISECONDS); 如果任何一個在障柵上等待的線程離開了障柵, 那麼障柵就被破壞了(線程可能離開是 因爲它調用 await 時設置了超時,或者因爲它被中斷了) 。在這種情況下,所有其他線程的 await 方法拋出 BrokenBarrierException 異常。那些已經在等待的線程立即終止 await 的調用。

   可以提供一個可選的障柵動作(barrier action), 當所有線程到達障柵的時候就會執行這 一動作。

Runnable barrierAction = ..

CyclicBarrier barrier = new CyclicBarrier(nthreads, barrierAction);

   該動作可以收集那些單個線程的運行結果。 障柵被稱爲是循環的(cyclic), 因爲可以在所有等待線程被釋放後被重用。在這一點上, 有別於 CountDownLatch, CountDownLatch 只能被使用一次。 Phaser 類增加了更大的靈活性,允許改變不同階段中參與線程的個數

交換器

  當兩個線程在同一個數據緩衝區的兩個實例上工作的時候, 就可以使用交換器 ( Exchanger) 典型的情況是, 一個線程向緩衝區填人數據, 另一個線程消耗這些數據。當它 們都完成以後,相互交換緩衝區。 

同步隊列

同步隊列是一種將生產者與消費者線程配對的機制。當一個線程調用 SynchronousQueue 的 put 方法時,它會阻塞直到另一個線程調用 take方法爲止,反之亦然。與 Exchanger 的情 況不同, 數據僅僅沿一個方向傳遞,從生產者到消費者。 即使 SynchronousQueue 類實現了 BlockingQueue 接口, 概念上講, 它依然不是一個隊 列。它沒有包含任何元素,它的 size方法總是返回 0。
 

 

 

 

 

 

 

 

 

 

 

 

 

 

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