Java多執行緒和併發問題集

貓尾巴發表於2018-06-24

併發是指什麼

併發是程式同時執行多個計算的能力。 這可以通過將計算分佈在機器的可用CPU核心上(多核CPU支援)或甚至通過同一網路內的不同機器來實現(後臺分散式)。

程式和執行緒有什麼區別

程式是作業系統提供的執行環境,具有自己的一組專用資源(例如記憶體,開啟的檔案等)。 執行緒相對程式而言,執行緒存活在一個程式中,並與程式的其他執行緒共享其資源(記憶體,開啟的檔案等)。 在不同執行緒之間共享資源的能力使執行緒更適合於對效能要求的任務。

在Java中,什麼是程式和執行緒

在Java中,程式對應於正在執行的Java虛擬機器(JVM),而執行緒駐留在JVM中,並且可以在執行時動態地由Java應用程式建立和停止。

什麼是 scheduler(排程程式)

scheduler是一種排程演算法的實現,它管理程式和執行緒對處理器或某些I / O通道等有限資源的訪問。 大多數排程演算法的目標是為可用程式/執行緒提供某種負載均衡,以確保每個程式/執行緒獲得適當的時間片來專門訪問請求的資源。

一個Java程式至少有多少個執行緒

每個Java程式都在主執行緒中執行; 因此每個Java應用程式至少有一個執行緒。

Java應用程式如何訪問當前執行緒

當前執行緒可以通過呼叫JDK中提供的類java.lang.Thread的靜態方法currentThread()來訪問:

public class MainThread {
    public static void main(String[] args) {
        long id = Thread.currentThread().getId();
        String name = Thread.currentThread().getName();
        ...
    }
}
複製程式碼

每個Java執行緒都有哪些屬性

每個Java執行緒都有以下屬性:

  • JVM中唯一的long型別識別符號
  • String型別的名稱
  • int型別的優先順序
  • 型別為java.lang.Thread.State的狀態
  • 執行緒所屬的執行緒組

執行緒組的目的是什麼

每個執行緒都屬於一組執行緒。 JDK類java.lang.ThreadGroup提供了一些方法來處理整組執行緒。 例如,通過這些方法,可以中斷執行緒組的所有執行緒或設定其最大優先順序。

執行緒可以擁有哪些狀態以及每個狀態的含義

  • NEW:尚未啟動的執行緒處於此狀態。
  • RUNNABLE:在Java虛擬機器中執行的執行緒處於此狀態。
  • BLOCKED:阻塞等待監視器鎖定的執行緒處於此狀態。
  • WAITING:無限期地等待另一個執行緒執行特定動作的執行緒處於這種狀態。
  • TIMED_WAITING:正在等待另一個執行緒執行動作達到指定等待時間的執行緒處於此狀態。
  • TERMINATED:已退出的執行緒處於此狀態。

我們如何設定執行緒的優先順序

執行緒的優先順序通過setPriority(int)方法設定。 要將優先順序設定為最大值,我們使用常量Thread.MAX_PRIORITY。要將其設定為最小值,我們使用常量Thread.MIN_PRIORITY,因為這些值在不同的JVM實現之間可能會有所不同。

Java中如何建立一個執行緒

基本上,有兩種方法可以在Java中建立執行緒。 第一個是編寫一個擴充套件JDK類java.lang.Thread並呼叫其方法start()的類:

public class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }
    @Override
    public void run() {
        System.out.println("Executing thread "+Thread.currentThread().getName());
    }
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread = new MyThread("myThread");
        myThread.start();
    }
}
複製程式碼

第二種方法是實現介面java.lang.Runnable並將此實現作為引數傳遞給java.lang.Thread的建構函式:

public class MyRunnable implements Runnable {
    public void run() {
        System.out.println("Executing thread "+Thread.currentThread().getName());
    }
    public static void main(String[] args) throws InterruptedException {
        Thread myThread = new Thread(new MyRunnable(), "myRunnable");
        myThread.start();
    }
}
複製程式碼

我們如何停止Java中的執行緒

public class StopThread {
    public static void main(String[] arg) throws InterruptedException {
        MyStopThread myStopThread = new MyStopThread();
        myStopThread.start();
        Thread.sleep(1000 * 5);
        myStopThread.stopThread();
    }

    private static class MyStopThread extends Thread {
        private volatile Thread stopIndicator;

        public void start() {
            stopIndicator = new Thread(this);
            stopIndicator.start();
        }

        public void stopThread() {
            stopIndicator = null;
        }

        @Override
        public void run() {
            Thread thisThread = Thread.currentThread();
            while (thisThread == stopIndicator) {
                try {
                    System.out.println("wait...");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
複製程式碼

輸出內容:

Java多執行緒和併發問題集

為什麼一個執行緒不能通過呼叫stop()方法來停止

不應該使用java.lang.Thread的廢棄方法stop()停止執行緒,因為此方法的呼叫會導致執行緒解鎖其已獲取的所有監視器。 如果任何一個由釋放鎖保護的物件處於不一致狀態,則此狀態對所有其他執行緒都可見。 當其他執行緒處理這個不一致的物件時,這可能導致不可測的行為。

是否有可能啟動一個執行緒兩次

不能,在通過呼叫start()方法啟動執行緒後,第二次呼叫start()將丟擲IllegalT hreadStateException異常。

以下程式碼的輸出是什麼

public class MultiThreading {
    private static class MyThread extends Thread {
        public MyThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName());
        }
        public static void main(String[] args) {
            MyThread myThread = new MyThread("myThread");
            myThread.run();
        }
    }
}
複製程式碼

上面的程式碼輸出“main”而不是“myThread”。 從main()方法的第二行可以看出,我們錯誤地呼叫run()方法而不是start()。 因此,沒有新的執行緒啟動,run()方法依舊在主執行緒中執行。

什麼是守護執行緒

當所有使用者執行緒(與守護執行緒相對)都終止時,JVM才會停止。當JVM決定是否停止時,不會考慮到守護執行緒的執行狀態。 因此,守護執行緒可以用於實現監視功能,只要所有使用者執行緒都停止了,守護執行緒就會被JVM停止:

public class Example {
    private static class MyDaemonThread extends Thread {
        public MyDaemonThread() {
            setDaemon(true);
        }
        @Override
        public void run() {
            while (true) {
                try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            }
    }
    public static void main(String[] args) throws InterruptedException {
                Thread thread = new MyDaemonThread();
                thread.start();
    }
}
複製程式碼

上面的示例應用程式將立即終止,即使守護程式執行緒仍在其while迴圈中執行。

是否有可能在普通使用者執行緒啟動後將其轉換為守護執行緒

使用者執行緒一旦啟動就無法轉換為守護執行緒。 在已經執行的執行緒例項上呼叫thread.setDaemon( true) 方法會導致IllegalThreadStateException異常。

busy waiting 告訴我們什麼

busy waiting 意味著通過執行一些主動計算來等待事件的實現,這些計算使執行緒/程式佔用處理器,儘管它已經可以被排程程式從中移除。 busy waiting 的一個例子是在迴圈內花費等待時間,該迴圈一次又一次地確定當前時間,直到達到某個時間點:

Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
                long millisToStop = System.currentTimeMillis() + 5000;
                long currentTimeMillis = System.currentTimeMillis();
                while (millisToStop > currentTimeMillis) {
                    currentTimeMillis = System.currentTimeMillis();
                } 
        }
});
複製程式碼

我們如何防止 busy waiting

防止 busy waiting 的一種方法是將當前執行緒休眠一段給定的時間。 這可以通過呼叫方法java.lang.Thread.sleep(long) 來完成,將毫秒數作為引數休眠。

我們可以使用Thread.sleep()進行實時處理嗎

傳遞給Thread.sleep(long) 呼叫的毫秒數只是scheduler指示當前執行緒不需要執行多長時間。 根據實際的實現情況,scheduler可能會讓執行緒再提前幾毫秒執行一次。 因此,Thread.sleep()的呼叫不應該用於實時處理。

如何在使用Thread.sleep()之前將執行緒喚醒

java.lang.Thread的interrupt()方法中斷正在睡眠的執行緒。 已通過呼叫Thread.sleep() 進入睡眠狀態的中斷執行緒被InterruptedException喚醒:

public class InterruptThread implements Runnable {
    public static void main(String[] arg) throws InterruptedException {
        Thread myThread = new Thread(new InterruptThread(), "myThread");
        myThread.start();

        System.out.println("[" + Thread.currentThread().getName() + "] Sleeping in main ←  thread for 5s...");
        Thread.sleep(5000);

        System.out.println("[" + Thread.currentThread().getName() + "] Interrupting ←  myThread");
        myThread.interrupt();
    }

    @Override
    public void run() {
        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("[" + Thread.currentThread().getName() + "] ←  Interrupted by exception!");
        }
    }
}
複製程式碼

輸出內容:

[main] Sleeping in main ←  thread for 5s...
[main] Interrupting ←  myThread
[myThread] ←  Interrupted by exception!
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at com.zpw.test.thread.InterruptThread.run(InterruptThread.java:23)
	at java.lang.Thread.run(Thread.java:745)

Process finished with exit code 0
複製程式碼

一個執行緒如何查詢它是否被中斷

如果執行緒不在像Thread.sleep() 這樣會丟擲InterruptedException的方法內,執行緒可以通過呼叫從java.lang.Thread繼承的靜態方法Thread.interrupted() 或方法isInterrupted() 來查詢它是否已被中斷 。

應該如何處理InterruptedException

像sleep()和join()這樣的方法會丟擲一個InterruptedException來告訴呼叫者另一個執行緒已經中斷了這個執行緒。 在大多數情況下,這是為了告訴當前執行緒停止當前的計算並以異常的方式完成它們。 因此,通過捕獲異常並僅將其記錄到控制檯或某些日誌檔案來忽略異常通常不是處理這種異常的適當方式。 這個異常的問題是,Runnable介面的run()方法不允許run()丟擲任何異常。 所以重新丟擲它並沒有意義。 這意味著run()的實現必須自己處理這個檢查的異常,這通常會導致它被捕獲並被忽略的事實。

在啟動一個子執行緒之後,我們如何在父執行緒中等待子執行緒的終止

等待執行緒終止是通過呼叫執行緒例項變數上的join()方法來完成的:

Thread thread = new Thread(new Runnable() {
        @Override
        public void run() {
        }
});
thread.start();
thread.join();
複製程式碼

以下程式的輸出是什麼

public class MyThreads {
    private static class MyDaemonThread extends Thread {
        public MyDaemonThread() {
                setDaemon(true);
        }
        @Override
        public void run() {
        try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new MyDaemonThread();
        thread.start();
        thread.join();
        System.out.println(thread.isAlive());
    }
}
複製程式碼

上述程式碼的輸出是“false”。 儘管MyDaemonThread的例項是一個守護執行緒,但呼叫join()會導致主執行緒等待,直到守護執行緒的執行完成。 因此,線上程例項上呼叫isAlive()會發現守護執行緒不再執行。

當未捕獲的異常離開run()方法時會發生什麼

可能會發生一個未經檢查的異常從run() 方法中逃脫。 在這種情況下,執行緒被Java虛擬機器停止。 可以通過註冊一個實現介面UncaughtExceptionHandler的例項作為異常處理程式來捕獲此異常。 這可以通過呼叫靜態方法Thread.setDefaultUncaughtExceptionHandler(Thread.Unc aughtExceptionHandler)來完成,該方法告訴JVM線上程本身沒有註冊特定處理程式的情況下使用提供的處理程式,或通過線上程例項本身呼叫setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler)。

什麼是 shutdown hook

shutdown hook 是在JVM關閉時執行的執行緒。 它可以通過在Runtime例項上呼叫addShutdownHook(Runnable)來註冊:

Runtime.getRuntime().addShutdownHook(new Thread() {
    @Override
    public void run() {
    }
});
複製程式碼

關於 synchronized 關鍵字的用途

當你必須實現對某個資源的獨佔訪問時,如某些靜態值或某個檔案引用,與獨佔資源一起工作的程式碼可以包含一個 synchronized 塊:

synchronized (SynchronizedCounter.class) {
    counter++;
}
複製程式碼

同步方法獲得誰的內在鎖

synchronized方法獲取該方法物件的內部鎖並在方法返回時釋放它。 即使該方法丟擲異常,內部鎖也被釋放。 因此,一個同步方法等於以下程式碼:

public void method() {
    synchronized(this) {
        ...
    }
}
複製程式碼

建構函式是否可以同步

建構函式不能同步。 導致語法錯誤的原因是隻有構造執行緒才能訪問正在構建的物件。

基本型別變數可以用做內部鎖嗎

基本型別變數不能用做內部鎖。

內在鎖可重入嗎

可以。內部鎖可以一次又一次地被相同的執行緒訪問。 否則,獲取鎖的程式碼將不得不注意,它不會意外地嘗試獲取它已獲取的鎖。

通過原子操作了解什麼

原子操作要麼完全執行,要麼根本不執行。

語句c++是原子性的嗎

不是,整數變數的增量由多個操作組成。 首先,我們必須載入c的當前值,然後增加它,然後最後將新值存回。 執行此增量的當前執行緒可能會在這三個步驟中的任何一個之間中斷,因此此操作不是原子操作。

Java中的原子操作是什麼

Java語言提供了一些基本的操作,因此可用於確保併發執行緒始終看到相同的值:

  • 以引用變數和原始變數(long和double除外)的讀取和寫入操作
  • 對所有宣告為volatile的變數進行讀寫操作

以下實現是執行緒安全的

public class DoubleCheckedSingleton {
        private DoubleCheckedSingleton instance = null;
        public DoubleCheckedSingleton getInstance() {
                if(instance == null) {
                    synchronized (DoubleCheckedSingleton.class) {
                        if(instance == null) {
                            instance = new DoubleCheckedSingleton();
                        }
                    }
                }
                return instance;
        }
}
複製程式碼

上面的程式碼不是執行緒安全的。 儘管它在同步塊內再次檢查例項的值(出於效能原因),但JIT編譯器可以重新排列位元組碼,以便在建構函式完成其執行之前設定對例項的引用。 這意味著getInstance()方法返回一個可能尚未完全初始化的物件。 為了讓程式碼是執行緒安全的,關鍵字volatile可以在Java 5以後用於例項變數。 標記為volatile的變數只有在物件的建構函式完成完成後才會被其他執行緒看到。

deadlock 是什麼

死鎖是兩個(或更多)執行緒在等待另一個執行緒釋放已鎖定的資源的情況,而執行緒本身鎖定了另一個執行緒正在等待的資源:執行緒1:鎖定資源A ,等待資源B;執行緒2:鎖定資源B,等待資源A。

死鎖發生的前提

通常可以確定以下死鎖要求:

  • 相互排斥:有一種資源只能在任何時間點由一個執行緒訪問。
  • 資源保持:當鎖定一個資源時,該執行緒試圖獲取另一個獨佔資源上的另一個鎖。
  • 不搶佔:沒有任何機制,如果某個執行緒在特定時間段內持有鎖,則釋放該資源。
  • 迴圈等待:在執行時期間會出現一個群集,其中兩個(或更多)執行緒互相都在另一個執行緒上等待以釋放它已鎖定的資源。

是否有可能防止死鎖

為了防止死鎖,必須消除一個(或多個)死鎖要求:

  • 相互排斥:在某些情況下,可以通過使用樂觀鎖定來防止相互排斥。
  • 資源儲存:當執行緒無法獲得所有排他鎖時,它可能會釋放其所有排他鎖。
  • 不搶佔:對排他鎖使用超時在給定時間後釋放鎖。
  • 迴圈等待:當所有執行緒以相同順序獲得所有排它鎖時,不會發生迴圈等待。

是否有可能實現死鎖檢測

當所有獨佔鎖都被監視並建模為定向圖時,死鎖檢測系統可以搜尋兩個執行緒,每個執行緒都在另一個執行緒上等待以釋放它已鎖定的資源。 等待的執行緒然後可以被某種異常強制釋放另一個執行緒正在等待的鎖。

什麼是活鎖

活鎖是兩個或多個執行緒通過響應由另一個執行緒引起的操作而彼此阻塞的情況。 與死鎖情況相反,兩個或更多執行緒在一個特定狀態下等待,參與活鎖的執行緒以阻止正常工作進度的方式更改其狀態。 一個例子就是兩個執行緒試圖獲得兩個鎖的情況,但是當他們無法獲得第二個鎖時釋放第一個獲得的鎖。 現在可能發生兩個執行緒同時嘗試獲取第一個執行緒。 由於只有一個執行緒成功,第二個執行緒可能成功獲取第二個鎖。 現在兩個執行緒都擁有兩個不同的鎖,但由於兩者都想擁有這兩個鎖,它們釋放它們的鎖並從頭開始重試。 這種情況可能會一次又一次地發生。

我們通過執行緒飢餓瞭解什麼

具有較低優先順序的執行緒比具有較高優先順序的執行緒獲得較少的執行時間。 當優先順序較低的執行緒執行長期持久的計算時,可能會發生這些執行緒沒有足夠的時間來及時完成其計算。 他們似乎“餓死”,因為具有更高優先順序的執行緒竊取他們的計算時間。

同步塊可能導致執行緒飢餓

沒有定義執行緒可以進入同步塊的順序。 所以理論上可能發生的情況是,如果許多執行緒正在等待同步塊的入口,則某些執行緒必須等待比其他執行緒更長的時間。 因此他們沒有足夠的計算時間來及時完成工作。

術語競賽條件來理解什麼

競爭條件描述了一些現象,其中一些多執行緒實現的結果取決於參與執行緒的確切時間行為。 在大多數情況下,不希望出現這種行為,因此術語競爭條件也意味著由於缺少執行緒同步而導致的錯誤會導致不同的結果。 一個競爭條件的簡單例子是由兩個併發執行緒增加一個整數變數。 由於該操作由多個單一操作和原子操作組成,因此可能會發生這兩個執行緒讀取並遞增相同的值。 在這個併發增量之後,整數變數的數量不會增加2,而只會增加1。

公平鎖是什麼

當選擇下一個將屏障傳遞給某個獨佔資源的執行緒時,公平鎖會將執行緒的等待時間考慮在內。 Java SDK提供了一個公平鎖的示例實現:java.util.concurrent。locks.ReentrantLock。 通過將建構函式使用布林標誌設定為true,則ReentrantLock授予訪問最長等待執行緒的許可權。

每個物件從java.lang.Object繼承的哪兩種方法可用於實現簡單的生產者/消費者方案

當工作執行緒完成當前任務並且新任務的佇列為空時,它可以通過獲取佇列物件的內部鎖並通過呼叫方法wait()來釋放處理器。 該執行緒將被某個生產者執行緒喚醒,該執行緒已將新任務放入佇列中,並再次獲取佇列物件上的相同內部鎖並呼叫notify()。

notify()和notifyAll()有什麼區別

這兩種方法都用來喚醒一個或多個通過呼叫wait()使自己進入睡眠狀態的執行緒。 雖然notify()只喚醒其中一個等待的執行緒,notifyAll()喚醒所有等待的執行緒。

如何通過呼叫notify()來確定哪個執行緒被喚醒

如果有多個執行緒正在等待,則不會指定哪個執行緒將通過呼叫notify()來喚醒。 因此,程式碼不應該依賴任何具體的JVM實現。

以下程式碼是從某個佇列實現中檢索整數值的正確方法嗎

public Integer getNextInt() {
    Integer retVal = null;
    synchronized (queue) {
        try {
            while (queue.isEmpty()) {
                queue.wait();
            }
        } catch (InterruptedException e) {
        }
    }
    synchronized (queue) {
        retVal = queue.poll();
        if (retVal == null) {
            System.err.println("retVal is null");
            throw new IllegalStateException();
        }
    }
    return retVal;
}
複製程式碼

儘管上面的程式碼使用佇列作為物件監視器,但它在多執行緒環境中的行為不正確。 原因是它有兩個獨立的同步塊。 當另一個呼叫notifyAll()的執行緒在第6行喚醒兩個執行緒時,兩個執行緒都會相繼輸入第二個同步塊。它的第二個塊現在只有一個新的值,因此第二個執行緒將輪詢一個空的佇列並將null作為返回值。

是否有可能檢查某個執行緒是否對某個給定物件持有監視器鎖

類java.lang.Thread提供了返回true的靜態方法Thread.holdsLock(Object) ,當且僅當當前執行緒持有作為方法呼叫的引數給定的物件上的鎖時才返回true。

Thread.yield()方法的作用

對靜態方法Thread.yield() 的呼叫為排程器提供了一條提示,即當前執行緒願意釋放處理器。 排程器可以自由地忽略這個提示。 由於沒有定義哪個執行緒在呼叫Thread.yield()後會得到處理器,因此甚至可能發生當前執行緒變為要執行的“下一個”執行緒。

將物件例項從一個執行緒傳遞到另一個執行緒時需要考慮什麼

線上程之間傳遞物件時,必須注意這些物件不能同時由兩個執行緒操縱。 一個例子是一個Map實現,其鍵/值對由兩個併發執行緒修改。 為了避免併發修改的問題,你可以設計一個物件為不可變的。

為了實現一個不可變的類,你必須遵循哪些規則

  • 所有欄位應該是final的和private。
  • 不應該有setter方法。
  • 為了防止子類違反不可變性原則,類本身應該被宣佈為final。
  • 如果欄位不是原始型別,而是對另一個物件的引用:
    • 不應該有一個getter方法將參考直接暴露給呼叫者。
    • 不要改變引用的物件(或者至少改變這些引用對物件的客戶端不可見)。

類java.lang.ThreadLocal的目的是什麼

由於記憶體在不同的執行緒之間共享,ThreadLocal提供了一種為每個執行緒單獨儲存和檢索值的方法。 ThreadLocal的實現儲存併為每個執行緒獨立檢索值,以便當執行緒A儲存值A1並且執行緒B將值B1儲存在同一個ThreadLocal例項中時,執行緒A稍後從此ThreadLocal例項中檢索值A1,並且執行緒B檢索值B1。

java.lang.ThreadLocal有哪些可能的用例

ThreadLocal的例項可用於在整個應用程式中傳輸資訊,而無需將它由方法傳遞給方法。 例子就是在ThreadLocal的一個例項中傳輸安全/登入資訊,這樣每個方法都可以訪問它。 另一個用例是傳輸事務資訊或一般物件,這些物件應該可以在所有方法中訪問,而無需將它們從方法傳遞到方法。

是否可以通過使用多執行緒來提高應用程式的效能

如果我們有多個CPU核心可用,如果可以通過可用的CPU核心對計算進行並行化,則可以通過多執行緒來提高應用程式的效能。 一個例子是縮放儲存在本地目錄結構中的所有影象的應用程式。 生產者/消費者實現可以使用單個執行緒來掃描目錄結構以及執行實際縮放操作的一群工作執行緒,而不是一個接一個地遍歷所有映像。 另一個例子是一個對映網頁的應用程式。 生產者執行緒可以解析第一個HTML頁面,並將它找到的連結釋出到佇列中,而不是一個接一個載入HTML頁面。 工作執行緒監視佇列並載入解析器找到的網頁。 當工作執行緒等待頁面完全載入時,其他執行緒可以使用CPU來解析已載入的頁面併發出新的請求。

術語可伸縮性代表什麼

可伸縮性意味著程式通過增加更多資源來提高效能的能力。

是否有可能通過使用多個處理器來計算應用程式的理論最大加速度

Amdahl’s law provides a formula to compute the theoretical maximum speed up by providing multiple processors to an applica- tion.ThetheoreticalspeedupiscomputedbyS(n) =1 /(B + (1-B)/n)wherendenotesthenumberofprocessorsandB the fraction of the program that cannot be executed in parallel. When n converges against infinity, the term (1-B)/n converges against zero. Hence the formula can be reduced in this special case to 1/B. As we can see, the theoretical maximum speedup behaves reciprocal to the fraction that has to be executed serially. This means the lower this fraction is, the more theoretical speedup can be achieved.

鎖爭奪

當兩個或兩個以上的執行緒競爭鎖時,會發生鎖爭奪。 排程器必須決定它是否允許執行緒等待休眠,並執行上下文切換以讓另一個執行緒佔用CPU,或讓等待執行緒 busy-waiting效率更高。 兩種方式都將空閒時間引入劣質執行緒。

減少鎖爭奪

在某些情況下,通過應用以下技術之一可以減少鎖爭用:

  • 鎖的範圍縮小。
  • 獲取特定鎖的次數減少(鎖分割)。
  • 使用硬體支援的樂觀鎖操作而不是同步。
  • 儘可能避免同步。
  • 避免使用物件池。

下面的程式碼可以應用哪種技術來減少鎖爭奪

synchronized (map) {
    UUID randomUUID = UUID.randomUUID();
    Integer value = Integer.valueOf(42);
    String key = randomUUID.toString();
    map.put(key, value);
}
複製程式碼

上面的程式碼執行隨機UUID的計算以及將文字42轉換為同步塊內的Integer物件,儘管這兩行程式碼對當前執行緒是本地的並且不影響其他執行緒。 因此可以將它們移出同步塊:

UUID randomUUID = UUID.randomUUID();
Integer value = Integer.valueOf(42);
String key = randomUUID.toString();
synchronized (map) {
    map.put(key, value);
}
複製程式碼

鎖分裂技術

當使用一個鎖來同步對相同應用程式的不同方面的訪問時,鎖分割可能是減少鎖爭用的一種方式。 假設我們有一個類來實現我們應用程式的一些統計資料的計算。 該類的第一個版本在每個方法簽名中使用關鍵字synchronized,以便在多個併發執行緒損壞之前保護內部狀態。 這也意味著每個方法呼叫都可能導致鎖爭用,因為其他執行緒可能會嘗試同時獲取相同的鎖。 但是也可以將物件例項上的鎖分為每種方法中每種統計資料的幾個較小的鎖。 因此,嘗試遞增統計資料D1的執行緒T1線上程T2同時更新資料D2的同時不必等待鎖。

SDK類ReadWriteLock使用了哪種減少鎖爭用的技術

SDK類ReadWriteLock使用這樣一個技術,即如果沒有其他執行緒嘗試更新值時,併發執行緒不需要獲取鎖就可以讀取值。 這是通過一對鎖實現的,一個用於只讀操作,另一個用於寫入操作。 雖然只讀鎖可以通過多個執行緒獲得,但是實現保證了一旦寫入鎖被釋放,所有讀操作看到更新的值。

鎖條紋技術

在鎖分割中,我們為應用程式的不同方面引入了不同的鎖,與鎖分割相反,鎖條紋使用多個鎖來保護相同資料結構的不同部分。 此技術的一個示例是JDK的java.util.concurrent包中的類ConcurrentHashMap。 Map實現使用內部不同的桶來儲存其值。 儲存桶由值的鍵選擇。 ConcurrentHashMap現在使用不同的鎖來保護不同的雜湊桶。 因此,一個嘗試訪問第一個雜湊桶的執行緒可以獲取該桶的鎖,而另一個執行緒可以同時訪問第二個桶。 與HashMap的同步版本相比,此技術可以在不同執行緒在不同儲存桶上工作時提高效能。

CAS操作

CAS代表比較和交換,意味著處理器提供了一個單獨的指令,只有當提供的值等於當前值時才更新暫存器的值。 CAS操作可以用來避免同步,因為執行緒可以通過向CAS操作提供當前值和新值來嘗試更新值。 如果另一個執行緒同時更新了該值,則該執行緒的值不等於當前值,並且更新操作失敗。 執行緒然後讀取新值並再次嘗試。 這種方式通過樂觀的自旋等待交換了必要的同步。

哪些Java類使用CAS操作

包java.util.concurrent.atomic中的SDK類(如AtomicInteger或AtomicBoolean)在內部使用CAS操作來實現併發增量。

public class CounterAtomic {
    private AtomicLong counter = new AtomicLong();

    public void increment() {
        counter.incrementAndGet();
    }

    public long get() {
        return counter.get();
    }
}
複製程式碼

提供一個例子說明為什麼單執行緒應用程式的效能改進會導致多執行緒應用程式的效能下降

這種優化的一個突出例子是List實現,它將元素的數量儲存為一個單獨的變數。 這可以提高單執行緒應用程式的效能,因為size()操作不必遍歷所有元素,但可以直接返回當前元素數。 在多執行緒應用程式中,附加計數器必須由鎖保護,因為多個併發執行緒可能會將元素插入到列表中。 當列表的更新數量多於size()操作的呼叫時,額外鎖可能會降低效能。

物件池總是對多執行緒應用程式的效能改進

避免建新物件的物件池可以提高單執行緒應用程式的效能,因為通過向池中請求新物件來交換物件建立成本。 在多執行緒應用程式中,這樣的物件池必須具有對池的同步訪問許可權,並且鎖爭奪的額外成本可能會超過額外構建和垃圾收集新物件所節省的成本。 因此,物件池並不總是可以提高多執行緒應用程式的整體效能。

介面Executor和ExecutorServices之間的關係

介面Executor只定義了一個方法:execute(Runnable)。 此介面的實現將不得不在未來的某個時間執行給定的Runnable例項。 ExecutorService介面是Executor介面的擴充套件,提供了關閉底層實現的其他方法,以等待終止所有提交的任務,並允許提交Callable例項。

將新任務submit()給ExecutorService例項(其佇列已滿)時會發生什麼情況

由submit()的方法簽名指示,ExecutorService實現應該丟擲Rejected ExecutionException異常。

ScheduledExecutorService

介面ScheduledExecutorService擴充套件了介面ExecutorService,並新增了允許將新任務提交給應該在給定時間點執行的底層實現的方法。 有兩種方法可以排程一次性任務和兩種方法來建立和執行週期性任務。

構造一個帶有5個執行緒的執行緒池,它執行將會返回值的任務

SDK提供了一個工廠和實用類的Executors,它們通過靜態方法newFixedThreadPool(int nThreads)允許建立一個具有固定數量執行緒的執行緒池(MyCallable的實現被省略):

public static void main2(String[] args) throws InterruptedException, ExecutionException {
    ExecutorService executorService = Executors.newFixedThreadPool(5);
    Future<Integer>[] futures = new Future[5];
    for (int i = 0; i < futures.length; i++) {
        futures[i] = executorService.submit(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return Integer.valueOf(UUID.randomUUID().toString());
            }
        });
    }
    for (int i = 0; i < futures.length; i++) {
        Integer retVal = futures[i].get();
        System.out.println(retVal);
    }
    executorService.shutdown();
}
複製程式碼

Runnable和Callable之間有什麼區別

Runnable介面定義了沒有任何返回值的方法run(),而Callable介面允許方法run()返回一個值並丟擲一個異常。

java.util.concurrent.Future的用例

類java.util.concurrent.Future的例項用於表示非同步計算的結果,其結果不是立即可用的。 因此,該類提供了檢查非同步計算是否完成,取消任務和檢索實際結果的方法。 後者可以使用提供的兩個get() 方法完成。 第一個get() 方法在結果可用之前不接受任何引數和塊,而第二個get() 方法接受一個超時引數,如果結果在給定時間範圍內不可用,則該方法呼叫將返回。

HashMap和Hashtable之間有什麼區別,特別是關於執行緒安全性

Hashtable的方法都是同步的。HashMap則不是。 因此Hashtable是執行緒安全的,而HashMap不是執行緒安全的。 對於單執行緒應用程式,使用HashMap實現更高效。

有沒有簡單的方法來建立一個任意實現的Collection,List或Map的同步例項

實用程式類Collections提供了返回給定例項支援的執行緒安全的collection/list/map的方法synchronizedCollection(Collection),synchronizedList(List)和synchronizedMap(Map) 。

Semaphore

訊號量是一個資料結構,它維護一組必須通過競爭執行緒獲取的許可證。 因此可以使用訊號量來控制有多少執行緒同時訪問關鍵部分或資源。 因此,java.util.concurrent.Semaphore的建構函式將第一個引數作為執行緒競爭許可的數量。 其acquire()方法的每次呼叫都會嘗試獲取其中一個可用的許可證。 方法acquire()沒有任何引數塊,直到下一個許可證可用。 稍後,當執行緒在關鍵資源上完成其工作時,它可以通過呼叫Semaphore例項上的方法release()來釋放許可證。

訊號量維護一個許可集,可通過acquire()獲取許可(若無可用許可則阻塞),通過release()釋放許可,從而可能喚醒一個阻塞等待許可的執行緒。

與互斥鎖類似,訊號量限制了同一時間訪問臨界資源的執行緒的個數,並且訊號量也分公平訊號量與非公平訊號量。而不同的是,互斥鎖保證同一時間只會有一個執行緒訪問臨界資源,而訊號量可以允許同一時間多個執行緒訪問特定資源。所以訊號量並不能保證原子性。

訊號量的一個典型使用場景是限制系統訪問量。每個請求進來後,處理之前都通過acquire獲取許可,若獲取許可成功則處理該請求,若獲取失敗則等待處理或者直接不處理該請求。

訊號量的使用方法

  • acquire(int permits) 申請permits(必須為非負數)個許可,若獲取成功,則該方法返回並且當前可用許可數減permits;若當前可用許可數少於permits指定的個數,則繼續等待可用許可數大於等於permits;若等待過程中當前執行緒被中斷,則丟擲InterruptedException。
  • acquire() 等價於acquire(1)。
  • acquireUninterruptibly(int permits) 申請permits(必須為非負數)個許可,若獲取成功,則該方法返回並且當前可用許可數減permits;若當前許可數少於permits,則繼續等待可用許可數大於等於permits;若等待過程中當前執行緒被中斷,繼續等待可用許可數大於等於permits,並且獲取成功後設定執行緒中斷狀態。
  • acquireUninterruptibly() 等價於acquireUninterruptibly(1)。
  • drainPermits() 獲取所有可用許可,並返回獲取到的許可個數,該方法不阻塞。
  • tryAcquire(int permits) 嘗試獲取permits個可用許可,如果當前許可個數大於等於permits,則返回true並且可用許可數減permits;否則返回false並且可用許可數不變。
  • tryAcquire() 等價於tryAcquire(1)。
  • tryAcquire(int permits, long timeout, TimeUnit unit) 嘗試獲取permits(必須為非負數)個許可,若在指定時間內獲取成功則返回true並且可用許可數減permits;若指定時間內當前執行緒被中斷,則丟擲InterruptedException;若指定時間內可用許可數均小於permits,則返回false。
  • tryAcquire(long timeout, TimeUnit unit) 等價於tryAcquire(1, long timeout, TimeUnit unit)*
  • release(int permits) 釋放permits個許可,該方法不阻塞並且某執行緒呼叫release方法前並不需要先呼叫acquire方法。
  • release() 等價於release(1)。

注意:與wait/notify和await/signal不同,acquire/release完全與鎖無關,因此acquire等待過程中,可用許可滿足要求時acquire可立即返回,而不用像鎖的wait和條件變數的await那樣重新獲取鎖才能返回。或者可以理解成,只要可用許可滿足需求,就已經獲得了鎖。

CountDownLatch

SDK類CountDownLatch提供了一個同步輔助工具,可用於實現執行緒必須等待其他執行緒達到相同狀態以便所有執行緒都可以啟動的場景。 這是通過提供一個減量的同步計數器來完成的,直到達到零值。 CountDownLatch例項達到零後,所有執行緒都可以繼續。 這可以用來讓所有執行緒在給定的時間點啟動,方法是使用計數器的值1或等待多個執行緒完成。 在後一種情況下,計數器用執行緒數進行初始化,每個完成其工作的執行緒將鎖存器計數一次。

Java多執行緒程式設計中經常會碰到這樣一種場景——某個執行緒需要等待一個或多個執行緒操作結束(或達到某種狀態)才開始執行。比如開發一個併發測試工具時,主執行緒需要等到所有測試執行緒均執行完成再開始統計總共耗費的時間,此時可以通過CountDownLatch輕鬆實現。

public class CountDownLatchDemo {
  public static void main(String[] args) throws InterruptedException {
    int totalThread = 3;
    long start = System.currentTimeMillis();
    CountDownLatch countDown = new CountDownLatch(totalThread);
    for(int i = 0; i < totalThread; i++) {
      final String threadName = "Thread " + i;
      new Thread(() -> {
        System.out.println(String.format("%s\t%s %s", new Date(), threadName, "started"));
        try {
          Thread.sleep(1000);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        countDown.countDown();
        System.out.println(String.format("%s\t%s %s", new Date(), threadName, "ended"));
      }).start();;
    }
    countDown.await();
    long stop = System.currentTimeMillis();
    System.out.println(String.format("Total time : %sms", (stop - start)));
  }
}
複製程式碼

執行結果

Sun Jun 19 20:34:31 CST 2016  Thread 1 started
Sun Jun 19 20:34:31 CST 2016  Thread 0 started
Sun Jun 19 20:34:31 CST 2016  Thread 2 started
Sun Jun 19 20:34:32 CST 2016  Thread 2 ended
Sun Jun 19 20:34:32 CST 2016  Thread 1 ended
Sun Jun 19 20:34:32 CST 2016  Thread 0 ended
Total time : 1072ms
複製程式碼

可以看到,主執行緒等待所有3個執行緒都執行結束後才開始執行。

CountDownLatch工作原理相對簡單,可以簡單看成一個倒計數器,在構造方法中指定初始值,每次呼叫countDown()方法時將計數器減1,而await()會等待計數器變為0。CountDownLatch關鍵介面如下

  • countDown() 如果當前計數器的值大於1,則將其減1;若當前值為1,則將其置為0並喚醒所有通過await等待的執行緒;若當前值為0,則什麼也不做直接返回。
  • await() 等待計數器的值為0,若計數器的值為0則該方法返回;若等待期間該執行緒被中斷,則丟擲InterruptedException並清除該執行緒的中斷狀態。
  • await(long timeout, TimeUnit unit) 在指定的時間內等待計數器的值為0,若在指定時間內計數器的值變為0,則該方法返回true;若指定時間內計數器的值仍未變為0,則返回false;若指定時間內計數器的值變為0之前當前執行緒被中斷,則丟擲InterruptedException並清除該執行緒的中斷狀態。
  • getCount() 讀取當前計數器的值,一般用於除錯或者測試。

CyclicBarrier

記憶體屏障,它能保證屏障之前的程式碼一定在屏障之後的程式碼之前被執行。CyclicBarrier可以譯為迴圈屏障,也有類似的功能。CyclicBarrier可以在構造時指定需要在屏障前執行await的個數,所有對await的呼叫都會等待,直到呼叫await的次數達到預定指,所有等待都會立即被喚醒。

從使用場景上來說,CyclicBarrier是讓多個執行緒互相等待某一事件的發生,然後同時被喚醒。而上文講的CountDownLatch是讓某一執行緒等待多個執行緒的狀態,然後該執行緒被喚醒。

public class CyclicBarrierDemo {
  public static void main(String[] args) {
    int totalThread = 5;
    CyclicBarrier barrier = new CyclicBarrier(totalThread);
    
    for(int i = 0; i < totalThread; i++) {
      String threadName = "Thread " + i;
      new Thread(() -> {
        System.out.println(String.format("%s\t%s %s", new Date(), threadName, " is waiting"));
        try {
          barrier.await();
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        System.out.println(String.format("%s\t%s %s", new Date(), threadName, "ended"));
      }).start();
    }
  }
}
複製程式碼

執行結果如下

Sun Jun 19 21:04:49 CST 2016  Thread 1  is waiting
Sun Jun 19 21:04:49 CST 2016  Thread 0  is waiting
Sun Jun 19 21:04:49 CST 2016  Thread 3  is waiting
Sun Jun 19 21:04:49 CST 2016  Thread 2  is waiting
Sun Jun 19 21:04:49 CST 2016  Thread 4  is waiting
Sun Jun 19 21:04:49 CST 2016  Thread 4 ended
Sun Jun 19 21:04:49 CST 2016  Thread 0 ended
Sun Jun 19 21:04:49 CST 2016  Thread 2 ended
Sun Jun 19 21:04:49 CST 2016  Thread 1 ended
Sun Jun 19 21:04:49 CST 2016  Thread 3 ended
複製程式碼

從執行結果可以看到,每個執行緒都不會在其它所有執行緒執行await()方法前繼續執行,而等所有執行緒都執行await()方法後所有執行緒的等待都被喚醒從而繼續執行。

CyclicBarrier提供的關鍵方法如下

  • await() 等待其它參與方的到來(呼叫await())。如果當前呼叫是最後一個呼叫,則喚醒所有其它的執行緒的等待並且如果在構造CyclicBarrier時指定了action,當前執行緒會去執行該action,然後該方法返回該執行緒呼叫await的次序(getParties()-1說明該執行緒是第一個呼叫await的,0說明該執行緒是最後一個執行await的),接著該執行緒繼續執行await後的程式碼;如果該呼叫不是最後一個呼叫,則阻塞等待;如果等待過程中,當前執行緒被中斷,則丟擲InterruptedException;如果等待過程中,其它等待的執行緒被中斷,或者其它執行緒等待超時,或者該barrier被reset,或者當前執行緒在執行barrier構造時註冊的action時因為丟擲異常而失敗,則丟擲BrokenBarrierException。
  • await(long timeout, TimeUnit unit) 與await()唯一的不同點在於設定了等待超時時間,等待超時時會丟擲TimeoutException。
  • reset() 該方法會將該barrier重置為它的初始狀態,並使得所有對該barrier的await呼叫丟擲BrokenBarrierException。

CountDownLatch和CyclicBarrier之間有什麼區別

兩個SDK類都在內部維護一個由不同執行緒遞減的計數器。 執行緒一直等到內部計數器達到0並從此處繼續。 但是與CountDownLatch相反,一旦值達到零CyclicBarrier類將內部值重置為初始值。 由於該名稱指示CyclicBarrier的例項因此可以用於實現執行緒必須一次又一次地彼此等待的用例。

Phaser

CountDownLatch和CyclicBarrier都是JDK 1.5引入的,而Phaser是JDK 1.7引入的。Phaser的功能與CountDownLatch和CyclicBarrier有部分重疊,同時也提供了更豐富的語義和更靈活的用法。

Phaser顧名思義,與階段相關。Phaser比較適合這樣一種場景,一種任務可以分為多個階段,現希望多個執行緒去處理該批任務,對於每個階段,多個執行緒可以併發進行,但是希望保證只有前面一個階段的任務完成之後才能開始後面的任務。這種場景可以使用多個CyclicBarrier來實現,每個CyclicBarrier負責等待一個階段的任務全部完成。但是使用CyclicBarrier的缺點在於,需要明確知道總共有多少個階段,同時並行的任務數需要提前預定義好,且無法動態修改。而Phaser可同時解決這兩個問題。

public class PhaserDemo {
  public static void main(String[] args) throws IOException {
    int parties = 3;
    int phases = 4;
    final Phaser phaser = new Phaser(parties) {
      @Override  
      protected boolean onAdvance(int phase, int registeredParties) {  
          System.out.println("====== Phase : " + phase + " ======");  
          return registeredParties == 0;  
      }  
    };
    
    for(int i = 0; i < parties; i++) {
      int threadId = i;
      Thread thread = new Thread(() -> {
        for(int phase = 0; phase < phases; phase++) {
          System.out.println(String.format("Thread %s, phase %s", threadId, phase));
          phaser.arriveAndAwaitAdvance();
        }
      });
      thread.start();
    }
  }
}
複製程式碼

執行結果如下

Thread 0, phase 0
Thread 1, phase 0
Thread 2, phase 0
====== Phase : 0 ======
Thread 2, phase 1
Thread 0, phase 1
Thread 1, phase 1
====== Phase : 1 ======
Thread 1, phase 2
Thread 2, phase 2
Thread 0, phase 2
====== Phase : 2 ======
Thread 0, phase 3
Thread 1, phase 3
Thread 2, phase 3
====== Phase : 3 ======
複製程式碼

從上面的結果可以看到,多個執行緒必須等到其它執行緒的同一階段的任務全部完成才能進行到下一個階段,並且每當完成某一階段任務時,Phaser都會執行其onAdvance方法。

Phaser主要介面如下

  • arriveAndAwaitAdvance() 當前執行緒當前階段執行完畢,等待其它執行緒完成當前階段。如果當前執行緒是該階段最後一個未到達的,則該方法直接返回下一個階段的序號(階段序號從0開始),同時其它執行緒的該方法也返回下一個階段的序號。
  • arriveAndDeregister() 該方法立即返回下一階段的序號,並且其它執行緒需要等待的個數減一,並且把當前執行緒從之後需要等待的成員中移除。如果該Phaser是另外一個Phaser的子Phaser(層次化Phaser會在後文中講到),並且該操作導致當前Phaser的成員數為0,則該操作也會將當前Phaser從其父Phaser中移除。
  • arrive() 該方法不作任何等待,直接返回下一階段的序號。
  • awaitAdvance(int phase) 該方法等待某一階段執行完畢。如果當前階段不等於指定的階段或者該Phaser已經被終止,則立即返回。該階段數一般由arrive()方法或者arriveAndDeregister()方法返回。返回下一階段的序號,或者返回引數指定的值(如果該引數為負數),或者直接返回當前階段序號(如果當前Phaser已經被終止)。
  • awaitAdvanceInterruptibly(int phase) 效果與awaitAdvance(int phase)相當,唯一的不同在於若該執行緒在該方法等待時被中斷,則該方法丟擲InterruptedException。
  • awaitAdvanceInterruptibly(int phase, long timeout, TimeUnit unit) 效果與awaitAdvanceInterruptibly(int phase)相當,區別在於如果超時則丟擲TimeoutException。
  • bulkRegister(int parties) 註冊多個party。如果當前phaser已經被終止,則該方法無效,並返回負數。如果呼叫該方法時,onAdvance方法正在執行,則該方法等待其執行完畢。如果該Phaser有父Phaser則指定的party數大於0,且之前該Phaser的party數為0,那麼該Phaser會被註冊到其父Phaser中。
  • forceTermination() 強制讓該Phaser進入終止狀態。已經註冊的party數不受影響。如果該Phaser有子Phaser,則其所有的子Phaser均進入終止狀態。如果該Phaser已經處於終止狀態,該方法呼叫不造成任何影響。

通過使用Fork / Join框架可以解決哪些型別的任務

Fork / Join Framework基類java.util.concurrent.ForkJoinPool基本上是一個執行緒池,用於執行java.util.concurrent.ForkJoinTask的例項。 類ForkJoinTask提供了fork()和join()兩個方法。 雖然fork()用於啟動任務的非同步執行,但join()方法用於等待計算結果。 因此Fork / Join框架可以用來實現分治演算法,其中一個更復雜的問題被分成許多更小更容易解決的問題。

是否可以使用Fork / Join-Framework在陣列陣列中找到最小的數字

在數字陣列中尋找最小數字的問題可以通過使用分而治之演算法來解決。 可以很容易解決的最小問題是兩個數字的陣列,因為我們可以直接通過一個比較來確定兩個數字中較小的一個。 使用分而治之的方法,初始陣列被分成兩部分長度相等,並且這兩部分都被提供給擴充套件類ForkJoinTask的兩個RecursiveTask例項。 通過分解它們執行的兩個任務並直接解決問題,如果它們的陣列切片長度為2,或者它們再次遞迴地將陣列分割成兩部分並分叉兩個新的RecursiveTasks。 最後,每個任務例項返回其結果(通過直接計算或等待兩個子任務)。 根任務然後返回陣列中的最小數字。

RecursiveTask和Recursiveaction之間有什麼區別

與RecursiveTask相比,RecursiveAction的方法compute()不必返回值。 因此,當動作直接在某些資料結構上工作時,可以使用RecursiveAction,而不必返回計算值。

是否有可能通過執行緒池在Java 8中執行流操作

集合提供了方法parallelStream()來建立一個由執行緒池處理的流。 或者可以呼叫給定流上的中間方法parallel()來將順序流轉換為並行物件。

如何訪問使用並行流操作的執行緒池

用於並行流操作的執行緒池可以通過ForkJoinPool.commonPool()訪問。 這樣我們就可以用commonPool.getParallelism()來查詢它的並行級別。 該級別在執行時不能更改,但可以通過提供以下JVM引數進行配置:-Djava.util.concurrent.ForkJoinPool.common. parallelism=5.

Executor 介面

用於執行提交給它的 Runnable 任務的物件。此介面提供了一種將任務提交與每個任務的執行機制解耦的方法,包括執行緒使用,排程等的詳細資訊。通常使用 Executor 而不是顯式建立執行緒。 例如,呼叫一組任務不再使用

new Thread(new(RunnableTask())).start()
複製程式碼

而是使用:

Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());
複製程式碼

但是,Executor 介面並不嚴格要求執行是非同步的。 在最簡單的情況下,executor 可以立即在呼叫者的執行緒中執行提交的任務:

class DirectExecutor implements Executor {
    public void execute(Runnable r) {
        r.run();
    }
}
複製程式碼

更典型地,任務在某個執行緒中執行而不是在呼叫者執行緒執行。 下面的 executor 為每個任務生成一個新執行緒。

class ThreadPerTaskExecutor implements Executor {
   public void execute(Runnable r) {
     new Thread(r).start();
   }
}
複製程式碼

許多 Executor 實現類對如何以及何時排程任務施加了某種限制。 下面的 executor 將任務提交序列化到第二個 executor,說明了一個複合執行程式。

class SerialExecutor implements Executor {
   final Queue<Runnable> tasks = new ArrayDeque<Runnable>();
   final Executor executor;
   Runnable active;

   SerialExecutor(Executor executor) {
        this.executor = executor;
   }

   public synchronized void execute(final Runnable r) {
        tasks.offer(new Runnable() {
            public void run() {
                try {
                   r.run();
                } finally {
                    scheduleNext();
                }
           }
        });
        if (active == null) {
           scheduleNext();
        }
  }

   protected synchronized void scheduleNext() {
        if ((active = tasks.poll()) != null) {
           executor.execute(active);
        }
   }
}
複製程式碼

記憶體一致性效果:在將 Runnable 物件提交給 Executor 執行之前的操作,happen-before 被提交的 Runnable 的執行,也許在另一個執行緒中。

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}
複製程式碼

ExecutorService 介面

作為一個 Executor,提供管理終止的方法和可以生成 Future 以跟蹤一個或多個非同步任務進度的方法。

ExecutorService 可以被關閉,這將導致它拒絕接收新任務。ExecutorService 提供了兩種不同的方法來關閉。 shutdown 方法將允許先前提交的任務在終止之前保持執行,而 shutdownNow 方法阻止等待任務啟動並嘗試停止當前正在執行的任務。 終止時,executor 沒有正在執行的任務,沒有等待執行的任務,也沒有任何新任務可以提交。 未使用的 ExecutorService 應被關閉以允許回收其資源。

方法 submit 通過建立並返回可用於取消執行和/或等待完成的 Future 來擴充套件基本方法 Executor.execute(Runnable)。 方法 invokeAny 和 invokeAll 執行最常用的批量執行形式,執行一組任務,然後等待至少一個或全部完成。類 ExecutorCompletionService 可用於編寫這些方法的自定義變體。

Executors 類為此包中提供的 executor 服務提供工廠方法。

下面是網路服務的草圖,其中執行緒池中的執行緒為傳入的請求提供服務。 它使用預配置的 Executors.newFixedThreadPool 工廠方法:

class NetworkService implements Runnable {
    private final ServerSocket serverSocket;
    private final ExecutorService pool;

    public NetworkService(int port, int poolSize) throws IOException {
        serverSocket = new ServerSocket(port);
        pool = Executors.newFixedThreadPool(poolSize);
    }

    public void run() { // run the service
        try {
            for (; ; ) {
                pool.execute(new Handler(serverSocket.accept()));
            }
        } catch (IOException ex) {
            pool.shutdown();
        }
    }
}

class Handler implements Runnable {
    private final Socket socket;

    Handler(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        // read and service request on socket
    }
}
複製程式碼

以下方法分兩個階段關閉 ExecutorService,首先呼叫 shutdown 拒絕傳入的任務,然後在必要時呼叫 shutdownNow 以取消任何延遲的任務:

void shutdownAndAwaitTermination(ExecutorService pool) {
    pool.shutdown(); // Disable new tasks from being submitted
    try {
        // Wait a while for existing tasks to terminate
        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
            pool.shutdownNow(); // Cancel currently executing tasks
            // Wait a while for tasks to respond to being cancelled
            if (!pool.awaitTermination(60, TimeUnit.SECONDS))
                System.err.println("Pool did not terminate");
        }
    } catch (InterruptedException ie) {
        // (Re-)Cancel if current thread also interrupted
        pool.shutdownNow();
        // Preserve interrupt status
        Thread.currentThread().interrupt();
    }
}
複製程式碼

記憶體一致性效果:在將 Runnable 或 Callable 任務提交到 ExecutorService 之前,執行緒中的操作 happen-before 該任務所採取的任何行動,而該任務 happen-before 通過 Future.get() 檢索的結果。

// 請求關閉、發生超時或者當前執行緒中斷,無論哪一個首先發生之後,都將導致阻塞,直到所有任務完成執行。
boolean awaitTermination(long timeout, TimeUnit unit)
// 執行給定的任務,當所有任務完成時,返回保持任務狀態和結果的 Future 列表。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
// 執行給定的任務,當所有任務完成或超時期滿時(無論哪個首先發生),返回保持任務狀態和結果的 Future 列表。
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
// 執行給定的任務,如果某個任務已成功完成(也就是未丟擲異常),則返回其結果。
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
// 執行給定的任務,如果在給定的超時期滿前某個任務已成功完成(也就是未丟擲異常),則返回其結果。
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit)
// 如果此執行程式已關閉,則返回 true。
boolean isShutdown()
// 如果關閉後所有任務都已完成,則返回 true。
boolean isTerminated()
// 啟動一次順序關閉,執行以前提交的任務,但不接受新任務。
void shutdown()
// 試圖停止所有正在執行的活動任務,暫停處理正在等待的任務,並返回等待執行的任務列表。
List<Runnable> shutdownNow()
// 提交一個返回值的任務用於執行,返回一個表示任務的未決結果的 Future。
<T> Future<T> submit(Callable<T> task)
// 提交一個 Runnable 任務用於執行,並返回一個表示該任務的 Future。
Future<?> submit(Runnable task)
// 提交一個 Runnable 任務用於執行,並返回一個表示該任務的 Future。
<T> Future<T> submit(Runnable task, T result)
複製程式碼

AbstractExecutorService 抽象類

提供 ExecutorService 執行方法的預設實現。 此類使用 newTaskFor 返回的 RunnableFuture 實現 submit, invokeAny 和 invokeAll 方法,預設為此中提供的 FutureTask 類包。 例如, submit(Runnable) 的實現會建立一個執行並返回的關聯 RunnableFuture。 子類可以覆蓋 newTaskFor 方法,以返回 FutureTask 以外的 RunnableFuture 實現。

擴充套件示例。 下面是一個類的草圖,它定製 ThreadPoolExecutor 以使用 CustomTask 類而不是預設的 FutureTask:

public class CustomThreadPoolExecutor extends ThreadPoolExecutor {
    static class CustomTask<V> implements RunnableFuture<V> {...
    }

    protected <V> RunnableFuture<V> newTaskFor(Callable<V> c) {
        return new CustomTask<V>(c);
    }

    protected <V> RunnableFuture<V> newTaskFor(Runnable r, V v) {
        return new CustomTask<V>(r, v);
    }
    // ... add constructors, etc.
}
複製程式碼
public abstract class AbstractExecutorService implements ExecutorService {

    /**
     * Returns a {@code RunnableFuture} for the given runnable and default
     * value.
     *
     * @param runnable the runnable task being wrapped
     * @param value the default value for the returned future
     * @param <T> the type of the given value
     * @return a {@code RunnableFuture} which, when run, will run the
     * underlying runnable and which, as a {@code Future}, will yield
     * the given value as its result and provide for cancellation of
     * the underlying task
     * @since 1.6
     */
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

    /**
     * Returns a {@code RunnableFuture} for the given callable task.
     *
     * @param callable the callable task being wrapped
     * @param <T> the type of the callable's result
     * @return a {@code RunnableFuture} which, when run, will call the
     * underlying callable and which, as a {@code Future}, will yield
     * the callable's result as its result and provide for
     * cancellation of the underlying task
     * @since 1.6
     */
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Runnable task, T result) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task, result);
        execute(ftask);
        return ftask;
    }

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

    /**
     * the main mechanics of invokeAny.
     */
    private <T> T doInvokeAny(Collection<? extends Callable<T>> tasks,
                              boolean timed, long nanos)
        throws InterruptedException, ExecutionException, TimeoutException {
        if (tasks == null)
            throw new NullPointerException();
        int ntasks = tasks.size();
        if (ntasks == 0)
            throw new IllegalArgumentException();
        ArrayList<Future<T>> futures = new ArrayList<Future<T>>(ntasks);
        ExecutorCompletionService<T> ecs =
            new ExecutorCompletionService<T>(this);

        // For efficiency, especially in executors with limited
        // parallelism, check to see if previously submitted tasks are
        // done before submitting more of them. This interleaving
        // plus the exception mechanics account for messiness of main
        // loop.

        try {
            // Record exceptions so that if we fail to obtain any
            // result, we can throw the last exception we got.
            ExecutionException ee = null;
            final long deadline = timed ? System.nanoTime() + nanos : 0L;
            Iterator<? extends Callable<T>> it = tasks.iterator();

            // Start one task for sure; the rest incrementally
            futures.add(ecs.submit(it.next()));
            --ntasks;
            int active = 1;

            for (;;) {
                Future<T> f = ecs.poll();
                if (f == null) {
                    if (ntasks > 0) {
                        --ntasks;
                        futures.add(ecs.submit(it.next()));
                        ++active;
                    }
                    else if (active == 0)
                        break;
                    else if (timed) {
                        f = ecs.poll(nanos, TimeUnit.NANOSECONDS);
                        if (f == null)
                            throw new TimeoutException();
                        nanos = deadline - System.nanoTime();
                    }
                    else
                        f = ecs.take();
                }
                if (f != null) {
                    --active;
                    try {
                        return f.get();
                    } catch (ExecutionException eex) {
                        ee = eex;
                    } catch (RuntimeException rex) {
                        ee = new ExecutionException(rex);
                    }
                }
            }

            if (ee == null)
                ee = new ExecutionException();
            throw ee;

        } finally {
            for (int i = 0, size = futures.size(); i < size; i++)
                futures.get(i).cancel(true);
        }
    }

    public <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException {
        try {
            return doInvokeAny(tasks, false, 0);
        } catch (TimeoutException cannotHappen) {
            assert false;
            return null;
        }
    }

    public <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                           long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException {
        return doInvokeAny(tasks, true, unit.toNanos(timeout));
    }

    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException {
        if (tasks == null)
            throw new NullPointerException();
        ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
        boolean done = false;
        try {
            for (Callable<T> t : tasks) {
                RunnableFuture<T> f = newTaskFor(t);
                futures.add(f);
                execute(f);
            }
            for (int i = 0, size = futures.size(); i < size; i++) {
                Future<T> f = futures.get(i);
                if (!f.isDone()) {
                    try {
                        f.get();
                    } catch (CancellationException ignore) {
                    } catch (ExecutionException ignore) {
                    }
                }
            }
            done = true;
            return futures;
        } finally {
            if (!done)
                for (int i = 0, size = futures.size(); i < size; i++)
                    futures.get(i).cancel(true);
        }
    }

    public <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                         long timeout, TimeUnit unit)
        throws InterruptedException {
        if (tasks == null)
            throw new NullPointerException();
        long nanos = unit.toNanos(timeout);
        ArrayList<Future<T>> futures = new ArrayList<Future<T>>(tasks.size());
        boolean done = false;
        try {
            for (Callable<T> t : tasks)
                futures.add(newTaskFor(t));

            final long deadline = System.nanoTime() + nanos;
            final int size = futures.size();

            // Interleave time checks and calls to execute in case
            // executor doesn't have any/much parallelism.
            for (int i = 0; i < size; i++) {
                execute((Runnable)futures.get(i));
                nanos = deadline - System.nanoTime();
                if (nanos <= 0L)
                    return futures;
            }

            for (int i = 0; i < size; i++) {
                Future<T> f = futures.get(i);
                if (!f.isDone()) {
                    if (nanos <= 0L)
                        return futures;
                    try {
                        f.get(nanos, TimeUnit.NANOSECONDS);
                    } catch (CancellationException ignore) {
                    } catch (ExecutionException ignore) {
                    } catch (TimeoutException toe) {
                        return futures;
                    }
                    nanos = deadline - System.nanoTime();
                }
            }
            done = true;
            return futures;
        } finally {
            if (!done)
                for (int i = 0, size = futures.size(); i < size; i++)
                    futures.get(i).cancel(true);
        }
    }

}
複製程式碼

ThreadPoolExecutor 類

作為一個 ExecutorService,它使用可能的幾個池化執行緒之一執行每個提交的任務,通常使用 Executors 工廠方法配置。

執行緒池解決了兩個不同的問題:它們通常在執行大量非同步任務時將提升效能,這是由於減少了每個任務的呼叫開銷,並且它們提供了一種繫結和管理資源和執行緒的方法,減少了執行一系列任務時所消耗的效能。 每個 ThreadPoolExecutor 還維護一些基本統計資訊,例如已完成任務的數量。

為了在各種上下文中有用,該類提供了許多可調引數和可擴充套件性鉤子。 但是,程式設計師應該使用更方便的 Executors 工廠方法 Executor.newCachedThreadPool(無界執行緒池,自動執行緒回收), Executors.newFixedThreadPool(固定大小的執行緒池)和 Executors.newSingleThreadExecutor(單一後臺執行緒),為最常見的使用場景預配置設定。 否則,在手動配置和調整此類時,請使用以下指南:

核心和最大池大小

ThreadPoolExecutor 將根據corePoolSize(getCorePoolSize)和maximumPoolSize(getMaximumPoolSize)設定的邊界自動調整執行緒池大小(getPoolSize)。

當在方法 execute(Runnable) 中提交新任務並且執行的執行緒少於corePoolSize時,即使其他工作執行緒處於空閒狀態,也會建立一個新執行緒來處理該請求。 如果有多於corePoolSize但少於maximumPoolSize的執行執行緒,則只有在佇列已滿時才會建立新執行緒。 通過設定corePoolSize和maximumPoolSize相同,可以建立一個固定大小的執行緒池。 通過將maximumPoolSize設定為基本無限制的值(例如 Integer.MAX_VALUE),可以允許池容納任意數量的併發任務。 最典型的情況是,核心和最大池大小僅在構造時設定,但也可以使用 setCorePoolSize 和 #setMaximumPoolSize 動態更改。

按需構建

預設情況下,即使核心執行緒最初只在新任務到達時建立並啟動,但可以使用方法 prestartCoreThread 或 prestartAllCoreThreads 動態覆寫。 如果使用非空佇列構造池,則可能需要預啟動執行緒。

建立新執行緒

使用 ThreadFactory 建立新執行緒。 如果沒有另外指定,則使用 Executors.defaultThreadFactory,它建立的執行緒都在同一個 ThreadGroup 中,並且具有相同的 NORM_PRIORITY 優先順序和非守護程式狀態。 通過提供不同的ThreadFactory,可以更改執行緒的名稱,執行緒組,優先順序,守護程式狀態等。如果 ThreadFactory 在通過從 newThread 返回null而無法建立執行緒時,executor 將繼續,但可能無法執行任何任務。 執行緒應該擁有“modifyThread” RuntimePermission。 如果使用池的工作執行緒或其他執行緒不具有此許可權,則服務可能會降級:配置更改可能不會及時生效,並且關閉池可能保持可以終止但未完成的狀態。

保持存活的時間

如果池當前具有多個corePoolSize執行緒,則多餘的執行緒如果空閒時間超過keepAliveTime,則將終止(getKeepAliveTime(TimeUnit))。 這提供了一種在不主動使用池時減少資源消耗的方法。 如果池稍後變得更活躍,則將構造新執行緒。 也可以使用方法 setKeepAliveTime(long,TimeUnit) 動態更改此引數。 使用值 Long.MAX_VALUE TimeUnit#NANOSECONDS 可以有效地禁止空閒執行緒在關閉之前終止。 預設情況下,僅當存在多個corePoolSize執行緒時,保持活動策略才適用。 但是方法 allowCoreThreadTimeOut(boolean) 也可用於將此超時策略應用於核心執行緒,只要keepAliveTime值為非零。

佇列

任何BlockingQueue都可以用於傳輸和儲存所提交的任務。這個佇列的使用與池大小互動:

  1. 如果執行的執行緒少於corePoolSize的執行緒,Executor 總是新增新執行緒,而不是排隊。

  2. 如果corePoolSize或更多的執行緒正在執行,Executor 總是喜歡排隊請求,而不是新增一個新的執行緒。

  3. 如果請求不能排隊,那麼將建立一個新的執行緒,除非這個執行緒超過maximumPoolSize,在這種情況下,該任務將被拒絕。

排隊的一般策略有三種:

直接切換。工作佇列的一個很好的預設選擇是 SynchronousQueue,它將任務移交給執行緒而不另外儲存它們。 在這裡,如果沒有執行緒立即可用於執行它,則嘗試對任務進行入隊將失敗,因此將構造新執行緒。 此策略在處理可能具有內部依賴性的請求集時避免了鎖定。 直接切換通常需要無限制的maximumPoolSizes以避免拒絕新提交的任務。 這反過來承認,當命令繼續以比處理它們更快的速度到達時,無限制的執行緒增長的可能性。

無界佇列。使用無界佇列(例如沒有預定義容量的 LinkedBlockingQueue 將導致新任務在所有corePoolSize執行緒忙時在佇列中等待。 因此,只會建立corePoolSize執行緒。 (maximumPoolSize的值因此沒有任何影響。)這可能是適當的每個任務完全獨立於其他任務,因此任務不會影響彼此的執行; 例如,在網頁伺服器中。 雖然這種排隊方式可以有助於平滑瞬態突發請求,但需要承認的是當命令繼續以平均到達的速度超過可處理速度時,可能導致無限制的工作佇列增長。

有界佇列。有界佇列(例如, ArrayBlockingQueue)與有限maximumPoolSizes一起使用時有助於防止資源耗盡,但可能更難調整和控制。 佇列大小和最大池大小可以相互交換:使用大型佇列和小型池最小化CPU使用率,OS資源和上下文切換開銷,但可能導致人為的低吞吐量。 如果任務經常被阻塞(例如,如果它們是I / O繫結的),那麼系統可能會安排更多執行緒的時間,而不是允許的時間。 使用小佇列通常需要更大的池大小,這會使CPU更繁忙,但可能會遇到不可接受的排程開銷,這也會降低吞吐量。

被拒絕的任務

當 Executor 關閉時,以及當Executor對最大執行緒和工作佇列容量使用有限邊界時,方法 execute(Runnable) 中提交的新任務將被拒絕,因為已經飽和了。 在任何一種情況下, execute 方法都會呼叫其 RejectedExecutionHandler 的 RejectedExecutionHandler.rejectedExecution(Runnable,ThreadPoolExecutor) 方法。 提供了四種預定義的處理程式策:

在預設的 ThreadPoolExecutor.AbortPolicy 中,處理程式在拒絕時丟擲執行時 異常 RejectedExecutionException。

在 ThreadPoolExecutor.CallerRunsPolicy 中,呼叫 execute 本身的執行緒執行該任務。 這提供了一個簡單的反饋控制機制,可以減慢提交新任務的速度。

在 ThreadPoolExecutor.DiscardPolicy 中,簡單地刪除了無法執行的任務。

在 ThreadPoolExecutor.DiscardOldestPolicy 中,如果 executor 未關閉,則會刪除工作佇列頭部的任務,然後重試執行(可能會再次失敗,導致重複執行)。

可以定義和使用其他種類的 RejectedExecutionHandler 擴充套件類。 這樣做需要特別小心,特別是當策略設計為僅在特定容量或排隊策略下工作時。

鉤子方法

此類提供在執行每個任務之前和之後呼叫的 protected 可覆蓋 beforeExecute(Thread,Runnable) 和 afterExecute(Runnable,Throwable) 方法。 這些可以用來操縱執行環境; 例如,重新初始化 ThreadLocals ,收集統計資訊或新增日誌條目。 此外,可以重寫方法 terminated 以執行 Executor 完全終止後需要執行的任何特殊處理。

如果鉤子或回撥方法丟擲異常,則內部工作者執行緒可能會失敗並突然終止。

佇列維護

方法 getQueue() 允許訪問工作佇列以進行監視和除錯。 強烈建議不要將此方法用於任何其他目的。 當大量排隊的任務被取消時,兩種提供的方法 remove(Runnable) 和 purge 可用於協助儲存回收。

終結

程式中不再引用的池和沒有剩餘的執行緒將自動 shutdown。 如果希望確保即使使用者忘記呼叫 shutdown 也會回收未引用的池,那麼必須通過設定適當的保持活動時間,使用零核心執行緒的下限來安排未使用的執行緒最終死亡 和/或設定 allowCoreThreadTimeOut(boolean)。

擴充套件示例。 此類的大多數擴充套件都會覆蓋一個或多個受保護的鉤子方法。 例如,這是一個新增簡單暫停/恢復功能的子類:

    class PausableThreadPoolExecutor extends ThreadPoolExecutor {
        private boolean isPaused;
        private ReentrantLock pauseLock = new ReentrantLock();
        private Condition unpaused = pauseLock.newCondition();

        public PausableThreadPoolExecutor(...) {
            super(...);
        }

        protected void beforeExecute(Thread t, Runnable r) {
            super.beforeExecute(t, r);
            pauseLock.lock();
            try {
                while (isPaused) unpaused.await();
            } catch (InterruptedException ie) {
                t.interrupt();
            } finally {
                pauseLock.unlock();
            }
        }

        public void pause() {
            pauseLock.lock();
            try {
                isPaused = true;
            } finally {
                pauseLock.unlock();
            }
        }

        public void resume() {
            pauseLock.lock();
            try {
                isPaused = false;
                unpaused.signalAll();
            } finally {
                pauseLock.unlock();
            }
        }
    }
複製程式碼
// 用給定的初始引數和預設的執行緒工廠及被拒絕的執行處理程式建立新的 ThreadPoolExecutor。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)
// 用給定的初始引數和預設的執行緒工廠建立新的 ThreadPoolExecutor。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)
// 用給定的初始引數和預設被拒絕的執行處理程式建立新的 ThreadPoolExecutor。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)
// 用給定的初始引數建立新的 ThreadPoolExecutor。
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)

// 基於完成執行給定 Runnable 所呼叫的方法。
protected void afterExecute(Runnable r, Throwable t)
// 如果在保持活動時間內沒有任務到達,新任務到達時正在替換(如果需要),則設定控制核心執行緒是超時還是終止的策略。
void allowCoreThreadTimeOut(boolean value)
// 如果此池允許核心執行緒超時和終止,如果在 keepAlive 時間內沒有任務到達,新任務到達時正在替換(如果需要),則返回 true。
boolean allowsCoreThreadTimeOut()
// 請求關閉、發生超時或者當前執行緒中斷,無論哪一個首先發生之後,都將導致阻塞,直到所有任務完成執行。
boolean awaitTermination(long timeout, TimeUnit unit)
// 在執行給定執行緒中的給定 Runnable 之前呼叫的方法。
protected void beforeExecute(Thread t, Runnable r)
// 在將來某個時間執行給定任務。
void execute(Runnable command)
// 當不再引用此執行程式時,呼叫 shutdown。
protected void finalize()
// 返回主動執行任務的近似執行緒數。
int getActiveCount()
// 返回已完成執行的近似任務總數。
long getCompletedTaskCount()
// 返回核心執行緒數。
int getCorePoolSize()
// 返回執行緒保持活動的時間,該時間就是超過核心池大小的執行緒可以在終止前保持空閒的時間值。
long getKeepAliveTime(TimeUnit unit)
// 返回曾經同時位於池中的最大執行緒數。
int getLargestPoolSize()
// 返回允許的最大執行緒數。
int getMaximumPoolSize()
// 返回池中的當前執行緒數。
int getPoolSize()
// 返回此執行程式使用的任務佇列。
BlockingQueue<Runnable> getQueue()
// 返回用於未執行任務的當前處理程式。
RejectedExecutionHandler getRejectedExecutionHandler()
// 返回曾計劃執行的近似任務總數。
long getTaskCount()
// 返回用於建立新執行緒的執行緒工廠。
ThreadFactory getThreadFactory()
// 如果此執行程式已關閉,則返回 true。
boolean isShutdown()
// 如果關閉後所有任務都已完成,則返回 true。
boolean isTerminated()
// 如果此執行程式處於在 shutdown 或 shutdownNow 之後正在終止但尚未完全終止的過程中,則返回 true。
boolean isTerminating()
// 啟動所有核心執行緒,使其處於等待工作的空閒狀態。
int prestartAllCoreThreads()
// 啟動核心執行緒,使其處於等待工作的空閒狀態。
boolean prestartCoreThread()
// 嘗試從工作佇列移除所有已取消的 Future 任務。
void purge()
// 從執行程式的內部佇列中移除此任務(如果存在),從而如果尚未開始,則其不再執行。
boolean remove(Runnable task)
// 設定核心執行緒數。
void setCorePoolSize(int corePoolSize)
// 設定執行緒在終止前可以保持空閒的時間限制。
void setKeepAliveTime(long time, TimeUnit unit)
// 設定允許的最大執行緒數。
void setMaximumPoolSize(int maximumPoolSize)
// 設定用於未執行任務的新處理程式。
void setRejectedExecutionHandler(RejectedExecutionHandler handler)
// 設定用於建立新執行緒的執行緒工廠。
void setThreadFactory(ThreadFactory threadFactory)
// 按過去執行已提交任務的順序發起一個有序的關閉,但是不接受新任務。
void shutdown()
// 嘗試停止所有的活動執行任務、暫停等待任務的處理,並返回等待執行的任務列表。
List<Runnable> shutdownNow()
// 當 Executor 已經終止時呼叫的方法。
protected void terminated()
複製程式碼

ScheduledExecutorService 介面

一個 ExecutorService,它可以排程命令在給定的延遲之後執行,或者定期執行。

schedule 方法建立具有各種延遲的任務,並返回可用於取消或檢查執行的任務物件。scheduleAtFixedRate 和 scheduleWithFixedDelay 方法建立並執行定期執行的任務,直到被取消。

使用 Executor.execute(Runnable) 和 ExecutorService.submit 方法提交的命令的排程請求延遲為零。 schedule 方法中也允許零延遲和負延遲(但不包括時間段),並將其視為立即執行的請求。

所有 schedule 方法都接受相對延遲和週期作為引數,而不是絕對時間或日期。 將表示為 java.util.Date 的絕對時間轉換為所需的形式是一件簡單的事情。 例如,要在某個將來安排 date,可以使用: schedule(task,date.getTime() - System.currentTimeMillis(),TimeUnit.MILLISECONDS) 。 但請注意,相對延遲的到期不一定與由於網路時間同步協議,時鐘漂移或其他因素而啟用任務的當前 Date 一致。

Executors 類為此程式包中提供的 ScheduledExecutorService 實現提供了方便的工廠方法。

這是一個帶有方法的類,該方法將 ScheduledExecutorService 設定為每隔一小時發出十秒鐘的嗶聲:

import static java.util.concurrent.TimeUnit.*;
class BeeperControl {
    private final ScheduledExecutorService scheduler =
            Executors.newScheduledThreadPool(1);
     

    public void beepForAnHour() {
        final Runnable beeper = new Runnable() {
            public void run() {
                System.out.println("beep");
            }
        };
        final ScheduledFuture<?> beeperHandle =
                scheduler.scheduleAtFixedRate(beeper, 10, 10, SECONDS);
        scheduler.schedule(new Runnable() {
            public void run() {
                beeperHandle.cancel(true);
            }
        }, 60 * 60, SECONDS);
    }
}
複製程式碼
// 建立並執行在給定延遲後啟用的 ScheduledFuture。 
ScheduledFuture schedule(Callable callable, long delay, TimeUnit unit) 

// 建立並執行在給定延遲後啟用的一次性操作。 
ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) 

// 建立並執行一個在給定初始延遲後首次啟用的定期操作,後續操作具有給定的週期;也就是將在 initialDelay 後開始執行,然後在 initialDelay+period 後執行,接著在 initialDelay + 2 * period 後執行,依此類推。 
ScheduledFuture<?> scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) 

// 建立並執行一個在給定初始延遲後首次啟用的定期操作,隨後,在每一次執行終止和下一次執行開始之間都存在給定的延遲。 
ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit)
複製程式碼

ScheduledThreadPoolExecutor

一個 ThreadPoolExecutor ,可以額外安排命令在給定的延遲後執行,或定期執行。 當需要多個工作執行緒時,或者當需要 ThreadPoolExecutor(此類擴充套件)的額外靈活性或功能時,此類優於 java.util.Timer。

延遲任務在啟用後立即執行,但沒有任何實時保證啟用它們後何時啟動它們。 按照先進先出(FIFO)提交順序啟用計劃完全相同執行時間的任務。

在提交的任務在執行之前取消時,將禁止執行。 預設情況下,此類已取消的任務不會自動從工作佇列中刪除,直到其延遲過去。 雖然這可以進一步檢查和監控,但也可能導致取消任務的無限制保留。 為避免這種情況,請將 setRemoveOnCancelPolicy 設定為 true ,這會導致在取消時立即從工作佇列中刪除任務。

通過 scheduleAtFixedRate 或 scheduleWithFixedDelay 安排的任務的連續執行不重疊。 雖然不同的執行可以由不同的執行緒執行,但先前執行的效果 happen-before 之後的效果。

雖然這個類繼承自 ThreadPoolExecutor,但是一些繼承的調優方法對它沒用。 特別是,因為它使用 corePoolSize 執行緒和無界佇列充當固定大小的池,所以對 maximumPoolSize 的調整沒有任何有用的效果。 此外,將 corePoolSize 設定為零或使用 allowCoreThreadTimeOut 幾乎絕不是一個好主意,因為一旦它們有資格執行,這可能會使池沒有執行緒來處理任務。

擴充套件註釋:此類覆蓋 ThreadPoolExecutor.execute(Runnable) execute 和 AbstractExecutorService.submit(Runnable) submit 方法,以生成內部 ScheduledFuture 物件以控制每個任務的延遲和排程。 為了保留功能,子類中這些方法的任何進一步覆蓋必須呼叫超類版本,這有效地禁用了其他任務自定義。 但是,此類提供了替代的受保護擴充套件方法 decorateTask 支援 Runnable 和 Callable 各一個版本),可用於自定義用於執行通過 submit, schedule, scheduleAtFixedRate 和 scheduleWithFixedDelay。 預設情況下, ScheduledThreadPoolExecutor 使用擴充套件 FutureTask 的任務型別。 但是,可以使用以下形式的子類來修改或替換它:

public class CustomScheduledExecutor extends ScheduledThreadPoolExecutor {

    static class CustomTask<V> implements RunnableScheduledFuture<V> {
        ...
    }

    protected <V> RunnableScheduledFuture<V> decorateTask(
            Runnable r, RunnableScheduledFuture<V> task) {
        return new CustomTask<V>(r, task);
    }

    protected <V> RunnableScheduledFuture<V> decorateTask(
            Callable<V> c, RunnableScheduledFuture<V> task) {
        return new CustomTask<V>(c, task);
    }
    // ... add constructors, etc.
}
複製程式碼

Executors 類

此程式包中定義的 Executor, ExecutorService,ScheduledExecutorService, ThreadFactory 和 Callable 類的工廠和實用程式方法。 該類支援以下幾種方法:

 *   <li> Methods that create and return an {@link ExecutorService}
 *        set up with commonly useful configuration settings.
 *   <li> Methods that create and return a {@link ScheduledExecutorService}
 *        set up with commonly useful configuration settings.
 *   <li> Methods that create and return a "wrapped" ExecutorService, that
 *        disables reconfiguration by making implementation-specific methods
 *        inaccessible.
 *   <li> Methods that create and return a {@link ThreadFactory}
 *        that sets newly created threads to a known state.
 *   <li> Methods that create and return a {@link Callable}
 *        out of other closure-like forms, so they can be used
 *        in execution methods requiring {@code Callable}.
複製程式碼
// 返回 Callable 物件,呼叫它時可執行給定特權的操作並返回其結果。
static Callable<Object> callable(PrivilegedAction<?> action)
// 返回 Callable 物件,呼叫它時可執行給定特權的異常操作並返回其結果。
static Callable<Object> callable(PrivilegedExceptionAction<?> action)
// 返回 Callable 物件,呼叫它時可執行給定的任務並返回 null。
static Callable<Object> callable(Runnable task)
// 返回 Callable 物件,呼叫它時可執行給定的任務並返回給定的結果。
static <T> Callable<T> callable(Runnable task, T result)
// 返回用於建立新執行緒的預設執行緒工廠。
static ThreadFactory defaultThreadFactory()
// 建立一個可根據需要建立新執行緒的執行緒池,但是在以前構造的執行緒可用時將重用它們。
static ExecutorService newCachedThreadPool()
// 建立一個可根據需要建立新執行緒的執行緒池,但是在以前構造的執行緒可用時將重用它們,並在需要時使用提供的 ThreadFactory 建立新執行緒。
static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
// 建立一個可重用固定執行緒數的執行緒池,以共享的無界佇列方式來執行這些執行緒。
static ExecutorService newFixedThreadPool(int nThreads)
// 建立一個可重用固定執行緒數的執行緒池,以共享的無界佇列方式來執行這些執行緒,在需要時使用提供的 ThreadFactory 建立新執行緒。
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
// 建立一個執行緒池,它可安排在給定延遲後執行命令或者定期地執行。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
// 建立一個執行緒池,它可安排在給定延遲後執行命令或者定期地執行。
static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)
// 建立一個使用單個 worker 執行緒的 Executor,以無界佇列方式來執行該執行緒。
static ExecutorService newSingleThreadExecutor()
// 建立一個使用單個 worker 執行緒的 Executor,以無界佇列方式來執行該執行緒,並在需要時使用提供的 ThreadFactory 建立新執行緒。
static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
// 建立一個單執行緒執行程式,它可安排在給定延遲後執行命令或者定期地執行。
static ScheduledExecutorService newSingleThreadScheduledExecutor()
// 建立一個單執行緒執行程式,它可安排在給定延遲後執行命令或者定期地執行。
static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory)
// 返回 Callable 物件,呼叫它時可在當前的訪問控制上下文中執行給定的 callable 物件。
static <T> Callable<T> privilegedCallable(Callable<T> callable)
// 返回 Callable 物件,呼叫它時可在當前的訪問控制上下文中,使用當前上下文類載入器作為上下文類載入器來執行給定的 callable 物件。
static <T> Callable<T> privilegedCallableUsingCurrentClassLoader(Callable<T> callable)
// 返回用於建立新執行緒的執行緒工廠,這些新執行緒與當前執行緒具有相同的許可權。
static ThreadFactory privilegedThreadFactory()
// 返回一個將所有已定義的 ExecutorService 方法委託給指定執行程式的物件,但是使用強制轉換可能無法訪問其他方法。
static ExecutorService unconfigurableExecutorService(ExecutorService executor)
// 返回一個將所有已定義的 ExecutorService 方法委託給指定執行程式的物件,但是使用強制轉換可能無法訪問其他方法。
static ScheduledExecutorService unconfigurableScheduledExecutorService(ScheduledExecutorService executor)
複製程式碼

Android 執行緒池執行器

它是一個功能強大的任務執行框架,因為它支援佇列中的任務新增,任務取消和任務優先順序。

它減少了與執行緒建立相關的開銷,因為它在其執行緒池中管理所需數量的執行緒。

執行緒池執行器有自己的建立執行緒的執行緒工廠。

public class PriorityThreadFactory implements ThreadFactory {

    private final int mThreadPriority;

    public PriorityThreadFactory(int threadPriority) {
        mThreadPriority = threadPriority;
    }

    @Override
    public Thread newThread(final Runnable runnable) {
        Runnable wrapperRunnable = new Runnable() {
            @Override
            public void run() {
                try {
                    Process.setThreadPriority(mThreadPriority);//設定優先順序
                } catch (Throwable t) {

                }
                runnable.run();
            }
        };
        return new Thread(wrapperRunnable);
    }
}
複製程式碼

主執行緒執行器

public class MainThreadExecutor implements Executor {

    private final Handler handler = new Handler(Looper.getMainLooper());

    @Override
    public void execute(Runnable runnable) {
        handler.post(runnable);
    }
}
複製程式碼

將執行緒池執行器封裝在 DefaultExecutorSupplier 中,可以包含多個執行緒池執行器。

/*
* Singleton class for default executor supplier
*/
public class DefaultExecutorSupplier{
    /*
    * Number of cores to decide the number of threads
    */
    public static final int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
    
    /*
    * thread pool executor for background tasks
    */
    private final ThreadPoolExecutor mForBackgroundTasks;
    /*
    * thread pool executor for light weight background tasks
    */
    private final ThreadPoolExecutor mForLightWeightBackgroundTasks;
    /*
    * thread pool executor for main thread tasks
    */
    private final Executor mMainThreadExecutor;
    /*
    * an instance of DefaultExecutorSupplier
    */
    private static DefaultExecutorSupplier sInstance;

    /*
    * returns the instance of DefaultExecutorSupplier
    */
    public static DefaultExecutorSupplier getInstance() {
       if (sInstance == null) {
         synchronized(DefaultExecutorSupplier.class){                                                                  
             sInstance = new DefaultExecutorSupplier();      
        }
        return sInstance;
    }

    /*
    * constructor for  DefaultExecutorSupplier
    */ 
    private DefaultExecutorSupplier() {
        
        // setting the thread factory
        ThreadFactory backgroundPriorityThreadFactory = new 
                PriorityThreadFactory(Process.THREAD_PRIORITY_BACKGROUND);
        
        // setting the thread pool executor for mForBackgroundTasks;
        mForBackgroundTasks = new ThreadPoolExecutor(
                NUMBER_OF_CORES * 2,
                NUMBER_OF_CORES * 2,
                60L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(),
                backgroundPriorityThreadFactory
        );
        
        // setting the thread pool executor for mForLightWeightBackgroundTasks;
        mForLightWeightBackgroundTasks = new ThreadPoolExecutor(
                NUMBER_OF_CORES * 2,
                NUMBER_OF_CORES * 2,
                60L,
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>(),
                backgroundPriorityThreadFactory
        );
        
        // setting the thread pool executor for mMainThreadExecutor;
        mMainThreadExecutor = new MainThreadExecutor();
    }

    /*
    * returns the thread pool executor for background task
    */
    public ThreadPoolExecutor forBackgroundTasks() {
        return mForBackgroundTasks;
    }

    /*
    * returns the thread pool executor for light weight background task
    */
    public ThreadPoolExecutor forLightWeightBackgroundTasks() {
        return mForLightWeightBackgroundTasks;
    }

    /*
    * returns the thread pool executor for main thread task
    */
    public Executor forMainThreadTasks() {
        return mMainThreadExecutor;
    }
}
複製程式碼

這樣子就可以使用了

/*
* Using it for Background Tasks
*/
public void doSomeBackgroundWork(){
  DefaultExecutorSupplier.getInstance().forBackgroundTasks()
    .execute(new Runnable() {
    @Override
    public void run() {
       // do some background work here.
    }
  });
}

/*
* Using it for Light-Weight Background Tasks
*/
public void doSomeLightWeightBackgroundWork(){
  DefaultExecutorSupplier.getInstance().forLightWeightBackgroundTasks()
    .execute(new Runnable() {
    @Override
    public void run() {
       // do some light-weight background work here.
    }
  });
}

/*
* Using it for MainThread Tasks
*/
public void doSomeMainThreadWork(){
  DefaultExecutorSupplier.getInstance().forMainThreadTasks()
    .execute(new Runnable() {
    @Override
    public void run() {
       // do some Main Thread work here.
    }
  });
}
複製程式碼

當前傳遞的任務都是 Runnable 型別,無法獲得返回值或者取消任務。可以使用下列方式操作任務:

/*
* Get the future of the task by submitting it to the pool
*/
Future future = DefaultExecutorSupplier.getInstance().forBackgroundTasks().submit(new Runnable() {
    @Override
    public void run() {
      // do some background work here.
    }
});

/*
* cancelling the task
*/
future.cancel(true); 
複製程式碼

新增可以設定執行緒優先順序的執行緒池處理器,首先設定優先順序配置:

/**
 * Priority levels
 */
public enum Priority {
    /**
     * NOTE: DO NOT CHANGE ORDERING OF THOSE CONSTANTS UNDER ANY CIRCUMSTANCES.
     * Doing so will make ordering incorrect.
     */

    /**
     * Lowest priority level. Used for prefetches of data.
     */
    LOW,

    /**
     * Medium priority level. Used for warming of data that might soon get visible.
     */
    MEDIUM,

    /**
     * Highest priority level. Used for data that are currently visible on screen.
     */
    HIGH,

    /**
     * Highest priority level. Used for data that are required instantly(mainly for emergency).
     */
    IMMEDIATE;
}
複製程式碼

建立具有優先順序的任務實現

public class PriorityRunnable implements Runnable {

    private final Priority priority;

    public PriorityRunnable(Priority priority) {
        this.priority = priority;
    }

    @Override
    public void run() {
      // nothing to do here.
    }

    public Priority getPriority() {
        return priority;
    }
}
複製程式碼

接著建立具有優先順序的執行緒池處理器

public class PriorityThreadPoolExecutor extends ThreadPoolExecutor {

   public PriorityThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
         TimeUnit unit, ThreadFactory threadFactory) {
        //在呼叫父類方法時使用具有優先順序的佇列
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit,new PriorityBlockingQueue<Runnable>(), threadFactory);
    }

    @Override
    public Future<?> submit(Runnable task) {
        //將普通的Runnable封裝成具有優先順序的Runnable
        PriorityFutureTask futureTask = new PriorityFutureTask((PriorityRunnable) task);
        //進行入隊操作
        execute(futureTask);
        return futureTask;
    }

    private static final class PriorityFutureTask extends FutureTask<PriorityRunnable>
            implements Comparable<PriorityFutureTask> {
        private final PriorityRunnable priorityRunnable;

        public PriorityFutureTask(PriorityRunnable priorityRunnable) {
            super(priorityRunnable, null);
            this.priorityRunnable = priorityRunnable;
        }
        
        /*
         * compareTo() method is defined in interface java.lang.Comparable and it is used
         * to implement natural sorting on java classes. natural sorting means the the sort 
         * order which naturally applies on object e.g. lexical order for String, numeric 
         * order for Integer or Sorting employee by there ID etc. most of the java core 
         * classes including String and Integer implements CompareTo() method and provide
         * natural sorting.
         */
        @Override
        public int compareTo(PriorityFutureTask other) {
            Priority p1 = priorityRunnable.getPriority();
            Priority p2 = other.priorityRunnable.getPriority();
            return p2.ordinal() - p1.ordinal();
        }
    }
}
複製程式碼

在 DefaultExecutorSupplier 中暴露出優先順序執行緒池執行器

public class DefaultExecutorSupplier{

private final PriorityThreadPoolExecutor mForBackgroundTasks;

private DefaultExecutorSupplier() {
  
        mForBackgroundTasks = new PriorityThreadPoolExecutor(
                NUMBER_OF_CORES * 2,
                NUMBER_OF_CORES * 2,
                60L,
                TimeUnit.SECONDS,
                backgroundPriorityThreadFactory
        );

    }
}
複製程式碼

使用方式為

/*
* do some task at high priority
*/
public void doSomeTaskAtHighPriority(){
  DefaultExecutorSupplier.getInstance().forBackgroundTasks()
    .submit(new PriorityRunnable(Priority.HIGH) {
    @Override
    public void run() {
      // do some background work here at high priority.
    }
});
}
複製程式碼

Android中的執行緒的優先順序

在Android中有兩種執行緒的優先順序,一種為Android API版本,另一種是 Java 原生版本。

Android API

THREAD_PRIORITY_DEFAULT,預設的執行緒優先順序,值為0。
THREAD_PRIORITY_LOWEST,最低的執行緒級別,值為19。
THREAD_PRIORITY_BACKGROUND 後臺執行緒建議設定這個優先順序,值為10。
THREAD_PRIORITY_FOREGROUND 使用者正在互動的UI執行緒,程式碼中無法設定該優先順序,系統會按照情況調整到該優先順序,值為-2。
THREAD_PRIORITY_DISPLAY 也是與UI互動相關的優先順序界別,但是要比THREAD_PRIORITY_FOREGROUND優先,程式碼中無法設定,由系統按照情況調整,值為-4。
THREAD_PRIORITY_URGENT_DISPLAY 顯示執行緒的最高階別,用來處理繪製畫面和檢索輸入事件,程式碼中無法設定成該優先順序。值為-8。
THREAD_PRIORITY_AUDIO 聲音執行緒的標準級別,程式碼中無法設定為該優先順序,值為 -16。
THREAD_PRIORITY_URGENT_AUDIO 聲音執行緒的最高階別,優先程度較THREAD_PRIORITY_AUDIO要高。程式碼中無法設定為該優先順序。值為-19。
THREAD_PRIORITY_MORE_FAVORABLE 相對THREAD_PRIORITY_DEFAULT稍微優先,值為-1。
THREAD_PRIORITY_LESS_FAVORABLE 相對THREAD_PRIORITY_DEFAULT稍微落後一些,值為1。
複製程式碼

使用方式

new Thread () {
    @Override
    public void run() {
      super.run();
        android.os.Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    }
}.start();
複製程式碼

Java API 的設定效果不如 Android API。

多執行緒程式設計中的三個核心概念

原子性

這一點,跟資料庫事務的原子性概念差不多,即一個操作(有可能包含有多個子操作)要麼全部執行(生效),要麼全部都不執行(都不生效)。

關於原子性,一個非常經典的例子就是銀行轉賬問題:比如A和B同時向C轉賬10萬元。如果轉賬操作不具有原子性,A在向C轉賬時,讀取了C的餘額為20萬,然後加上轉賬的10萬,計算出此時應該有30萬,但還未來及將30萬寫回C的賬戶,此時B的轉賬請求過來了,B發現C的餘額為20萬,然後將其加10萬並寫回。然後A的轉賬操作繼續——將30萬寫回C的餘額。這種情況下C的最終餘額為30萬,而非預期的40萬。

可見性

可見性是指,當多個執行緒併發訪問共享變數時,一個執行緒對共享變數的修改,其它執行緒能夠立即看到。可見性問題是好多人忽略或者理解錯誤的一點。

CPU從主記憶體中讀資料的效率相對來說不高,現在主流的計算機中,都有幾級快取。每個執行緒讀取共享變數時,都會將該變數載入進其對應CPU的快取記憶體裡,修改該變數後,CPU會立即更新該快取,但並不一定會立即將其寫回主記憶體(實際上寫回主記憶體的時間不可預期)。此時其它執行緒(尤其是不在同一個CPU上執行的執行緒)訪問該變數時,從主記憶體中讀到的就是舊的資料,而非第一個執行緒更新後的資料。

這一點是作業系統或者說是硬體層面的機制,所以很多應用開發人員經常會忽略。

順序性

順序性指的是,程式執行的順序按照程式碼的先後順序執行。

以下面這段程式碼為例

boolean started = false; // 語句1
long counter = 0L; // 語句2
counter = 1; // 語句3
started = true; // 語句4
複製程式碼

從程式碼順序上看,上面四條語句應該依次執行,但實際上JVM真正在執行這段程式碼時,並不保證它們一定完全按照此順序執行。

處理器為了提高程式整體的執行效率,可能會對程式碼進行優化,其中的一項優化方式就是調整程式碼順序,按照更高效的順序執行程式碼。但它會保證程式最終的執行結果和程式碼順序執行時的結果一致。

Java如何保證原子性

鎖和同步

常用的保證Java操作原子性的工具是鎖和同步方法(或者同步程式碼塊)。使用鎖,可以保證同一時間只有一個執行緒能拿到鎖,也就保證了同一時間只有一個執行緒能執行申請鎖和釋放鎖之間的程式碼。

public void testLock () {
  lock.lock();
  try{
    int j = i;
    i = j + 1;
  } finally {
    lock.unlock();
  }
}
複製程式碼

與鎖類似的是同步方法或者同步程式碼塊。使用非靜態同步方法時,鎖住的是當前例項;使用靜態同步方法時,鎖住的是該類的Class物件;使用靜態程式碼塊時,鎖住的是synchronized關鍵字後面括號內的物件。下面是同步程式碼塊示例

public void testLock () {
  synchronized (anyObject){
    int j = i;
    i = j + 1;
  }
}
複製程式碼

無論使用鎖還是synchronized,本質都是一樣,通過鎖來實現資源的排它性,從而實際目的碼段同一時間只會被一個執行緒執行,進而保證了目的碼段的原子性。這是一種以犧牲效能為代價的方法。

CAS(compare and swap)

基礎型別變數自增(i++)是一種常被新手誤以為是原子操作而實際不是的操作。Java中提供了對應的原子操作類來實現該操作,並保證原子性,其本質是利用了CPU級別的CAS指令。由於是CPU級別的指令,其開銷比需要作業系統參與的鎖的開銷小。AtomicInteger使用方法如下。

AtomicInteger atomicInteger = new AtomicInteger();
for(int b = 0; b < numThreads; b++) {
  new Thread(() -> {
    for(int a = 0; a < iteration; a++) {
      atomicInteger.incrementAndGet();
    }
  }).start();
}
複製程式碼

Java如何保證可見性

Java提供了volatile關鍵字來保證可見性。當使用volatile修飾某個變數時,它會保證對該變數的修改會立即被更新到記憶體中,並且將其它快取中對該變數的快取設定成無效,因此其它執行緒需要讀取該值時必須從主記憶體中讀取,從而得到最新的值。

Java如何保證順序性

編譯器和處理器對指令進行重新排序時,會保證重新排序後的執行結果和程式碼順序執行的結果一致,所以重新排序過程並不會影響單執行緒程式的執行,卻可能影響多執行緒程式併發執行的正確性。

Java中可通過volatile在一定程式上保證順序性,另外還可以通過synchronized和鎖來保證順序性。

synchronized和鎖保證順序性的原理和保證原子性一樣,都是通過保證同一時間只會有一個執行緒執行目的碼段來實現的。

除了從應用層面保證目的碼段執行的順序性外,JVM還通過被稱為happens-before原則隱式地保證順序性。兩個操作的執行順序只要可以通過happens-before推匯出來,則JVM會保證其順序性,反之JVM對其順序性不作任何保證,可對其進行任意必要的重新排序以獲取高效率。

happens-before原則(先行發生原則)

  • 傳遞規則:如果操作1在操作2前面,而操作2在操作3前面,則操作1肯定會在操作3前發生。該規則說明了happens-before原則具有傳遞性
  • 鎖定規則:一個unlock操作肯定會在後面對同一個鎖的lock操作前發生。這個很好理解,鎖只有被釋放了才會被再次獲取
  • volatile變數規則:對一個被volatile修飾的寫操作先發生於後面對該變數的讀操作
  • 程式次序規則:一個執行緒內,按照程式碼順序執行
  • 執行緒啟動規則:Thread物件的start()方法先發生於此執行緒的其它動作
  • 執行緒終結原則:執行緒的終止檢測後發生於執行緒中其它的所有操作
  • 執行緒中斷規則: 對執行緒interrupt()方法的呼叫先發生於對該中斷異常的獲取
  • 物件終結規則:一個物件構造先於它的finalize發生

volatile適用場景

volatile適用於不需要保證原子性,但卻需要保證可見性的場景。一種典型的使用場景是用它修飾用於停止執行緒的狀態標記。如下所示

boolean isRunning = false;
public void start () {
  new Thread( () -> {
    while(isRunning) {
      someOperation();
    }
  }).start();
}
public void stop () {
  isRunning = false;
}
複製程式碼

在這種實現方式下,即使其它執行緒通過呼叫stop()方法將isRunning設定為false,迴圈也不一定會立即結束。可以通過volatile關鍵字,保證while迴圈及時得到isRunning最新的狀態從而及時停止迴圈,結束執行緒。

執行緒安全Q&A

問:平時專案中使用鎖和synchronized比較多,而很少使用volatile,難道就沒有保證可見性? 答:鎖和synchronized即可以保證原子性,也可以保證可見性。都是通過保證同一時間只有一個執行緒執行目的碼段來實現的。

問:鎖和synchronized為何能保證可見性? 答:根據JDK 7的Java doc中對concurrent包的說明,一個執行緒的寫結果保證對另外執行緒的讀操作可見,只要該寫操作可以由happen-before原則推斷出在讀操作之前發生。

問:既然鎖和synchronized即可保證原子性也可保證可見性,為何還需要volatile? 答:synchronized和鎖需要通過作業系統來仲裁誰獲得鎖,開銷比較高,而volatile開銷小很多。因此在只需要保證可見性的條件下,使用volatile的效能要比使用鎖和synchronized高得多。

問:既然鎖和synchronized可以保證原子性,為什麼還需要AtomicInteger這種的類來保證原子操作? 答:鎖和synchronized需要通過作業系統來仲裁誰獲得鎖,開銷比較高,而AtomicInteger是通過CPU級的CAS操作來保證原子性,開銷比較小。所以使用AtomicInteger的目的還是為了提高效能。

問:還有沒有別的辦法保證執行緒安全 答:有。儘可能避免引起非執行緒安全的條件——共享變數。如果能從設計上避免共享變數的使用,即可避免非執行緒安全的發生,也就無須通過鎖或者synchronized以及volatile解決原子性、可見性和順序性的問題。

問:synchronized即可修飾非靜態方式,也可修飾靜態方法,還可修飾程式碼塊,有何區別 答:synchronized修飾非靜態同步方法時,鎖住的是當前例項;synchronized修飾靜態同步方法時,鎖住的是該類的Class物件;synchronized修飾靜態程式碼塊時,鎖住的是synchronized關鍵字後面括號內的物件。

sleep和wait到底什麼區別

其實這個問題應該這麼問——sleep和wait有什麼相同點。因為這兩個方法除了都能讓當前執行緒暫停執行完,幾乎沒有其它相同點。

wait方法是Object類的方法,這意味著所有的Java類都可以呼叫該方法。sleep方法是Thread類的靜態方法。

wait是在當前執行緒持有wait物件鎖的情況下,暫時放棄鎖,並讓出CPU資源,並積極等待其它執行緒呼叫同一物件的notify或者notifyAll方法。注意,即使只有一個執行緒在等待,並且有其它執行緒呼叫了notify或者notifyAll方法,等待的執行緒只是被啟用,但是它必須得再次獲得鎖才能繼續往下執行。換言之,即使notify被呼叫,但只要鎖沒有被釋放,原等待執行緒因為未獲得鎖仍然無法繼續執行。

public class Wait {
  public static void main(String[] args) {
    Thread thread1 = new Thread(() -> {
      synchronized (Wait.class) {
        try {
          System.out.println(new Date() + " Thread1 is running");
          Wait.class.wait();
          System.out.println(new Date() + " Thread1 ended");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
    });
    thread1.start();
    
    Thread thread2 = new Thread(() -> {
      synchronized (Wait.class) {
        try {
          System.out.println(new Date() + " Thread2 is running");
          Wait.class.notify();
          // Don't use sleep method to avoid confusing
          for(long i = 0; i < 200000; i++) {
            for(long j = 0; j < 100000; j++) {}
          }
          System.out.println(new Date() + " Thread2 release lock");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
      
      for(long i = 0; i < 200000; i++) {
        for(long j = 0; j < 100000; j++) {}
      }
      System.out.println(new Date() + " Thread2 ended");
    });
    
    // Don't use sleep method to avoid confusing
    for(long i = 0; i < 200000; i++) {
      for(long j = 0; j < 100000; j++) {}
    }
    thread2.start();
  }
}
複製程式碼

執行結果如下

Tue Jun 14 22:51:11 CST 2016 Thread1 is running
Tue Jun 14 22:51:23 CST 2016 Thread2 is running
Tue Jun 14 22:51:36 CST 2016 Thread2 release lock
Tue Jun 14 22:51:36 CST 2016 Thread1 ended
Tue Jun 14 22:51:49 CST 2016 Thread2 ended
複製程式碼

從執行結果可以看出

  • thread1執行wait後,暫停執行
  • thread2執行notify後,thread1並沒有繼續執行,因為此時thread2尚未釋放鎖,thread1因為得不到鎖而不能繼續執行
  • thread2執行完synchronized語句塊後釋放鎖,thread1得到通知並獲得鎖,進而繼續執行

注意:wait方法需要釋放鎖,前提條件是它已經持有鎖。所以wait和notify(或者notifyAll)方法都必須被包裹在synchronized語句塊中,並且synchronized後鎖的物件應該與呼叫wait方法的物件一樣。否則丟擲IllegalMonitorStateException

sleep方法告訴作業系統至少指定時間內不需為執行緒排程器為該執行緒分配執行時間片,並不釋放鎖(如果當前已經持有鎖)。實際上,呼叫sleep方法時並不要求持有任何鎖。

public class Sleep {
  public static void main(String[] args) {
    Thread thread1 = new Thread(() -> {
      synchronized (Sleep.class) {
        try {
          System.out.println(new Date() + " Thread1 is running");
          Thread.sleep(2000);
          System.out.println(new Date() + " Thread1 ended");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
    });
    thread1.start();
    
    Thread thread2 = new Thread(() -> {
      synchronized (Sleep.class) {
        try {
          System.out.println(new Date() + " Thread2 is running");
          Thread.sleep(2000);
          System.out.println(new Date() + " Thread2 ended");
        } catch (Exception ex) {
          ex.printStackTrace();
        }
      }
      
      for(long i = 0; i < 200000; i++) {
        for(long j = 0; j < 100000; j++) {}
      }
    });
    
    // Don't use sleep method to avoid confusing
    for(long i = 0; i < 200000; i++) {
      for(long j = 0; j < 100000; j++) {}
    }
    thread2.start();
  }
}
複製程式碼

執行結果如下

Thu Jun 16 19:46:06 CST 2016 Thread1 is running
Thu Jun 16 19:46:08 CST 2016 Thread1 ended
Thu Jun 16 19:46:13 CST 2016 Thread2 is running
Thu Jun 16 19:46:15 CST 2016 Thread2 ended
複製程式碼

由於thread 1和thread 2的run方法實現都在同步塊中,無論哪個執行緒先拿到鎖,執行sleep時並不釋放鎖,因此其它執行緒無法執行。直到前面的執行緒sleep結束並退出同步塊(釋放鎖),另一個執行緒才得到鎖並執行。

注意:sleep方法並不需要持有任何形式的鎖,也就不需要包裹在synchronized中。

呼叫sleep方法的執行緒,在jstack中顯示的狀態為sleeping。

java.lang.Thread.State: TIMED_WAITING (sleeping)
複製程式碼

呼叫wait方法的執行緒,在jstack中顯示的狀態為on object monitor

java.lang.Thread.State: WAITING (on object monitor)
複製程式碼

synchronized幾種用法

每個Java物件都可以用做一個實現同步的互斥鎖,這些鎖被稱為內建鎖。執行緒進入同步程式碼塊或方法時自動獲得內建鎖,退出同步程式碼塊或方法時自動釋放該內建鎖。進入同步程式碼塊或者同步方法是獲得內建鎖的唯一途徑。

例項同步方法

synchronized用於修飾例項方法(非靜態方法)時,執行該方法需要獲得的是該類例項物件的內建鎖(同一個類的不同例項擁有不同的內建鎖)。如果多個例項方法都被synchronized修飾,則當多個執行緒呼叫同一例項的不同同步方法(或者同一方法)時,需要競爭鎖。但當呼叫的是不同例項的方法時,並不需要競爭鎖。

靜態同步方法

synchronized用於修飾靜態方法時,執行該方法需要獲得的是該類的class物件的內建鎖(一個類只有唯一一個class物件)。呼叫同一個類的不同靜態同步方法時會產生鎖競爭。

同步程式碼塊

synchronized用於修飾程式碼塊時,進入同步程式碼塊需要獲得synchronized關鍵字後面括號內的物件(可以是例項物件也可以是class物件)的內建鎖。

synchronized使用總結

鎖的使用是為了操作臨界資源的正確性,而往往一個方法中並非所有的程式碼都操作臨界資源。換句話說,方法中的程式碼往往並不都需要同步。此時建議不使用同步方法,而使用同步程式碼塊,只對操作臨界資源的程式碼,也即需要同步的程式碼加鎖。這樣做的好處是,當一個執行緒在執行同步程式碼塊時,其它執行緒仍然可以執行該方法內同步程式碼塊以外的部分,充分發揮多執行緒併發的優勢,從而相較於同步整個方法而言提升效能。

釋放Java內建鎖的唯一方式是synchronized方法或者程式碼塊執行結束。若某一執行緒在synchronized方法或程式碼塊內發生死鎖,則對應的內建鎖無法釋放,其它執行緒也無法獲取該內建鎖(即進入跟該內建鎖相關的synchronized方法或者程式碼塊)。

使用jstack dump執行緒棧時,可檢視到相關執行緒通過synchronized獲取到或等待的物件,但Locked ownable synchronizers仍然顯示為None。下例中,執行緒thead-test-b已獲取到型別為java.lang.Double的物件的內建鎖(monitor),且該物件的記憶體地址為0x000000076ab95cb8

"thread-test-b" #11 prio=5 os_prio=31 tid=0x00007fab0190b800 nid=0x5903 runnable [0x0000700010249000]
   java.lang.Thread.State: RUNNABLE
        at com.jasongj.demo.TestJstack.lambda$1(TestJstack.java:27)
        - locked <0x000000076ab95cb8> (a java.lang.Double)
        at com.jasongj.demo.TestJstack$$Lambda$2/1406718218.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
   Locked ownable synchronizers:
        - None
複製程式碼

Java中的鎖

重入鎖

Java中的重入鎖(即ReentrantLock)與Java內建鎖一樣,是一種排它鎖。使用synchronized的地方一定可以用ReentrantLock代替。

重入鎖需要顯示請求獲取鎖,並顯示釋放鎖。為了避免獲得鎖後,沒有釋放鎖,而造成其它執行緒無法獲得鎖而造成死鎖,一般建議將釋放鎖操作放在finally塊裡,如下所示。

try{
  renentrantLock.lock();
  // 使用者操作
} finally {
  renentrantLock.unlock();
}
複製程式碼

如果重入鎖已經被其它執行緒持有,則當前執行緒的lock操作會被阻塞。除了lock()方法之外,重入鎖(或者說鎖介面)還提供了其它獲取鎖的方法以實現不同的效果。

  • lockInterruptibly() 該方法嘗試獲取鎖,若獲取成功立即返回;若獲取不成功則阻塞等待。與lock方法不同的是,在阻塞期間,如果當前執行緒被打斷(interrupt)則該方法丟擲InterruptedException。該方法提供了一種解除死鎖的途徑。
  • tryLock() 該方法試圖獲取鎖,若該鎖當前可用,則該方法立即獲得鎖並立即返回true;若鎖當前不可用,則立即返回false。該方法不會阻塞,並提供給使用者對於成功獲利鎖與獲取鎖失敗進行不同操作的可能性。
  • tryLock(long time, TimeUnit unit) 該方法試圖獲得鎖,若該鎖當前可用,則立即獲得鎖並立即返回true。若鎖當前不可用,則等待相應的時間(由該方法的兩個引數決定):1)若該時間內鎖可用,則獲得鎖,並返回true;2)若等待期間當前執行緒被打斷,則丟擲InterruptedException;3)若等待時間結束仍未獲得鎖,則返回false。

重入鎖可定義為公平鎖或非公平鎖,預設實現為非公平鎖。

  • 公平鎖是指多個執行緒獲取鎖被阻塞的情況下,鎖變為可用時,最新申請鎖的執行緒獲得鎖。可通過在重入鎖(RenentrantLock)的構造方法中傳入true構建公平鎖,如Lock lock = new RenentrantLock(true)
  • 非公平鎖是指多個執行緒等待鎖的情況下,鎖變為可用狀態時,哪個執行緒獲得鎖是隨機的。synchonized相當於非公平鎖。可通過在重入鎖的構造方法中傳入false或者使用無參構造方法構建非公平鎖。

使用jstack dump執行緒棧時,可檢視到獲取到或正在等待的鎖物件,獲取到該鎖的執行緒會在Locked ownable synchronizers處顯示該鎖的物件型別及記憶體地址。在下例中,從Locked ownable synchronizers部分可看到,執行緒thread-test-e獲取到公平重入鎖,且該鎖物件的記憶體地址為0x000000076ae3d708

"thread-test-e" #17 prio=5 os_prio=31 tid=0x00007fefaa0b6800 nid=0x6403 runnable [0x0000700002939000]
   java.lang.Thread.State: RUNNABLE
        at com.jasongj.demo.TestJstack.lambda$4(TestJstack.java:64)
        at com.jasongj.demo.TestJstack$$Lambda$5/466002798.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
   Locked ownable synchronizers:
        - <0x000000076af86810> (a java.util.concurrent.locks.ReentrantLock$FairSync)
複製程式碼

而執行緒thread-test-f由於未獲取到鎖,而處於WAITING(parking)狀態,且它等待的鎖正是上文執行緒thread-test-e獲取的鎖(記憶體地址0x000000076af86810)

"thread-test-f" #18 prio=5 os_prio=31 tid=0x00007fefaa9b2800 nid=0x6603 waiting on condition [0x0000700002a3c000]
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        - parking to wait for  <0x000000076af86810> (a java.util.concurrent.locks.ReentrantLock$FairSync)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
        at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
        at java.util.concurrent.locks.ReentrantLock$FairSync.lock(ReentrantLock.java:224)
        at java.util.concurrent.locks.ReentrantLock.lock(ReentrantLock.java:285)
        at com.jasongj.demo.TestJstack.lambda$5(TestJstack.java:69)
        at com.jasongj.demo.TestJstack$$Lambda$6/33524623.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:745)
   Locked ownable synchronizers:
        - None
複製程式碼

讀寫鎖

鎖可以保證原子性和可見性。而原子性更多是針對寫操作而言。對於讀多寫少的場景,一個讀操作無須阻塞其它讀操作,只需要保證讀和寫或者寫與寫不同時發生即可。此時,如果使用重入鎖(即排它鎖),對效能影響較大。Java中的讀寫鎖(ReadWriteLock)就是為這種讀多寫少的場景而創造的。

實際上,ReadWriteLock介面並非繼承自Lock介面,ReentrantReadWriteLock也只實現了ReadWriteLock介面而未實現Lock介面。ReadLock和WriteLock,是ReentrantReadWriteLock類的靜態內部類,它們實現了Lock介面。

一個ReentrantReadWriteLock例項包含一個ReentrantReadWriteLock.ReadLock例項和一個ReentrantReadWriteLock.WriteLock例項。通過readLock()和writeLock()方法可分別獲得讀鎖例項和寫鎖例項,並通過Lock介面提供的獲取鎖方法獲得對應的鎖。

讀寫鎖的鎖定規則如下:

  • 獲得讀鎖後,其它執行緒可獲得讀鎖而不能獲取寫鎖
  • 獲得寫鎖後,其它執行緒既不能獲得讀鎖也不能獲得寫鎖
public class ReadWriteLockDemo {
  public static void main(String[] args) {
    ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    new Thread(() -> {
      readWriteLock.readLock().lock();
      try {
        System.out.println(new Date() + "\tThread 1 started with read lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
        }
        System.out.println(new Date() + "\tThread 1 ended");
      } finally {
        readWriteLock.readLock().unlock();
      }
    }).start();
    new Thread(() -> {
      readWriteLock.readLock().lock();
      try {
        System.out.println(new Date() + "\tThread 2 started with read lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
        }
        System.out.println(new Date() + "\tThread 2 ended");
      } finally {
        readWriteLock.readLock().unlock();
      }
    }).start();
    new Thread(() -> {
      Lock lock = readWriteLock.writeLock();
      lock.lock();
      try {
        System.out.println(new Date() + "\tThread 3 started with write lock");
        try {
          Thread.sleep(2000);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        System.out.println(new Date() + "\tThread 3 ended");
      } finally {
        lock.unlock();
      }
    }).start();
  }
}
複製程式碼

執行結果如下

Sat Jun 18 21:33:46 CST 2016  Thread 1 started with read lock
Sat Jun 18 21:33:46 CST 2016  Thread 2 started with read lock
Sat Jun 18 21:33:48 CST 2016  Thread 2 ended
Sat Jun 18 21:33:48 CST 2016  Thread 1 ended
Sat Jun 18 21:33:48 CST 2016  Thread 3 started with write lock
Sat Jun 18 21:33:50 CST 2016  Thread 3 ended
複製程式碼

從上面的執行結果可見,thread 1和thread 2都只需獲得讀鎖,因此它們可以並行執行。而thread 3因為需要獲取寫鎖,必須等到thread 1和thread 2釋放鎖後才能獲得鎖。

條件鎖

條件鎖只是一個幫助使用者理解的概念,實際上並沒有條件鎖這種鎖。對於每個重入鎖,都可以通過newCondition()方法繫結若干個條件物件。

條件物件提供以下方法以實現不同的等待語義

  • await() 呼叫該方法的前提是,當前執行緒已經成功獲得與該條件物件繫結的重入鎖,否則呼叫該方法時會丟擲IllegalMonitorStateException。呼叫該方法外,當前執行緒會釋放當前已經獲得的鎖(這一點與上文講述的Java內建鎖的wait方法一致),並且等待其它執行緒呼叫該條件物件的signal()或者signalAll()方法(這一點與Java內建鎖wait後等待notify()或notifyAll()很像)。或者在等待期間,當前執行緒被打斷,則wait()方法會丟擲InterruptedException並清除當前執行緒的打斷狀態。
  • await(long time, TimeUnit unit) 適用條件和行為與await()基本一致,唯一不同點在於,指定時間之內沒有收到signal()或signalALL()訊號或者執行緒中斷時該方法會返回false;其它情況返回true。
  • awaitNanos(long nanosTimeout) 呼叫該方法的前提是,當前執行緒已經成功獲得與該條件物件繫結的重入鎖,否則呼叫該方法時會丟擲IllegalMonitorStateException。nanosTimeout指定該方法等待訊號的的最大時間(單位為納秒)。若指定時間內收到signal()或signalALL()則返回nanosTimeout減去已經等待的時間;若指定時間內有其它執行緒中斷該執行緒,則丟擲InterruptedException並清除當前執行緒的打斷狀態;若指定時間內未收到通知,則返回0或負數。
  • awaitUninterruptibly() 呼叫該方法的前提是,當前執行緒已經成功獲得與該條件物件繫結的重入鎖,否則呼叫該方法時會丟擲IllegalMonitorStateException。呼叫該方法後,結束等待的唯一方法是其它執行緒呼叫該條件物件的signal()或signalALL()方法。等待過程中如果當前執行緒被中斷,該方法仍然會繼續等待,同時保留該執行緒的中斷狀態。
  • awaitUntil(Date deadline) 適用條件與行為與awaitNanos(long nanosTimeout)完全一樣,唯一不同點在於它不是等待指定時間,而是等待由引數指定的某一時刻。

呼叫條件等待的注意事項

  • 呼叫上述任意條件等待方法的前提都是當前執行緒已經獲得與該條件物件對應的重入鎖。
  • 呼叫條件等待後,當前執行緒讓出CPU資源。
  • 上述等待方法結束後,方法返回的前提是它能重新獲得與該條件物件對應的重入鎖。如果無法獲得鎖,仍然會繼續等待。這也是awaitNanos(long nanosTimeout)可能會返回負值的原因。
  • 一旦條件等待方法返回,則當前執行緒肯定已經獲得了對應的重入鎖。
  • 重入鎖可以建立若干個條件物件,signal()和signalAll()方法只能喚醒相同條件物件的等待。
  • 一個重入鎖上可以生成多個條件變數,不同執行緒可以等待不同的條件,從而實現更加細粒度的的執行緒間通訊。

signal()與signalAll()

  • signal() 若有一個或若干個執行緒在等待該條件變數,則該方法會喚醒其中的一個(具體哪一個,無法預測)。呼叫該方法的前提是當前執行緒持有該條件變數對應的鎖,否則丟擲IllegalMonitorStateException。
  • signalALL() 若有一個或若干個執行緒在等待該條件變數,則該方法會喚醒所有等待。呼叫該方法的前提是當前執行緒持有該條件變數對應的鎖,否則丟擲IllegalMonitorStateException。
public class ConditionTest {
  public static void main(String[] args) throws InterruptedException {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    new Thread(() -> {
      lock.lock();
      try {
        System.out.println(new Date() + "\tThread 1 is waiting");
        try {
          long waitTime = condition.awaitNanos(TimeUnit.SECONDS.toNanos(2));
          System.out.println(new Date() + "\tThread 1 remaining time " + waitTime);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        System.out.println(new Date() + "\tThread 1 is waken up");
      } finally {
        lock.unlock();
      }
    }).start();
    
    new Thread(() -> {
      lock.lock();
      try{
        System.out.println(new Date() + "\tThread 2 is running");
        try {
          Thread.sleep(4000);
        } catch (Exception ex) {
          ex.printStackTrace();
        }
        condition.signal();
        System.out.println(new Date() + "\tThread 2 ended");
      } finally {
        lock.unlock();
      }
    }).start();
  }
}
複製程式碼

執行結果如下

Sun Jun 19 15:59:09 CST 2016  Thread 1 is waiting
Sun Jun 19 15:59:09 CST 2016  Thread 2 is running
Sun Jun 19 15:59:13 CST 2016  Thread 2 ended
Sun Jun 19 15:59:13 CST 2016  Thread 1 remaining time -2003467560
Sun Jun 19 15:59:13 CST 2016  Thread 1 is waken up
複製程式碼

從執行結果可以看出,雖然thread 2一開始就呼叫了signal()方法去喚醒thread 1,但是因為thread 2在4秒鐘後才釋放鎖,也即thread 1在4秒後才獲得鎖,所以thread 1的await方法在4秒鐘後才返回,並且返回負值。

相關文章