Java 并發入門

一、并發

進程:每個進程都擁有自己的一套變量

線程:線程之間共享數據

1.線程

Java中為多線程任務提供了很多的類。包括最基礎的Thread類、Runnable等接口,用于線程同步的鎖、阻塞隊列、同步器,使用線程池的執行器、執行框架,還有可以在多線程中使用的線程安全集合等。

(1)使用多線程給其他任務提供機會

創建線程:

  • 將任務代碼移到實現了Runnable接口的類的run方法中,這個接口非常簡單,只有一個方法:
public interface Runnable{
    void run();
}
  • 由Runnable創建一個Thread對象
Thread t = new Thread(r);
  • 啟動線程:
t.start();

注意:不要調用Thread或Runnable對象中的run方法,只會執行同一個線程中的任務,不會啟動新的線程,應該使用Thread的start方法。


二、中斷線程

沒有終止線程的方法,只能通過interrupt方法來請求中斷。

Thread或Runnable對象的run()方法包裝了新線程中執行的代碼,在run()方法中遇到下面的情況,線程會終止。

  • 正常終止。執行完最后一條語句,也包括遇到return返回
  • 異常終止。出現未捕獲的異常

強制結束:

  • 調用Thread對象的stop()方法。拋出一個ThreadDeath異常,停止線程執行(這個異常如果被捕獲一定要重新拋出)。這個方法已經不推薦使用,原因是線程可能停止在一個不安全的狀態(例如轉賬操作,從一個賬號減了錢,還沒加到另一個賬號,線程被強制結束了),應該使用請求中斷的方式。
  • 請求中斷方式。要結束一個線程,就設置該線程的中斷變量(調用Thread對象的interrupt()方法)(表明著有人想要中斷這個線程),線程中的代碼自己要負責查詢中斷變量(Thread類靜態方法interrupted()或Thread對象的isInterrupted()方法),如果發現中斷變量被設置了就自覺點不要再執行了,恢復到安全的狀態后自行退出。請求中斷不是強制的,如果線程中的代碼不查詢中斷變量,或者發現中斷變量已經被設置了但是不理會繼續厚著臉皮執行,這個線程還是會一直運行不會被停止。
//栗子

public class InterruptTest {  
    public static void main(String[] args) throws InterruptedException {  
        MyThread t = new MyThread("MyThread");  
        t.start();  
        Thread.sleep(100);// 睡眠100毫秒  
        t.interrupt();// 中斷t線程  
    }  
}  
class MyThread extends Thread {  
    int i = 0;  
    public MyThread(String name) {  
        super(name);  
    }  
    public void run() {  
        while(!isInterrupted()) {// 當前線程沒有被中斷,則執行  
            System.out.println(getName() + getId() + "執行了" + ++i + "次");  
        }  
    }  
}  

void interrupt()方法和InterruptedException特別說明

  • 如果調用interrupt方法時,若線程正被某些可中斷的方法阻塞著(sleep,wait或可中斷IO調用等),那么現在肯定是無法檢測中斷狀態的,系統會清理中斷狀態,拋出InterruptedException異常,阻塞的方法調用會立即被這個異常中斷。
  • 如果調用interrupt方法將中斷狀態設置為了true,不久就調用了一個可中斷的方法(sleep,wait,可中斷IO調用等),這個調用不會成功,并且同樣會清除中斷狀態標志,使中斷標志為false,拋出InterruptedException異常。可見,如果會循環調用sleep()這類可中斷的方法,就不需要再手動檢測中斷狀態了。
  • interrupt向線程發送中斷請求,線程的中斷狀態將被設置為true,如果目前線程被阻塞,那么InterruptedException異常將被拋出,中斷狀態會被設為false。

Thread的static boolean interrupted():

  • 測試當前線程是否被中斷,這一調用會產生一個副作用,它將當前線程的中斷狀態重置為false

Thread的boolean isInterrupted():

  • 測試線程是否被終止,這一調用不會改變線程的中斷狀態。

可見如果不設置中斷,InterruptedException肯定不會出現,而只要拋出InterruptedException,設置的中斷狀態肯定已經被清理了,這種情況只有InterruptedException這個異常是我們知道有中斷請求的唯一標識了,因此我們要向外層通知有中斷發生,千萬不要再把這個異常壓制住,否則怎么調用interrupt()方法請求中斷都不會有作用,線程中外層的代碼壓根不知道有中斷這回事,照常運行。將這個中斷請求通知給外層有兩種方式:

  • catch到InterruptedException時,調用Thread.currentThread().interrupt(),重新把中斷狀態設置上,讓外層可以檢測到。
  • 最好的方法是,不要再catch InterruptedException異常啦,只要有這個異常就往外層拋吧。一直拋到最外層,在Thread對象或Runnable對象的run()方法中處理這個異常(處理操作:恢復到安全的狀態然后自覺退出)。
public class Erupt {
    static class MyInterruptableExceptionTask implements Runnable
    {
        private int begin=0;
        public MyInterruptableExceptionTask(int s){begin=s;}
        @Override
        public void run() {
            try {
                int end=begin+10;
                for(int i=begin; i<end; i++){
                    System.out.println("sub: "+i);
                    Thread.sleep(1000);    //如果設置中斷時正在sleep,或設置完中斷后一個循環里遇到sleep,都會拋出InterruptedException異常,不需要再手動檢測中斷狀態了
                }
            } catch (InterruptedException e) {
                System.out.println("the call Thread.sleep(n) is interrupted by InterruptedExcetpion");
                Thread.currentThread().interrupt();    //產生InterruptedException異常時中斷狀態被清除,所以要重新設置中斷或將中斷異常向外拋出供后續代碼檢測是否發生了中斷
            }

            if(Thread.currentThread().isInterrupted())
                System.out.println("sub thread is interrupted");
            else
                System.out.println("sub natural stop");
        }
    }

    public static void main(String[] args) {
        Thread t=new Thread(new MyInterruptableExceptionTask(111));
        t.start();

        for(int i=0; i<10; i++){
            System.out.println("main: "+i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if(i==5)
                t.interrupt();
        }
    }


}

三、線程狀態

線程有6中狀態:

  • New:用new操作符創建一個新線程時,如 new Thread(r), 該線程還沒有開始運行。這意味著它的狀態是new,當一個線程處在new狀態,程序還沒有開始運行線程中的代碼。在線程運行之前還有一些基礎工作要做。
  • Runnable:一旦調用start()方法,線程就處于runnable狀態。可以可運行的線程可能正在運行也可能沒有運行,這取決于操作系統給線程提供運行的時間(這就是為什么這個狀態成為可運行而不是運行),事實上,運行中的線程被中斷,目的是為了讓他們線程獲得運行機會。線程調度的細節依賴于操作系統提供的服務。搶占式調度系統給每一個可運行線程一個時間片來執行任務,當時間片用完,操作系統剝奪該線程的運行權,并給另一個線程可運行機會。當選擇下一個線程時,操作系統考慮線程的優先級。
  • Bolocked:阻塞,當一個線程試圖獲取一個內部的對象鎖(而不是java.util.concurrent庫里的鎖), 而該鎖被其他線程持有,則該線程進入阻塞狀態。當其他線程釋放該鎖,并且線程調度器允許本線程持有它的時候,該線程將變成非阻塞狀態。
  • Waiting:等待,當線程通知另一個線程通知調度器一個條件時,它自己進入等待狀態。在調用Object.wait方法或Thread.join方法,或者是等待java.util.concurrent庫中的Lock或Condition時,就會出現這種情況。實際上,被阻塞狀態與被等待狀態是有很大不同的。
  • Timed waiting:計時等待,有幾個方法有一個超時參數。調用它們導致線程進入計時等待(timed waiting)狀態。這一狀態將一直保持到超時期滿或者接收到適當的通知。帶有超時參數的方法有Thread.sleep和Object.wait, Thrad.join, Lock.tryLock以及Condition.await的計時版。
  • Terminated:因為run方法正常退出而自然死亡;-因為一個沒有捕獲的異常終止了run方法二意外死亡
image
image

要確定一個線程的當前狀態,可以調用Thread的getState()方法。

1.創建新線程

new Thread(r)

2.可運行線程

調用start后,線程處于Runnable轉態

3.被阻塞線程和等待線程

  • 當試圖獲取一個內部的對象鎖,而該鎖被他人持有,則進入阻塞狀態。
  • 當線程等待另一個線程通知調度器一個條件時,自己進入等待狀態。
  • 有幾個方法有一個超時參數,調用他們導致線程進入計時等待

4.被終止的線程

  • 自然退出
  • 沒有捕獲異常終止了run方法而以外死亡
  • void join():阻塞調用此方法的線程(calling thread),直到線程t完成,此線程再繼續;通常用于在main()主線程內,等待其它線程完成再結束main()主線程,底層通過wait實現的。
  • void join(long millis):等待指定的線程死亡或者經過指定的毫秒數

四、線程屬性

1.線程優先級

默認情況下,一個線程繼承它父親的優先級,可以用setPriority()方法來提高一個線程的優先級:

  • MIN_PRIORITY:1
  • MAX_PRIORITY:10
  • NORM_PRIORITY:5
  • void setPriority(int newPriority)
  • static void yield():理論上,yield意味著放手,放棄,投降。一個調用yield()方法的線程告訴虛擬機它樂意讓其他線程占用自己的位置。這表明該線程沒有在做一些緊急的事情。注意,這僅是一個暗示,并不能保證不會產生任何影響。

2.守護線程

通過調用:

t.setDaemon(true)

可以將當前線程轉換為守護線程。

守護線程的唯一用途是為其他線程提供服務。當只剩下守護線程時,虛擬機就退出來了。

守護線程很容易被中斷,所以盡量避免用守護線程去訪問文件、數據庫等固有資源。

3.未捕獲異常處理器

線程運行不能按照順序執行過程中捕獲異常的方式來處理異常,異常會被直接拋出到控制臺(由于線程的本質,使得你不能捕獲從線程中逃逸的異常。一旦異常逃逸出任務的run方法,它就會向外傳播到控制臺,除非你采用特殊的形式捕獲這種異常。),這樣會讓你很頭疼,無法捕捉到異常就無法處理異常而引發的問題。

線程的run方法是不會拋出任何受查異常,所以異常需要被傳播到一個用于未捕獲異常的處理器,該處理器必須實現Thread.UncaughtExceptionHandler接口的類,這個接口只有一個方法:

void uncaughtException(Thread t,Throwable e);
/*
 * 第一步:定義符合線程異常處理器規范的“異常處理器”
 * 實現Thread.UncaughtExceptionHandler規范
 */
class MyUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler{
    /*
     * Thread.UncaughtExceptionHandler.uncaughtException()會在線程因未捕獲的異常而臨近死亡時被調用
     */
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        System.out.println("caught    "+e);
    }
}
/*
 * 第二步:定義線程工廠
 * 線程工廠用來將任務附著給線程,并給該線程綁定一個異常處理器
 */
class HanlderThreadFactory implements ThreadFactory{
    @Override
    public Thread newThread(Runnable r) {
        System.out.println(this+"creating new Thread");
        Thread t = new Thread(r);
        System.out.println("created "+t);
        t.setUncaughtExceptionHandler(new MyUncaughtExceptionHandler());//設定線程工廠的異常處理器
        System.out.println("eh="+t.getUncaughtExceptionHandler());
        return t;
    }
}
/*
 * 第三步:我們的任務可能會拋出異常
 * 顯示的拋出一個exception
 */
class ExceptionThread implements Runnable{
    @Override
    public void run() {
        Thread t = Thread.currentThread();
        System.out.println("run() by "+t);
        System.out.println("eh = "+t.getUncaughtExceptionHandler());
        throw new RuntimeException();
    }
}
/*
 * 第四步:使用線程工廠創建線程池,并調用其execute方法
 */
public class ThreadExceptionUncaughtExceptionHandler{
    public static void main(String[] args){
        ExecutorService exec = Executors.newCachedThreadPool(new HanlderThreadFactory());
        exec.execute(new ExceptionThread());
    }
}

可以使用setUncaughtExceptionHandler方法安裝一個處理器,也可以使用setDefaultUncaughtExceptionHandler為所有線程安裝一個默認的處理器。

public static void main(String[] args){
        Thread.setDefaultUncaughtExceptionHandler(new MyUncaughtExceptionHandler());
        ExecutorService exec =Executors.newCachedThreadPool();
        exec.execute(new ExceptionThread());
}

在java中要捕捉多線程產生的異常,需要自定義異常處理器,并設定到對應的線程工廠中(即第一步和第二步)。

如果你知道將要在代碼中處使用相同的異常處理器,那么更簡單的方式是在Thread類中設置一個靜態域,并將這個處理器設置為默認的未捕獲處理器。
這個處理器只有在不存在線程專有的未捕獲異常處理器的情況下才會被調用。


五、同步

1.鎖對象

ReentrantLock是Java并發包中互斥鎖,它有公平鎖和非公平鎖兩種實現方式,以lock()為例,其使用方式為:

myLock.lock();
try{
    critical section
}
finally{
    myLock.unLock();
}
public void transfer(int from,int to,double amount){
        bankLock.lock();
        try{
            if (accounts[from]<amount){
                return;
            }
            System.out.println(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());
        }
        finally {
            bankLock.unlock();
        }

    }

那么,ReentrantLock內部是如何實現鎖的呢?接下來我們就以JDK1.7中的ReentrantLock的lock()為例詳細研究下。ReentrantLock類實現了Lock和java.io.Serializable接口,其內部有一個實現鎖功能的關鍵成員變量Sync類型的sync,定義如下:

/** Synchronizer providing all implementation mechanics */  
private final Sync sync;

2.條件對象

使用一個條件對象來管理那些已經獲得一個鎖但是不能做有用工作的線程

private Condition sufficentFunds;

    private final double[] accounts ;

    public Bank(int n,double initialBalance){
        accounts = new double[n];
        Arrays.fill(accounts,initialBalance);
        sufficentFunds = bankLock.newCondition();
    }

如果發現銀行存儲不足,等待直到另一個線程向賬戶注入資金,但是由于鎖對象的排他性,通過條件對象使當前線程被阻塞,放棄了鎖。如果transfer發現資金不足,會調用sufficientFunds.await();而轉入線程調會調用sufficientFunds.singalAll()

sufficientFunds.singalAll():通知等待的線程可能條件滿足,可以讓sufficientFunds.await()返回阻塞地方再次測試一下。

singal是隨機選擇一條等待線程。

public class Bank {
    private Lock bankLock = new ReentrantLock();
    private Condition sufficentFunds;

    private final double[] accounts ;

    public Bank(int n,double initialBalance){
        accounts = new double[n];
        Arrays.fill(accounts,initialBalance);
        sufficentFunds = bankLock.newCondition();
    }


    public void transfer(int from,int to,double amount){
        bankLock.lock();
        try{
            if (accounts[from]<amount){
                sufficentFunds.await();
            }
            System.out.println(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());
            sufficentFunds.signalAll();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bankLock.unlock();
        }

    }

    private double getTotalBalance() {
        double sum = 0;
        for (double a:accounts){
            sum+=a;
        }
        return sum;

    }

    public int size(){
        return accounts.length;
    }
}

3.synchronized關鍵字

鎖和條件對象的關鍵之處:

  • 鎖用來保護代碼片段,任何時刻只能有一個線程執行保護的代碼
  • 鎖可以管理試圖進入被保護代碼段的線程
  • 鎖可以擁有一個或多個相關的條件對象
  • 每個條件對象管理那些已經進入被保護代碼段但還不能運行的線程
public synchronized void method(){
    method body
}

等價于

public void method(){
    this.intrinsiclock.lock();
    try{
        method 
    }
    finally{
        this.intrinsiclock.unlock();
    }
}

wait方法等價于intrinsicCondition.awaiy();

notifyAll等價于intrinsicCondition.singalAll();

public class Bank {
    private Lock bankLock = new ReentrantLock();
    private Condition sufficentFunds;

    private final double[] accounts ;

    public Bank(int n,double initialBalance){
        accounts = new double[n];
        Arrays.fill(accounts,initialBalance);
        sufficentFunds = bankLock.newCondition();
    }


    public synchronized void transfer(int from,int to,double amount) throws InterruptedException {

            if (accounts[from]<amount){
                wait();
            }
            System.out.println(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());
            notifyAll();


    }

    private double getTotalBalance() {
        double sum = 0;
        for (double a:accounts){
            sum+=a;
        }
        return sum;

    }

    public int size(){
        return accounts.length;
    }
}

(1)修飾一個代碼塊

一個線程訪問一個對象中的synchronized(this)同步代碼塊時,其他試圖訪問該對象的線程將被阻塞。我們看下面一個例子:

/**
 * 同步線程
 */
class SyncThread implements Runnable {
   private static int count;

   public SyncThread() {
      count = 0;
   }

   public  void run() {
      synchronized(this) {
         for (int i = 0; i < 5; i++) {
            try {
               System.out.println(Thread.currentThread().getName() + ":" + (count++));
               Thread.sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }

   public int getCount() {
      return count;
   }
}
SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();
SyncThread1:0 
SyncThread1:1 
SyncThread1:2 
SyncThread1:3 
SyncThread1:4 
SyncThread2:5 
SyncThread2:6 
SyncThread2:7 
SyncThread2:8 
SyncThread2:9

當兩個并發線程(thread1和thread2)訪問同一個對象(syncThread)中的synchronized代碼塊時,在同一時刻只能有一個線程得到執行,另一個線程受阻塞,必須等待當前線程執行完這個代碼塊以后才能執行該代碼塊。Thread1和thread2是互斥的,因為在執行synchronized代碼塊時會鎖定當前的對象,只有執行完該代碼塊才能釋放該對象鎖,下一個線程才能執行并鎖定該對象。
我們再把SyncThread的調用稍微改一下:

Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
thread1.start();
thread2.start();
SyncThread1:0 
SyncThread2:1 
SyncThread1:2 
SyncThread2:3 
SyncThread1:4 
SyncThread2:5 
SyncThread2:6 
SyncThread1:7 
SyncThread1:8 
SyncThread2:9

這時創建了兩個SyncThread的對象syncThread1和syncThread2,線程thread1執行的是syncThread1對象中的synchronized代碼(run),而線程thread2執行的是syncThread2對象中的synchronized代碼(run);我們知道synchronized鎖定的是對象,這時會有兩把鎖分別鎖定syncThread1對象和syncThread2對象,而這兩把鎖是互不干擾的,不形成互斥,所以兩個線程可以同時執行。

(2)當一個線程訪問對象的一個synchronized(this)同步代碼塊時,另一個線程仍然可以訪問該對象中的非synchronized(this)同步代碼塊。

public class SyncTest implements Runnable{
    private static int count;


    @Override
    public void run() {
        for (int i = 0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+":"+(count++));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

    }

    public void print(){
        for (int i = 0;i<5;i++){
            System.out.println(Thread.currentThread().getName()+":"+(count++));
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public static void main(String[] args) {

        SyncTest s = new SyncTest();
        Thread t1 = new Thread(s);
        Thread t2 = new Thread(s);
        t1.start();
        t2.start();
    }
}
Thread-1:0
Thread-0:0
Thread-1:1
Thread-0:2
Thread-1:3
Thread-0:4
Thread-0:5
Thread-1:5
Thread-0:7
Thread-1:6

(3)指定要給某個對象加鎖

/**
 * 銀行賬戶類
 */
class Account {
   String name;
   float amount;

   public Account(String name, float amount) {
      this.name = name;
      this.amount = amount;
   }
   //存錢
   public  void deposit(float amt) {
      amount += amt;
      try {
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
   //取錢
   public  void withdraw(float amt) {
      amount -= amt;
      try {
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }

   public float getBalance() {
      return amount;
   }
}

/**
 * 賬戶操作類
 */
class AccountOperator implements Runnable{
   private Account account;
   public AccountOperator(Account account) {
      this.account = account;
   }

   public void run() {
      synchronized (account) {
         account.deposit(500);
         account.withdraw(500);
         System.out.println(Thread.currentThread().getName() + ":" + account.getBalance());
      }
   }
}
Account account = new Account("zhang san", 10000.0f);
AccountOperator accountOperator = new AccountOperator(account);

final int THREAD_NUM = 5;
Thread threads[] = new Thread[THREAD_NUM];
for (int i = 0; i < THREAD_NUM; i ++) {
   threads[i] = new Thread(accountOperator, "Thread" + i);
   threads[i].start();
}
Thread3:10000.0 
Thread2:10000.0 
Thread1:10000.0 
Thread4:10000.0 
Thread0:10000.0

在AccountOperator 類中的run方法里,我們用synchronized 給account對象加了鎖。這時,當一個線程訪問account對象時,其他試圖訪問account對象的線程將會阻塞,直到該線程訪問account對象結束。也就是說誰拿到那個鎖誰就可以運行它所控制的那段代碼。

當有一個明確的對象作為鎖時,就可以用類似下面這樣的方式寫程序。

public void method3(SomeObject obj)
{
   //obj 鎖定的對象
   synchronized(obj)
   {
      // todo
   }
}

當沒有明確的對象作為鎖,只是想讓一段代碼同步時,可以創建一個特殊的對象來充當鎖:

class Test implements Runnable
{
   private byte[] lock = new byte[0];  // 特殊的instance變量
   public void method()
   {
      synchronized(lock) {
         // todo 同步代碼塊
      }
   }

   public void run() {

   }
}

說明:零長度的byte數組對象創建起來將比任何對象都經濟――查看編譯后的字節碼:生成零長度的byte[]對象只需3條操作碼,而Object lock = new Object()則需要7行操作碼。

(4)修飾一個方法

ynchronized修飾一個方法很簡單,就是在方法的前面加synchronized,public synchronized void method(){//todo}; synchronized修飾方法和修飾一個代碼塊類似,只是作用范圍不一樣,修飾代碼塊是大括號括起來的范圍,而修飾方法范圍是整個函數。

public synchronized void run() {
   for (int i = 0; i < 5; i ++) {
      try {
         System.out.println(Thread.currentThread().getName() + ":" + (count++));
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
}

(5)Synchronized也可修飾一個靜態方法,用法如下:

public synchronized static void method() {
   // todo
}

將靜態方法聲明為synchronize,如果調用這個方法,則該類的class對象的鎖被鎖住,其他線程可以調用同一個類的這個或任何其他同步靜態方法都需要等待當前線程釋放。

(6)修飾一個類:

同修飾static的效果一樣

內部鎖和條件存在的局限性:

  • 不能中斷一個正在試圖獲取鎖的線程
  • 試圖獲得鎖時不能設定超時
  • 每個鎖僅有單一的條件,可能是不夠的
  • 最好是不使用Lock/Condition也不使用synchronize關鍵字,多數情況下使用concurrent包中的一種機制
  • synchronized盡量使用
  • 有特殊情況才使用Lock/Conditions

7.Volatile域

有時候,僅僅為了同步一兩個實例域就使用synchronized關鍵字或是Lock/Condition,會造成很多不必要的開銷。這時候我們可以使用volatile關鍵字,使用volatile關鍵字修飾一個實例域會告訴編譯器和虛擬機這個域可能會被多線程并發訪問,這樣編譯器和虛擬機就能確保它的值總是我們所期望的。

volatile關鍵字的實現原理大致是這樣的:我們在訪問內存中的變量時,通常都會把它緩存在寄存器中,以后再需要讀它的值時,只需從相應寄存器中讀取,若要對該變量進行寫操作,則直接寫相應寄存器,最后寫回該變量所在的內存單元。若線程A把count變量的值緩存在寄存器中,并將count加2(將相應寄存器的值加2),這時線程B被調度,它讀取count變量加2后并寫回。然后線程A又被調度,它會接著剛才的操作,也就是會把count值寫回,此時線程A是直接把寄存器中的值寫回count所在單元,而這個值是過期的。若count被volatile關鍵字修飾,這個問題便可被圓滿解決。volatile變量有一個性質,就是任何時候讀取它的值時,都會直接去相應內存單元讀取,而不是讀取緩存在寄存器中的值。這樣一來,在上面那個場景中,線程A把count寫回時,會從內存中讀取count最新的值,從而確保了count的值總是我們所期望的。

8.final變量

關鍵字 final 可以視為 C++ 中 const 機制的一種受限版本,用于構造不可變對象。final 類型的域是不能修改的(但如果 final 域所引用的對象時可變的,那么這些被引用的對象是可以修改的)。然而,在 Java 內存模型中,final 域還有著特殊的語義。final 域能確保初始化過程的安全性,從而可以不受限制的訪問不可變對象,并在共享這些對象時無需同步。

注: 個人理解為,final 字段一旦被初始化完成,并且構造器沒有把 this 引用傳遞出去,那么在其他線程中就能看到 final 字段的值(域內變量可見性,和 volatile 類似),而且其外部可見狀態永遠也不會改變。它所帶來的安全性是最簡單最純粹的。

注: 即使對象是可變的,通過將對象的某些域聲明為final類型,仍然可以 簡化對狀態的判斷 ,因此限制對象的可變性也就相當于限制了該對象可能的狀態集合。僅包含一個或兩個可變狀態的“基本不可變”對象仍然比包含多個可變狀態的對象簡單。通過將域聲明為final類型,也相當于告訴維護人員這些域是不會變化的。

正如“除非需要更高的可見性,否則應將所有的餓域都聲明為私有域”[EJ Item 12]是一個良好的變成習慣,“除非需要某個域是可變的,否則應將其聲明為final域”也是一個良好的變成習慣。

9.原子性

原子是世界上的最小單位,具有不可分割性。比如 a=0;(a非long和double類型) 這個操作是不可分割的,那么我們說這個操作時原子操作。再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。非原子操作都會存在線程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那么我們稱它具有原子性。Java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

11.鎖測試與超時

通過tryLock()去試圖申請鎖,如果返回true則立即執行,返回false則去做其他事

public class TestTryLock {

    private List<Object> list = new ArrayList<Object>();
    private Lock         lock = new ReentrantLock();

    public static void main(String[] args) {
        final TestTryLock test = new TestTryLock();
        new Thread("第一個線程  ") {

            @Override
            public void run() {
                test.doSomething(Thread.currentThread());
            }
        }.start();

        new Thread("第二個線程  ") {

            @Override
            public void run() {
                test.doSomething(Thread.currentThread());
            }
        }.start();
    }

    public void doSomething(Thread thread) {
        if (lock.tryLock()) {
            try {
                System.out.println(thread.getName() + "得到了鎖.");
                for (int i = 0; i < 10; i++) {
                    list.add(i);
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                System.out.println(thread.getName() + "釋放了鎖.");
                lock.unlock();
            }
        } else {
            System.out.println(thread.getName() + "獲取鎖失敗.");
        }
    }
}

以上代碼運行結果如下:

第一個線程  得到了鎖.
第一個線程  釋放了鎖.
第二個線程  得到了鎖.
第二個線程  釋放了鎖.

12.讀寫鎖

若很多線程從一個內存區域讀取數據,但其中只有極少的一部分線程會對其中的數據進行修改,此時我們希望所有Reader線程共享數據,而所有Writer線程對數據的訪問要互斥。我們可以使用讀/寫鎖來達到這一目的。

Java中的讀/寫鎖對應著ReentrantReadWriteLock類,它實現了ReadWriteLock接口,這個接口的定義如下:

public interface ReadWriteLock {
    /**
     * Returns the lock used for reading.
     *
     * @return the lock used for reading
     */
    Lock readLock();

    /**
     * Returns the lock used for writing.
     *
     * @return the lock used for writing
     */
    Lock writeLock();
}

我們可以看到這個接口就定義了兩個方法,其中readLock方法用來獲取一個“讀鎖”,writeLock方法用來獲取一個“寫鎖”。

ReentrantReadWriteLock類的使用步驟通常如下所示:

//構造一個ReentrantReadWriteLock對象
private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();

//分別從中“提取”讀鎖和寫鎖
private Lock readLock = rwl.readLock();
private Lock writeLock = rwl.writeLock();

//對所有的Reader線程加讀鎖
readLock.lock();
try {
    //讀操作可并發,但寫操作會互斥
} finally {
    readLock.unlock();
}

//對所有的Writer線程加寫鎖
writeLock.lock();
try {
    //排斥所有其他線程的讀和寫操作
} finally {
    writeLock.unlock();
}

在使用ReentrantReadWriteLock類時,我們需要注意以下兩點:

  • 若當前已經有線程占用了讀鎖,其他要申請寫鎖的線程需要占用讀鎖的線程釋放了讀鎖才能申請成功;
  • 若當前已經有線程占用了寫鎖,其他要申請讀鎖或寫鎖的線程都需要等待占用寫鎖的線程釋放了寫鎖才能申請成功。

13.為什么棄用stop和suspend方法

stop方法會終止所有未結束的方法,包括run方法,當前線程被終止,立即釋放被它鎖住的所有對象的鎖,會導致對象狀態的不一致。

suspend是掛起一個持有鎖對象的線程,如果調用suspend方法的線程試圖獲取同一個鎖,就會產生死鎖。假如有A,B兩個線程,A線程在獲得某個鎖之后被suspend阻塞,這時A不能繼續執行,線程B在或者相同的鎖之后才能調用resume方法將A喚醒,但是此時的鎖被A占有,B不能繼續執行,也就不能及時的喚醒A,此時A,B兩個線程都不能繼續向下執行而形成了死鎖。這就是suspend被棄用的原因。

六、阻塞隊列

以上我們所介紹的都屬于Java并發機制的底層基礎設施。在實際編程我們應該盡量避免使用以上介紹的較為底層的機制,而使用Java類庫中提供給我們封裝好的較高層次的抽象。對于許多同步問題,我們可以通過使用一個或多個隊列來解決:生產者線程向隊列中插入元素,消費者線程則取出他們。考慮一下我們最開始提到的Counter類,我們可以通過隊列來這樣解決它的同步問題:增加計數值的線程不能直接訪問Counter對象,而是把add指令對象插入到隊列中,然后由另一個可訪問Counter對象的線程從隊列中取出add指令對象并執行add操作(只有這個線程能訪問Counter對象,因此無需采取額外措施來同步)。

當試圖向滿隊列中添加元素或者向空隊列中移除元素時,阻塞隊列(blocking queue)會導致線程阻塞。通過阻塞隊列,我們可以按以下模式來工作:工作者線程可以周期性的將中間結果放入阻塞隊列中,其他線程可取出中間結果并進行進一步操作。若前者工作的比較慢(還沒來得及向隊列中插入元素),后者會等待它(試圖從空隊列中取元素從而阻塞);若前者運行的快(試圖向滿隊列中插元素),它會等待其他線程。阻塞隊列提供了以下方法:

  • add方法:添加一個元素。若隊列已滿,會拋出IllegalStateException異常。
  • element方法:返回隊列的頭元素。若隊列為空,會拋出NoSuchElementException異常。
  • offer方法:添加一個元素,若成功則返回true。若隊列已滿,則返回false。
  • peek方法:返回隊列的頭元素。若隊列為空,則返回null。
  • poll方法:刪除并返回隊列的頭元素。若隊列為空,則返回null。
  • put方法:添加一個元素。若隊列已滿,則阻塞。
  • remove方法:移除并返回頭元素。若隊列為空,會拋出NoSuchElementException。
  • take方法:移除并返回頭元素。若隊列為空,則阻塞。

阻塞隊列的方法分為以下三種:

  • 當隊列作為線程管理工具,可以使用put和take作為阻塞線程的手段
  • 當向滿的隊列中添加或者從空的隊列中移出元素,add、remove和element操作將拋出異常。
  • 當隊列會在任何時刻滿或者空,要使用offer、poll、peek方法作為代替。
image
  • 拋異常:如果試圖的操作無法立即執行,拋一個異常。
  • 特定值:如果試圖的操作無法立即執行,返回一個特定的值(常常是 true / false)。
  • 阻塞:如果試圖的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行。
  • 超時:如果試圖的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行,但等待時間不會超過給定值。返回一個特定值以告知該操作是否成功(典型的是true / false)。

BlockingQueue 是個接口,你需要使用它的實現之一來使用BlockingQueue,Java.util.concurrent包下具有以下 BlockingQueue 接口的實現類:

  • ArrayBlockingQueue:ArrayBlockingQueue 是一個有界的阻塞隊列,其內部實現是將對象放到一個數組里。有界也就意味著,它不能夠存儲無限多數量的元素。它有一個同一時間能夠存儲元素數量的上限。你可以在對其初始化的時候設定這個上限,但之后就無法對這個上限進行修改了(譯者注:因為它是基于數組實現的,也就具有數組的特性:一旦初始化,大小就無法修改)。
  • DelayQueue:DelayQueue 對元素進行持有直到一個特定的延遲到期。注入其中的元素必須實現 java.util.concurrent.Delayed 接口。
  • LinkedBlockingQueue:LinkedBlockingQueue 內部以一個鏈式結構(鏈接節點)對其元素進行存儲。如果需要的話,這一鏈式結構可以選擇一個上限。如果沒有定義上限,將使用 Integer.MAX_VALUE 作為上限。
  • PriorityBlockingQueue:PriorityBlockingQueue 是一個無界的并發隊列。它使用了和類 java.util.PriorityQueue 一樣的排序規則。你無法向這個隊列中插入 null 值。所有插入到 PriorityBlockingQueue 的元素必須實現 java.lang.Comparable 接口。因此該隊列中元素的排序就取決于你自己的 Comparable 實現。
  • SynchronousQueue:SynchronousQueue 是一個特殊的隊列,它的內部同時只能夠容納單個元素。如果該隊列已有一元素的話,試圖向隊列中插入一個新元素的線程將會阻塞,直到另一個線程將該元素從隊列中抽走。同樣,如果該隊列為空,試圖向隊列中抽取一個元素的線程將會阻塞,直到另一個線程向隊列中插入了一條新的元素。據此,把這個類稱作一個隊列顯然是夸大其詞了。它更多像是一個匯合點。

demo:

package blocking_queue;

import java.util.Random;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * @author Sean
 * @version 1.0
 * @date 創建時間:2017/7/15 14:29
 * @parameter
 * @return
 */
public class BlockingQueueTest {
    public static class Producer implements Runnable{

        private final BlockingQueue<Integer> blockingQueue;
        private volatile boolean flag;
        private Random random;


        public Producer(BlockingQueue<Integer> blockingQueue) {
            this.blockingQueue = blockingQueue;
            flag = false;
            random = new Random();
        }


        @Override
        public void run() {
            while (!flag){
                int info = random.nextInt(100);

                try {
                    blockingQueue.put(info);
                    System.out.println(Thread.currentThread().getName()+" procuce "+info);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        public void shutDown(){
            flag = true;
        }
    }

    public static class Consumer implements Runnable{
        private final BlockingQueue<Integer> blockingQueue;
        private volatile boolean flag;
        public Consumer(BlockingQueue<Integer> blockingQueue) {
            this.blockingQueue = blockingQueue;
        }
        public void run() {
            while(!flag){
                int info;
                try {
                    info = blockingQueue.take();
                    System.out.println(Thread.currentThread().getName()+" consumer "+info);
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
        public void shutDown(){
            flag=true;
        }
    }

    public static void main(String[] args) {
        BlockingQueue<Integer> blockingQueue = new LinkedBlockingQueue<Integer>(10);
        Producer producer=new Producer(blockingQueue);
        Consumer consumer=new Consumer(blockingQueue);
        //創建5個生產者,5個消費者
        for(int i=0;i<10;i++){
            if(i<5){
                new Thread(producer,"producer"+i).start();
            }else{
                new Thread(consumer,"consumer"+(i-5)).start();
            }
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        producer.shutDown();
        consumer.shutDown();

    }
}

java.util.concurrent包提供了以下幾種阻塞隊列:

  • LinkedBlockingQueue是一個基于鏈表實現的阻塞隊列。默認容量沒有上限,但也有可以指定最大容量的構造方法。它有的“雙端隊列版本”為LinkedBlockingDeque。
  • ArrayBlockingQueue是一個基于數組實現的阻塞隊列,它在構造時需要指定容量。它還有一個構造方法可以指定一個公平性參數,若這個參數為true,那么等待了最長時間的線程會得到優先處理(指定公平性參數會降低性能)。
  • PriorityBlockingQueue是一個基于堆實現的帶優先級的阻塞隊列。元素會按照它們的優先級被移除隊列。
package blockingQueue;

import java.util.concurrent.ArrayBlockingQueue;

public class BlockingQueueTest {
    private int size = 20;
    private ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(size);

    public static void main(String[] args)  {
        BlockingQueueTest test = new BlockingQueueTest();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();

        producer.start();
        consumer.start();
    }

    class Consumer extends Thread{
        @Override
        public void run() {
            while(true){
                try {
                    //從阻塞隊列中取出一個元素
                    blockingQueue.take();
                    System.out.println("隊列剩余" + blockingQueue.size() + "個元素");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    class Producer extends Thread{
        @Override
        public void run() {
            while (true) {
                try {
                    //向阻塞隊列中插入一個元素
                    blockingQueue.put(1);
                    System.out.println("隊列剩余空間:" + (size - blockingQueue.size()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

在以上代碼中,我們有一個生產者線程不斷地向一個阻塞隊列中插入元素,同時消費者線程從這個隊列中取出元素。若生產者生產的比較快,消費者取的比較慢導致隊列滿,此時生產者再嘗試插入時就會阻塞在put方法中,直到消費者取出一個元素;反過來,若消費者消費的比較快,生產者生產的比較慢導致隊列空,此時消費者嘗試從中取出時就會阻塞在take方法中,直到生產者插入一個元素。


七、Callable與Future

Callable和Future,它倆很有意思的,一個產生結果,一個拿到結果。

我們之前提到了創建線程的兩種方式,它們有一個共同的缺點,那就是異步方法run沒有返回值,也就是說我們無法直接獲取它的執行結果,只能通過共享變量或者線程間通信等方式來獲取。好消息是通過使用Callable和Future,我們可以方便的獲得線程的執行結果。
Callable接口與Runnable接口類似,區別在于它定義的異步方法call有返回值。Callable接口的定義如下:

public interface Callable<V> {
    V call() throws Exception;
}

類型參數V即為異步方法call的返回值類型。

來看個簡單栗子:

public class CallableAndFuture {
    public static void main(String[] args) {
        Callable<Integer> callable = new Callable<Integer>() {
            public Integer call() throws Exception {
                return new Random().nextInt(100);
            }
        };
        FutureTask<Integer> future = new FutureTask<Integer>(callable);
        new Thread(future).start();
        try {
            Thread.sleep(5000);// 可能做一些事情
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

FutureTask實現了兩個接口,Runnable和Future,所以它既可以作為Runnable被線程執行,又可以作為Future得到Callable的返回值,那么這個組合的使用有什么好處呢?假設有一個很耗時的返回值需要計算,并且這個返回值不是立刻需要的話,那么就可以使用這個組合,用另一個線程去計算返回值,而當前線程在使用這個返回值之前可以做其它的操作,等到需要這個返回值時,再通過Future得到,豈不美哉!

Future可以對具體的Runnable或者Callable任務的執行結果進行取消、查詢是否完成以及獲取結果。可以通過get方法獲取執行結果,該方法會阻塞直到任務返回結果。Future接口的定義如下:

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

在Future接口中聲明了5個方法,每個方法的作用如下:

  • cancel方法用來取消任務,如果取消任務成功則返回true,如果取消任務失敗則返回false。參數mayInterruptIfRunning表示是否允許取消正在執行卻沒有執行完畢的任務,如果設置true,則表示可以取消正在執行過程中的任務。如果任務已經完成,則無論mayInterruptIfRunning為true還是false,此方法肯定返回false(即如果取消已經完成的任務會返回false);如果任務正在執行,若mayInterruptIfRunning設置為true,則返回true,若mayInterruptIfRunning設置為false,則返回false;如果任務還沒有執行,則無論mayInterruptIfRunning為true還是false,肯定返回true。

  • isCancelled方法表示任務是否被取消成功,如果在任務正常完成前被取消成功,則返回 true。

  • isDone方法表示任務是否已經完成,若任務完成,則返回true;

  • get()方法用來獲取執行結果,這個方法會阻塞,一直等到任務執行完才返回;

  • get(long timeout, TimeUnit unit)用來獲取執行結果,如果在指定時間內,還沒獲取到結果,就直接返回null。

    Future接口的實現類是FutureTask:

public class FutureTask<V> implements RunnableFuture<V>

FutureTask類實現了RunnableFuture接口,這個接口的定義如下:

public interface RunnableFuture<V> implements Runnable, Future<V> {
    void run();
}

可以看到RunnableFuture接口擴展了Runnable接口和Future接口。

FutureTask類有如下兩個構造器:

public FutureTask(Callable<V> callable) 
public FutureTask(Runnable runnable, V result) 


FutureTask通常與線程池配合使用,通常會創建一個包裝了Callable對象的FutureTask實例,并用submit方法將它提交到一個線程池去執行,我們可以通過FutureTask的get方法獲取返回結果。

public class CallableAndFuture {
    public static void main(String[] args) {
        ExecutorService threadPool = Executors.newSingleThreadExecutor();
        Future<Integer> future = threadPool.submit(new Callable<Integer>() {
            public Integer call() throws Exception {
                return new Random().nextInt(100);
            }
        });
        try {
            Thread.sleep(5000);// 可能做一些事情
            System.out.println(future.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

九、執行器

創建一個新線程涉及和操作系統的交互,因此會產生一定的開銷。在有些應用場景下,我們會在程序中創建大量生命周期很短的線程,這時我們應該使用線程池(thread pool)。通常,一個線程池中包含一些準備運行的空閑線程,每次將Runnable對象交給線程池,就會有一個線程執行run方法。當run方法執行完畢時,線程不會進入Terminated
狀態,而是在線程池中準備等下一個Runnable到來時提供服務。使用線程池統一管理線程可以減少并發線程的數目,線程數過多往往會在線程上下文切換上以及同步操作上浪費過多時間。

執行器類(java.util.concurrent.Executors)提供了許多靜態工廠方法來構建線程池。

1.線程池

在Java中,線程池通常指一個ThreadPoolExecutor對象,ThreadPoolExecutor類繼承了AbstractExecutorService類,而AbstractExecutorService抽象類實現了ExecutorService接口,ExecutorService接口又擴展了Executor接口。也就是說,Executor接口是Java中實現線程池的最基本接口。我們在使用線程池時通常不直接調用ThreadPoolExecutor類的構造方法,二回使用Executors類提供給我們的靜態工廠方法,這些靜態工廠方法內部會調用ThreadPoolExecutor的構造方法,并為我們準備好相應的構造參數。

Executor是類中的以下三個方法會返回一個實現了ExecutorService接口的ThreadPoolExecutor類的對象:


newCachedThreadPool() //返回一個帶緩存的線程池,該池在必要的時候創建線程,在線程空閑60s后終止線程
newFixedThreadPool(int threads) //返回一個線程池,線程數目由threads參數指明
newSingleThreadExecutor() //返回只含一個線程的線程池,它在一個單一的線程中依次執行各個任務
newScheduledThreadPool()//包含預定執行而構建的線程池
  • 對于newCachedThreadPool方法返回的線程池:對每個任務,若有空閑線程可用,則立即讓它執行任務;若沒有可用的空閑線程,它就會創建一個新線程并加入線程池中;
  • newFixedThreadPool方法返回的線程池里的線程數目由創建時指定,并一直保持不變。若提交給它的任務多于線程池中的空閑線程數目,那么就會把任務放到隊列中,當其他任務執行完畢后再來執行它們;
  • newSingleThreadExecutor會返回一個大小為1的線程池,由一個線程執行提交的任務。

以下方法可將一個Runnable對象或Callable對象提交給線程池:

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

調用submit方法會返回一個Future對象,可通過這個對象查詢該任務的狀態。我們可以在這個Future對象上調用isDone、cancle、isCanceled等方法(Future接口會在下面進行介紹)。第一個submit方法提交一個Callable對象到線程池中;第二個方法提交一個Runnable對象,并且Future的get方法在完成的時候返回指定的result對象。

當我們使用完線程池時,就調用shutdown方法,該方法會啟動該線程池的關閉例程。被關閉的線程池不能再接受新的任務,當關閉前已存在的任務執行完畢后,線程池死亡。shutdownNow方法可以取消線程池中尚未開始的任務并嘗試中斷所有線程池中正在運行的線程。

在使用線程池時,我們通常應該按照以下步驟來進行:

  • 調用Executors中相關方法構建一個線程池;
  • 調用submit方法提交一個Runnable對象或Callable對象到線程池中;
  • 若想要取消一個任務,需要保存submit返回的Future對象;
  • 當不再提交任何任務時,調用shutdown方法。
package test;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);  
  for (int i = 0; i < 10; i++) {  
   final int index = i;  
   fixedThreadPool.execute(new Runnable() {  
    public void run() {  
     try {  
      System.out.println(index);  
      Thread.sleep(2000);  
     } catch (InterruptedException e) {  
      e.printStackTrace();  
     }  
    }  
   });  
  }  
 }  
} 

2.預定執行

ScheduledExecutorService接口含有為預定執行(Scheduled Execution)或重復執行的任務專門設計的方法。Executors類的newScheduledThreadPool和newSingleThreadScheduledExecutor方法會返回實現了ScheduledExecutorService接口的對象。可以使用以下方法來預定執行的任務:

ScheduledFuture<V> schedule(Callable<V> task, long time, TimeUnit unit)
ScheduledFuture<?> schedule(Runnable task, long time, TimeUnit unit)
//以上兩個方法預定在指定時間過后執行任務
SchedukedFuture<?> scheduleAtFixedRate(Runnable task, long initialDelay, long period, TimeUnit unit) //在指定的延遲(initialDelay)過后,周期性地執行給定任務
ScheduledFuture<?> scheduleWithFixedDelay(Runnable task, long initialDelay, long delay, TimeUnit unit) //在指定延遲(initialDelay)過后周期性的執行任務,每兩個任務間的間隔為delay指定的時間
package test;  
import java.util.concurrent.Executors;  
import java.util.concurrent.ScheduledExecutorService;  
import java.util.concurrent.TimeUnit;  
public class ThreadPoolExecutorTest {  
 public static void main(String[] args) {  
  ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);  
  scheduledThreadPool.schedule(new Runnable() {  
   public void run() {  
    System.out.println("delay 3 seconds");  
   }  
  }, 3, TimeUnit.SECONDS);  
 }  
}  

3.控制任務組

對ExecutorService對象調用invokeAny方法可以把一個Callable對象集合提交到相應的線程池中執行,并返回某個已經完成的任務的結果,該方法的定義如下:

T invokeAny(Collection<Callable<T>> tasks)
T invokeAny(Collection<Callable<T>> tasks, long timeout, TimeUnit unit)

該方法可以指定一個超時參數。這個方法的不足在于我們無法知道它返回的結果是哪個任務執行的結果。如果集合中的任意Callable對象的執行結果都能滿足我們的需求的話,使用invokeAny方法是很好的。

invokeAll方法也會提交Callable對象集合到相應的線程池中,并返回一個Future對象列表,代表所有任務的解決方案。該方法的定義如下:

List<Future<T>> invokeAll(Collection<Callable<T>> tasks)
List<Future<T>> invokeAll(Collection<Callable<T>> tasks, long timeout, TimeUnit unit)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,546評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,570評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,505評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,017評論 1 313
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,786評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,219評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,287評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,438評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,971評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,796評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,995評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,540評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,230評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,662評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,918評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,697評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,991評論 2 374

推薦閱讀更多精彩內容