併發程式設計進階
在我們的程式中,多多少少都會用到多執行緒技術,而我們以往都是使用Thread類來建立一個新的執行緒:
public static void main(String[] args) {
Thread t = new Thread(() -> System.out.println("Hello World!"));
t.start();
}
利用多執行緒,我們的程式可以更加合理地使用CPU多核心資源,在同一時間完成更多的工作。
但是,如果我們的程式頻繁地建立執行緒,由於執行緒的建立和銷燬也需要佔用系統資源,因此這樣會降低我們整個程式的效能,那麼怎麼做,才能更高效地使用多執行緒呢?
我們其實可以將已建立的執行緒複用,利用池化技術,就像資料庫連線池一樣,我們也可以建立很多個執行緒,然後反覆地使用這些執行緒,而不對它們進行銷燬。
雖然聽起來這個想法比較新穎,但是實際上執行緒池早已利用到各個地方。
比如我們的Tomcat伺服器,要在同一時間接受和處理大量的請求,那麼就必須要在短時間內建立大量的執行緒,結束後又進行銷燬,這顯然會導致很大的開銷,因此這種情況下使用執行緒池顯然是更好的解決方案。
由於執行緒池可以反覆利用已有執行緒執行多執行緒操作,所以它一般是有容量限制的,當所有的執行緒都處於工作狀態時,那麼新的多執行緒請求會被阻塞,直到有一個執行緒空閒出來為止,實際上這裡就會用到我們之前講解的阻塞佇列。
所以我們可以暫時得到下面一個圖示:
當然,JUC提供的執行緒池肯定沒有這麼簡單,接下來就讓我們深入進行了解。
執行緒池的使用
我們可以直接建立一個新的執行緒池物件,它已經提前幫助我們實現好了執行緒的排程機制,我們先來看它的構造方法:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
引數稍微有一點多,這裡我們依次進行講解:
- corePoolSize:核心執行緒池大小,我們每向執行緒池提交一個多執行緒任務時,都會建立一個新的
核心執行緒
,無論是否存在其他空閒執行緒,直到到達核心執行緒池大小為止,之後會嘗試複用執行緒資源。當然也可以在一開始就全部初始化好,呼叫prestartAllCoreThreads()
即可。 - maximumPoolSize:最大執行緒池大小,當目前執行緒池中所有的執行緒都處於執行狀態,並且等待佇列已滿,那麼就會直接嘗試繼續建立新的
非核心執行緒
執行,但是不能超過最大執行緒池大小。 - keepAliveTime:執行緒最大空閒時間,當一個
非核心執行緒
空閒超過一定時間,會自動銷燬。 - unit:執行緒最大空閒時間的時間單位
- workQueue:執行緒等待佇列,當執行緒池中核心執行緒數已滿時,就會將任務暫時存到等待佇列中,直到有執行緒資源可用為止,這裡可以使用我們上一章學到的阻塞佇列。
- threadFactory:執行緒建立工廠,我們可以干涉執行緒池中執行緒的建立過程,進行自定義。
- handler:拒絕策略,當等待佇列和執行緒池都沒有空間了,真的不能再來新的任務時,來了個新的多執行緒任務,那麼只能拒絕了,這時就會根據當前設定的拒絕策略進行處理。
最為重要的就是執行緒池大小的限定了,這個也是很有學問的,合理地分配大小會使得執行緒池的執行效率事半功倍:
- 首先我們可以分析一下,執行緒池執行任務的特性,是CPU 密集型還是 IO 密集型
- CPU密集型:主要是執行計算任務,響應時間很快,CPU一直在執行,這種任務CPU的利用率很高,那麼執行緒數應該是根據 CPU 核心數來決定,CPU 核心數 = 最大同時執行執行緒數。
- IO密集型:主要是進行 IO 操作,因為執行 IO 操作的時間比較較長,比如從硬碟讀取資料之類的,CPU就得等著IO操作,很容易出現空閒狀態,導致 CPU 的利用率不高,這種情況下可以適當增加執行緒池的大小,讓更多的執行緒可以一起進行IO操作,一般可以配置為CPU核心數的2倍。
這裡我們手動建立一個新的執行緒池看看效果:
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(2, 4, //2個核心執行緒,最大執行緒數為4個
3, TimeUnit.SECONDS, //最大空閒時間為3秒鐘
new ArrayBlockingQueue<>(2)); //這裡使用容量為2的ArrayBlockingQueue佇列
for (int i = 0; i < 6; i++) { //開始6個任務
int finalI = i;
executor.execute(() -> {
try {
System.out.println(Thread.currentThread().getName()+" 開始執行!("+ finalI);
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName()+" 已結束!("+finalI);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
TimeUnit.SECONDS.sleep(1); //看看當前執行緒池中的執行緒數量
System.out.println("執行緒池中執行緒數量:"+executor.getPoolSize());
TimeUnit.SECONDS.sleep(5); //等到超過空閒時間
System.out.println("執行緒池中執行緒數量:"+executor.getPoolSize());
executor.shutdownNow(); //使用完執行緒池記得關閉,不然程式不會結束,它會取消所有等待中的任務以及試圖中斷正在執行的任務,關閉後,無法再提交任務,一律拒絕
//executor.shutdown(); 同樣可以關閉,但是會執行完等待佇列中的任務再關閉
}
這裡我們建立了一個核心容量為2,最大容量為4,等待佇列長度為2,空閒時間為3秒的執行緒池,
現在我們向其中執行6個任務,每個任務都會進行1秒鐘休眠,那麼當執行緒池中2個核心執行緒都被佔用時,還有4個執行緒就只能進入到等待佇列中了,但是等待佇列中只有2個容量,這時緊接著的2個任務,執行緒池將直接嘗試建立執行緒,由於不大於最大容量,因此可以成功建立。最後所有執行緒完成之後,在等待5秒後,超過了執行緒池的最大空閒時間,非核心執行緒
被回收了,所以執行緒池中只有2個執行緒存在。
那麼要是等待佇列設定為沒有容量的SynchronousQueue呢,這個時候會發生什麼?
pool-1-thread-1 開始執行!(0
pool-1-thread-4 開始執行!(3
pool-1-thread-3 開始執行!(2
pool-1-thread-2 開始執行!(1
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.test.Main$$Lambda$1/1283928880@682a0b20 rejected from java.util.concurrent.ThreadPoolExecutor@3d075dc0[Running, pool size = 4, active threads = 4, queued tasks = 0, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at com.test.Main.main(Main.java:15)
可以看到,前4個任務都可以正常執行,但是到第五個任務時,直接丟擲了異常,這其實就是因為等待佇列的容量為0,相當於沒有容量,那麼這個時候,就只能拒絕任務了,拒絕的操作會根據拒絕策略決定。
執行緒池的拒絕策略預設有以下幾個:
- AbortPolicy(預設):像上面一樣,直接拋異常。
- CallerRunsPolicy:直接讓提交任務的執行緒執行這個任務,比如在主執行緒向執行緒池提交了任務,那麼就直接由主執行緒執行。
- DiscardOldestPolicy:丟棄佇列中最近的一個任務,替換為當前任務。
- DiscardPolicy:什麼也不用做。
這裡我們進行一下測試:
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(2, 4,
3, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadPoolExecutor.CallerRunsPolicy());
//使用另一個構造方法,最後一個引數傳入策略,比如這裡我們使用了CallerRunsPolicy策略
CallerRunsPolicy策略是誰提交的誰自己執行,所以:
pool-1-thread-1 開始執行!(0
pool-1-thread-2 開始執行!(1
main 開始執行!(4
pool-1-thread-4 開始執行!(3
pool-1-thread-3 開始執行!(2
pool-1-thread-3 已結束!(2
pool-1-thread-2 已結束!(1
pool-1-thread-1 已結束!(0
main 已結束!(4
pool-1-thread-4 已結束!(3
pool-1-thread-1 開始執行!(5
pool-1-thread-1 已結束!(5
執行緒池中執行緒數量:4
執行緒池中執行緒數量:2
可以看到,當佇列塞不下時,直接在主執行緒執行任務,執行完之後再繼續向下執行。
我們把策略修改為DiscardOldestPolicy試試看:
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(2, 4,
3, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1),
//這裡設定為ArrayBlockingQueue,長度為1
new ThreadPoolExecutor.DiscardOldestPolicy());
它會移除等待佇列中的最近的一個任務,所以可以看到有一個任務實際上是被拋棄了的:
pool-1-thread-1 開始執行!(0
pool-1-thread-4 開始執行!(4
pool-1-thread-3 開始執行!(3
pool-1-thread-2 開始執行!(1
pool-1-thread-1 已結束!(0
pool-1-thread-4 已結束!(4
pool-1-thread-1 開始執行!(5
執行緒池中執行緒數量:4
pool-1-thread-3 已結束!(3
pool-1-thread-2 已結束!(1
pool-1-thread-1 已結束!(5
執行緒池中執行緒數量:2
比較有意思的是,如果選擇沒有容量的SynchronousQueue作為等待佇列會爆棧:
pool-1-thread-1 開始執行!(0
pool-1-thread-3 開始執行!(2
pool-1-thread-2 開始執行!(1
pool-1-thread-4 開始執行!(3
Exception in thread "main" java.lang.StackOverflowError
at java.util.concurrent.SynchronousQueue.offer(SynchronousQueue.java:912)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1371)
...
pool-1-thread-1 已結束!(0
pool-1-thread-2 已結束!(1
pool-1-thread-4 已結束!(3
pool-1-thread-3 已結束!(2
這是為什麼呢?
我們來看看這個拒絕策略的原始碼:
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public DiscardOldestPolicy() { }
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll(); //會先執行一次出隊操作,但是這對於SynchronousQueue來說毫無意義
e.execute(r); //這裡會再次呼叫execute方法
}
}
}
可以看到,它會先對等待佇列進行出隊操作,但是由於SynchronousQueue壓根沒容量,所有這個操作毫無意義,然後就會遞迴執行execute
方法,而進入之後,又發現沒有容量不能插入,於是又重複上面的操作,這樣就會無限的遞迴下去,最後就爆棧了。
當然,除了使用官方提供的4種策略之外,我們還可以使用自定義的策略:
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(2, 4,
3, TimeUnit.SECONDS,
new SynchronousQueue<>(),
(r, executor1) -> { //比如這裡我們也來實現一個就在當前執行緒執行的策略
System.out.println("哎呀,執行緒池和等待佇列都滿了,你自己耗子尾汁吧");
r.run(); //直接執行
});
接著我們來看執行緒建立工廠,我們可以自己決定如何建立新的執行緒:
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor =
new ThreadPoolExecutor(2, 4,
3, TimeUnit.SECONDS,
new SynchronousQueue<>(),
new ThreadFactory() {
int counter = 0;
@Override
public Thread newThread(Runnable r) {
return new Thread(r, "我的自定義執行緒-"+counter++);
}
});
for (int i = 0; i < 4; i++) {
executor.execute(() -> System.out.println(Thread.currentThread().getName()+" 開始執行!"));
}
}
這裡傳入的Runnable物件就是我們提交的任務,可以看到需要我們返回一個Thread物件,其實就是執行緒池建立執行緒的過程,而如何建立這個物件,以及它的一些屬性,就都由我們來決定。
各位有沒有想過這樣一個情況,如果我們的任務在執行過程中出現異常了,那麼是不是會導致執行緒池中的執行緒被銷燬呢?
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, //最大容量和核心容量鎖定為1
0, TimeUnit.MILLISECONDS, new LinkedBlockingDeque<>());
executor.execute(() -> {
System.out.println(Thread.currentThread().getName());
throw new RuntimeException("我是異常!");
});
TimeUnit.SECONDS.sleep(1);
executor.execute(() -> {
System.out.println(Thread.currentThread().getName());
});
}
可以看到,出現異常之後,再次提交新的任務,執行的執行緒是一個新的執行緒了。
除了我們自己建立執行緒池之外,官方也提供了很多的執行緒池定義,我們可以使用Executors
工具類來快速建立執行緒池:
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2); //直接建立一個固定容量的執行緒池
}
可以看到它的內部實現為:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
這裡直接將最大執行緒和核心執行緒數量設定為一樣的,並且等待時間為0,因為壓根不需要,並且採用的是一個無界的LinkedBlockingQueue作為等待佇列。
使用newSingleThreadExecutor來建立只有一個執行緒的執行緒池:
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
//建立一個只有一個執行緒的執行緒池
}
原理如下:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
可以看到這裡並不是直接建立的一個ThreadPoolExecutor物件,而是套了一層FinalizableDelegatedExecutorService,那麼這個又是什麼東西呢?
static class FinalizableDelegatedExecutorService
extends DelegatedExecutorService {
FinalizableDelegatedExecutorService(ExecutorService executor) {
super(executor);
}
protected void finalize() { //在GC時,會執行finalize方法,此方法中會關閉掉執行緒池,釋放資源
super.shutdown();
}
}
static class DelegatedExecutorService extends AbstractExecutorService {
private final ExecutorService e; //被委派物件
DelegatedExecutorService(ExecutorService executor) { e = executor; } //實際上所以的操作都是讓委派物件執行的,有點像代理
public void execute(Runnable command) { e.execute(command); }
public void shutdown() { e.shutdown(); }
public List<Runnable> shutdownNow() { return e.shutdownNow(); }
所以,下面兩種寫法的區別在於:
public static void main(String[] args) throws InterruptedException {
ExecutorService executor1 = Executors.newSingleThreadExecutor();
ExecutorService executor2 = Executors.newFixedThreadPool(1);
}
前者實際上是被代理了,我們沒辦法直接修改前者的相關屬性,顯然使用前者建立只有一個執行緒的執行緒池更加專業和安全(可以防止屬性被修改)一些。
最後我們來看newCachedThreadPool
方法:
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newCachedThreadPool();
//它是一個會根據需要無限制建立新執行緒的執行緒池
}
我們來看看它的實現:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
可以看到,核心執行緒數為0,那麼也就是說所有的執行緒都是非核心執行緒
,也就是說執行緒空閒時間超過1秒鐘,一律銷燬。
但是它的最大容量是Integer.MAX_VALUE
,也就是說,它可以無限制地增長下去,所以一定要慎用。
執行帶返回值的任務
一個多執行緒任務不僅僅可以是void無返回值任務,比如我們現在需要執行一個任務,但是我們需要在任務執行之後得到一個結果,這個時候怎麼辦呢?
這裡我們就可以使用到Future了,它可以返回任務的計算結果,我們可以通過它來獲取任務的結果以及任務當前是否完成:
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newSingleThreadExecutor();
//直接用Executors建立,方便就完事了
Future<String> future = executor.submit(() -> "我是字串!");
//使用submit提交任務,會返回一個Future物件
//注意提交的物件可以是Runable也可以是Callable,這裡使用的是Callable能夠自定義返回值
System.out.println(future.get());
//如果任務未完成,get會被阻塞,任務完成返回Callable執行結果返回值
executor.shutdown();
}
當然結果也可以一開始就定義好,然後等待Runnable執行完之後再返回:
public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "我是字串!");
System.out.println(future.get());
executor.shutdown();
}
還可以通過傳入FutureTask物件的方式:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService service = Executors.newSingleThreadExecutor();
FutureTask<String> task = new FutureTask<>(() -> "我是字串!");
service.submit(task);
System.out.println(task.get());
executor.shutdown();
}
我們可以還通過Future物件獲取當前任務的一些狀態:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> "都看到這裡了,不賞UP主一個一鍵三連嗎?");
System.out.println(future.get());
System.out.println("任務是否執行完成:"+future.isDone());
System.out.println("任務是否被取消:"+future.isCancelled());
executor.shutdown();
}
我們來試試看在任務執行途中取消任務:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> {
TimeUnit.SECONDS.sleep(10);
return "這次一定!";
});
System.out.println(future.cancel(true));
System.out.println(future.isCancelled());
executor.shutdown();
}
執行定時任務
既然執行緒池怎麼強大,那麼執行緒池能不能執行定時任務呢?
我們之前如果需要執行一個定時任務,那麼肯定會用到Timer和TimerTask,但是它只會建立一個執行緒處理我們的定時任務,無法實現多執行緒排程,並且它無法處理異常情況一旦丟擲未捕獲異常那麼會直接終止,顯然我們需要一個更加強大的定時器。
JDK5之後,我們可以使用ScheduledThreadPoolExecutor來提交定時任務,它繼承自ThreadPoolExecutor,並且所有的構造方法都必須要求最大執行緒池容量為Integer.MAX_VALUE,並且都是採用的DelayedWorkQueue作為等待佇列。
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), handler);
}
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
我們來測試一下它的方法,這個方法可以提交一個延時任務,只有到達指定時間之後才會開始:
public static void main(String[] args) throws ExecutionException, InterruptedException {
//直接設定核心執行緒數為1
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(1);
//這裡我們計劃在3秒後執行
executor.schedule(() -> System.out.println("HelloWorld!"), 3, TimeUnit.SECONDS);
executor.shutdown();
}
我們也可以像之前一樣,傳入一個Callable物件,用於接收返回值:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
//這裡使用ScheduledFuture
ScheduledFuture<String> future = executor.schedule(() -> "????", 3, TimeUnit.SECONDS);
System.out.println("任務剩餘等待時間:"+future.getDelay(TimeUnit.MILLISECONDS) / 1000.0 + "s");
System.out.println("任務執行結果:"+future.get());
executor.shutdown();
}
可以看到schedule
方法返回了一個ScheduledFuture物件,和Future一樣,它也支援返回值的獲取、包括對任務的取消同時還支援獲取剩餘等待時間。
那麼如果我們希望按照一定的頻率不斷執行任務呢?
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledThreadPoolExecutor executor = new ScheduledThreadPoolExecutor(2);
executor.scheduleAtFixedRate(() -> System.out.println("Hello World!"),
3, 1, TimeUnit.SECONDS);
//三秒鐘延遲開始,之後每隔一秒鐘執行一次
}
Executors也為我們預置了newScheduledThreadPool方法用於建立執行緒池:
public static void main(String[] args) throws ExecutionException, InterruptedException {
ScheduledExecutorService service = Executors.newScheduledThreadPool(1);
service.schedule(() -> System.out.println("Hello World!"), 1, TimeUnit.SECONDS);
}
執行緒池實現原理
前面我們瞭解了執行緒池的使用,那麼接著我們來看看它的詳細實現過程,結構稍微有點複雜。
這裡需要首先介紹一下ctl變數:
//這個變數比較關鍵,用到了原子AtomicInteger。
//用於同時儲存執行緒池執行狀態和執行緒數量(使用原子類是為了保證原子性)。
//它是通過拆分32個bit位來儲存資料的,前3位儲存狀態。
//後29位儲存工作執行緒數量。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3; //29位,執行緒數量位
private static final int CAPACITY = (1 << COUNT_BITS) - 1; //計算得出最大容量(1左移29位,最大容量為2的29次方-1)
// 所有的執行狀態,注意都是隻佔用前3位,不會佔用後29位
// 接收新任務,並等待執行佇列中的任務
private static final int RUNNING = -1 << COUNT_BITS; //111 | 0000... (後29數量位,下同)
// 不接收新任務,但是依然等待執行佇列中的任務
private static final int SHUTDOWN = 0 << COUNT_BITS; //000 | 數量位
// 不接收新任務,也不執行佇列中的任務,並且還要中斷正在執行中的任務
private static final int STOP = 1 << COUNT_BITS; //001 | 數量位
// 所有的任務都已結束,執行緒數量為0,即將完全關閉
private static final int TIDYING = 2 << COUNT_BITS; //010 | 數量位
// 完全關閉
private static final int TERMINATED = 3 << COUNT_BITS; //011 | 數量位
// 封裝和解析ctl變數的一些方法
private static int runStateOf(int c) { return c & ~CAPACITY; } //對CAPACITY取反就是後29位全部為0,前三位全部為1,接著與c進行與運算,這樣就可以只得到前三位的結果了,所以這裡是取執行狀態
private static int workerCountOf(int c) { return c & CAPACITY; }
//同上,這裡是為了得到後29位的結果,所以這裡是取執行緒數量
private static int ctlOf(int rs, int wc) { return rs | wc; }
// 比如上面的RUNNING, 0,進行與運算之後:
// 111 | 0000000000000000000000000
我們先從最簡單的入手,看看在呼叫execute
方法之後,執行緒池會做些什麼:
//這個就是我們指定的阻塞佇列
private final BlockingQueue<Runnable> workQueue;
//再次提醒,這裡沒加鎖!!該有什麼意識不用我說了吧,所以說ctl才會使用原子類。
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException(); //如果任務為null,那執行個寂寞,所以說直接空指標
int c = ctl.get(); //獲取ctl的值,一會要讀取資訊的
if (workerCountOf(c) < corePoolSize) { //判斷工作執行緒數量是否小於核心執行緒數
if (addWorker(command, true)) //如果是,那不管三七二十一,直接加新的執行緒執行,然後返回即可
return;
c = ctl.get(); //如果執行緒新增失敗(有可能其他執行緒也在對執行緒池進行操作),那就更新一下c的值
}
if (isRunning(c) && workQueue.offer(command)) { //繼續判斷,如果當前執行緒池是執行狀態,那就嘗試向阻塞佇列中新增一個新的等待任務
int recheck = ctl.get(); //再次獲取ctl的值
if (! isRunning(recheck) && remove(command)) //這裡是再次確認當前執行緒池是否關閉,如果新增等待任務後執行緒池關閉了,那就把剛剛加進去任務的又拿出來
reject(command); //然後直接拒絕當前任務的提交(會根據我們的拒絕策略決定如何進行拒絕操作)
else if (workerCountOf(recheck) == 0) //如果這個時候執行緒池依然在執行狀態,那麼就檢查一下當前工作執行緒數是否為0,如果是那就直接新增新執行緒執行
addWorker(null, false); //新增一個新的非核心執行緒,但是注意沒新增任務
//其他情況就啥也不用做了
}
else if (!addWorker(command, false)) //這種情況要麼就是執行緒池沒有執行,要麼就是佇列滿了,按照我們之前的規則,核心執行緒數已滿且佇列已滿,那麼會直接新增新的非核心執行緒,但是如果已經新增到最大數量,這裡肯定是會失敗的
reject(command); //確實裝不下了,只能拒絕
}
是不是感覺思路還挺清晰的,我們接著來看addWorker
是怎麼建立和執行任務的,又是一大堆程式碼:
private boolean addWorker(Runnable firstTask, boolean core) {
//這裡給最外層迴圈打了個標籤,方便一會的跳轉操作
retry:
for (;;) { //無限迴圈,老套路了,注意這裡全程沒加鎖
int c = ctl.get(); //獲取ctl值
int rs = runStateOf(c); //解析當前的執行狀態
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && //判斷執行緒池是否不是處於執行狀態
! (rs == SHUTDOWN && //如果不是執行狀態,判斷執行緒是SHUTDOWN狀態並、任務不為null、等待佇列不為空,只要有其中一者不滿足,直接返回false,新增失敗
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) { //內層又一輪無限迴圈,這個迴圈是為了將執行緒計數增加,然後才可以真正地新增一個新的執行緒
int wc = workerCountOf(c); //解析當前的工作執行緒數量
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize)) //判斷一下還裝得下不,如果裝得下,看看是核心執行緒還是非核心執行緒,如果是核心執行緒,不能大於核心執行緒數的限制,如果是非核心執行緒,不能大於最大執行緒數限制
return false;
if (compareAndIncrementWorkerCount(c)) //CAS自增執行緒計數,如果增加成功,任務完成,直接跳出繼續
break retry; //注意這裡要直接跳出最外層迴圈,所以用到了標籤(類似於goto語句)
c = ctl.get(); // 如果CAS失敗,更新一下c的值
if (runStateOf(c) != rs) //如果CAS失敗的原因是因為執行緒池狀態和一開始的不一樣了,那麼就重新從外層迴圈再來一次
continue retry; //注意這裡要直接從最外層迴圈繼續,所以用到了標籤(類似於goto語句)
// 如果是其他原因導致的CAS失敗,那隻可能是其他執行緒同時在自增,所以重新再來一次內層迴圈
}
}
//好了,執行緒計數自增也完了,接著就是新增新的工作執行緒了
boolean workerStarted = false; //工作執行緒是否已啟動
boolean workerAdded = false; //工作執行緒是否已新增
Worker w = null; //暫時理解為工作執行緒,別急,我們之後會解讀Worker類
try {
w = new Worker(firstTask); //建立新的工作執行緒,傳入我們提交的任務
final Thread t = w.thread; //拿到工作執行緒中封裝的Thread物件
if (t != null) { //如果執行緒不為null,那就可以安排幹活了
final ReentrantLock mainLock = this.mainLock; //又是ReentrantLock加鎖環節,這裡開始就是隻有一個執行緒能進入了
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get()); //獲取當前執行緒的執行狀態
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) { //只有當前執行緒池是正在執行狀態,或是SHUTDOWN狀態且firstTask為空,那麼就繼續
if (t.isAlive()) // 檢查一下執行緒是否正在執行狀態
throw new IllegalThreadStateException(); //如果是那肯定是不能執行我們的任務的
workers.add(w); //直接將新建立的Work丟進 workers 集合中
int s = workers.size(); //看看當前workers的大小
if (s > largestPoolSize) //這裡是記錄執行緒池執行以來,歷史上的最多執行緒數
largestPoolSize = s;
workerAdded = true; //工作執行緒已新增
}
} finally {
mainLock.unlock(); //解鎖
}
if (workerAdded) {
t.start(); //啟動執行緒
workerStarted = true; //工作執行緒已啟動
}
}
} finally {
if (! workerStarted) //如果執行緒在上面的啟動過程中失敗了
addWorkerFailed(w); //將w移出workers並將計數器-1,最後如果執行緒池是終止狀態,會嘗試加速終止執行緒池
}
return workerStarted; //返回是否成功
}
接著我們來看Worker類是如何實現的,它繼承自AbstractQueuedSynchronizer,時隔兩章,居然再次遇到AQS,那也就是說,它本身就是一把鎖:
private final class Worker
extends AbstractQueuedSynchronizer
implements Runnable {
//用來幹活的執行緒
final Thread thread;
//要執行的第一個任務,構造時就確定了的
Runnable firstTask;
//幹活數量計數器,也就是這個執行緒完成了多少個任務
volatile long completedTasks;
Worker(Runnable firstTask) {
setState(-1); // 執行Task之前不讓中斷,將AQS的state設定為-1
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this); //通過預定義或是我們自定義的執行緒工廠建立執行緒
}
public void run() {
runWorker(this); //真正開始幹活,包括當前活幹完了又要等新的活來,就從這裡開始,一會詳細介紹
}
//0就是沒加鎖,1就是已加鎖
protected boolean isHeldExclusively() {
return getState() != 0;
}
...
}
最後我們來看看一個Worker到底是怎麼在進行任務的:
final void runWorker(Worker w) {
Thread wt = Thread.currentThread(); //獲取當前執行緒
Runnable task = w.firstTask; //取出要執行的任務
w.firstTask = null; //然後把Worker中的任務設定為null
w.unlock(); // 因為一開始為-1,這裡是通過unlock操作將其修改回0,只有state大於等於0才能響應中斷
boolean completedAbruptly = true;
try {
//只要任務不為null,或是任務為空但是可以從等待佇列中取出任務不為空,那麼就開始執行這個任務,注意這裡是無限迴圈,也就是說如果當前沒有任務了,那麼會在getTask方法中卡住,因為要從阻塞佇列中等著取任務
while (task != null || (task = getTask()) != null) {
w.lock(); //對當前Worker加鎖,這裡其實並不是防其他執行緒,而是在shutdown時保護此任務的執行
//由於執行緒池在STOP狀態及以上會禁止新執行緒加入並且中斷正在進行的執行緒
if ((runStateAtLeast(ctl.get(), STOP) || //只要執行緒池是STOP及以上的狀態,那肯定是不能開始新任務的
(Thread.interrupted() && //執行緒是否已經被打上中斷標記並且執行緒一定是STOP及以上
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted()) //再次確保執行緒被沒有打上中斷標記
wt.interrupt(); //打中斷標記
try {
beforeExecute(wt, task); //開始之前的準備工作,這裡暫時沒有實現
Throwable thrown = null;
try {
task.run(); //OK,開始執行任務
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown); //執行之後的工作,也沒實現
}
} finally {
task = null; //任務已完成,不需要了
w.completedTasks++; //任務完成數++
w.unlock(); //解鎖
}
}
completedAbruptly = false;
} finally {
//如果能走到這一步,那說明上面的迴圈肯定是跳出了,也就是說這個Worker可以丟棄了
//所以這裡會直接將 Worker 從 workers 裡刪除掉
processWorkerExit(w, completedAbruptly);
}
}
那麼它是怎麼從阻塞佇列裡面獲取任務的呢:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) { //無限迴圈獲取
int c = ctl.get(); //獲取ctl
int rs = runStateOf(c); //解析執行緒池執行狀態
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) { //判斷是不是沒有必要再執行等待佇列中的任務了,也就是處於關閉執行緒池的狀態了
decrementWorkerCount(); //直接減少一個工作執行緒數量
return null; //返回null,這樣上面的runWorker就直接結束了,下同
}
int wc = workerCountOf(c); //如果執行緒池執行正常,那就獲取當前的工作執行緒數量
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize; //如果執行緒數大於核心執行緒數或是允許核心執行緒等待超時,那麼就標記為可超時的
//超時或maximumPoolSize在執行期間被修改了,並且執行緒數大於1或等待佇列為空,那也是不能獲取到任務的
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c)) //如果CAS減少工作執行緒成功
return null; //返回null
continue; //否則開下一輪迴圈
}
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : //如果可超時,那麼最多等到超時時間
workQueue.take(); //如果不可超時,那就一直等著拿任務
if (r != null) //如果成功拿到任務,ok,返回
return r;
timedOut = true; //否則就是超時了,下一輪迴圈將直接返回null
} catch (InterruptedException retry) {
timedOut = false;
}
//開下一輪迴圈吧
}
}
接著我們來看當執行緒池關閉時會做什麼事情:
//普通的shutdown會繼續將等待佇列中的執行緒執行完成後再關閉執行緒池
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
//判斷是否有許可權終止
checkShutdownAccess();
//CAS將執行緒池執行狀態改為SHUTDOWN狀態,還算比較溫柔,詳細過程看下面
advanceRunState(SHUTDOWN);
//讓閒著的執行緒(比如正在等新的任務)中斷,但是並不會影響正在執行的執行緒,詳細過程請看下面
interruptIdleWorkers();
onShutdown(); //給ScheduledThreadPoolExecutor提供的鉤子方法,就是等ScheduledThreadPoolExecutor去實現的,當前類沒有實現
} finally {
mainLock.unlock();
}
tryTerminate(); //最後嘗試終止執行緒池
}
private void advanceRunState(int targetState) {
for (;;) {
int c = ctl.get(); //獲取ctl
if (runStateAtLeast(c, targetState) || //是否大於等於指定的狀態
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c)))) //CAS設定ctl的值
break; //任意一個條件OK就可以結束了
}
}
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread; //拿到Worker中的執行緒
if (!t.isInterrupted() && w.tryLock()) { //先判斷一下執行緒是不是沒有被中斷然後嘗試加鎖,但是通過前面的runWorker()原始碼我們得知,開始之後是讓Worker加了鎖的,所以如果執行緒還在執行任務,那麼這裡肯定會false
try {
t.interrupt(); //如果走到這裡,那麼說明執行緒肯定是一個閒著的執行緒,直接給中斷吧
} catch (SecurityException ignore) {
} finally {
w.unlock(); //解鎖
}
}
if (onlyOne) //如果只針對一個Worker,那麼就結束迴圈
break;
}
} finally {
mainLock.unlock();
}
}
而shutdownNow()
方法也差不多,但是這裡會更直接一些:
//shutdownNow開始後,不僅不允許新的任務到來,也不會再執行等待佇列的執行緒,而且會終止正在執行的執行緒
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
//這裡就是直接設定為STOP狀態了,不再像shutdown那麼溫柔
advanceRunState(STOP);
//直接中斷所有工作執行緒,詳細過程看下面
interruptWorkers();
//取出仍處於阻塞佇列中的執行緒
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks; //最後返回還沒開始的任務
}
private void interruptWorkers() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) //遍歷所有Worker
w.interruptIfStarted(); //無差別對待,一律加中斷標記
} finally {
mainLock.unlock();
}
}
最後的最後,我們再來看看tryTerminate()
是怎麼完完全全終止掉一個執行緒池的:
final void tryTerminate() {
for (;;) { //無限迴圈
int c = ctl.get(); //上來先獲取一下ctl值
//只要是正在執行 或是 執行緒池基本上關閉了 或是 處於SHUTDOWN狀態且工作佇列不為空,那麼這時還不能關閉執行緒池,返回
if (isRunning(c) ||
runStateAtLeast(c, TIDYING) ||
(runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
return;
//走到這裡,要麼處於SHUTDOWN狀態且等待佇列為空或是STOP狀態
if (workerCountOf(c) != 0) { // 如果工作執行緒數不是0,這裡也會中斷空閒狀態下的執行緒
interruptIdleWorkers(ONLY_ONE); //這裡最多隻中斷一個空閒執行緒,然後返回
return;
}
//走到這裡,工作執行緒也為空了,可以終止執行緒池了
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) { //先CAS將狀態設定為TIDYING表示基本終止,正在做最後的操作
try {
terminated(); //終止,暫時沒有實現
} finally {
ctl.set(ctlOf(TERMINATED, 0)); //最後將狀態設定為TERMINATED,執行緒池結束了它年輕的生命
termination.signalAll(); //如果有執行緒呼叫了awaitTermination方法,會等待當前執行緒池終止,到這裡差不多就可以喚醒了
}
return; //結束
}
//注意如果CAS失敗會直接進下一輪迴圈重新判斷
} finally {
mainLock.unlock();
}
// else retry on failed CAS
}
}
併發工具類
計數器鎖 CountDownLatch
多工同步神器。它允許一個或多個執行緒,等待其他執行緒完成工作,比如現在我們有這樣的一個需求:
- 有20個計算任務,我們需要先將這些任務的結果全部計算出來,每個任務的執行時間未知
- 當所有任務結束之後,立即整合統計最終結果
要實現這個需求,那麼有一個很麻煩的地方,我們不知道任務到底什麼時候執行完畢,那麼可否將最終統計延遲一定時間進行呢?
但是最終統計無論延遲多久進行,要麼不能保證所有任務都完成,要麼可能所有任務都完成了而這裡還在等。
所以說,我們需要一個能夠實現子任務同步的工具。
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(20); //建立一個初始值為10的計數器鎖
for (int i = 0; i < 20; i++) {
int finalI = i;
new Thread(() -> {
try {
Thread.sleep((long) (2000 * new Random().nextDouble()));
System.out.println("子任務"+ finalI +"執行完成!");
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown(); //每執行一次計數器都會-1
}).start();
}
//開始等待所有的執行緒完成,當計數器為0時,恢復執行
latch.await(); //這個操作可以同時被多個執行緒執行,一起等待,這裡只演示了一個
System.out.println("所有子任務都完成!任務完成!!!");
//注意這個計數器只能使用一次,用完只能重新創一個,沒有重置的說法
}
我們在呼叫await()
方法之後,實際上就是一個等待計數器衰減為0的過程,而進行自減操作則由各個子執行緒來完成,當子執行緒完成工作後,那麼就將計數器-1,所有的子執行緒完成之後,計數器為0,結束等待。
那麼它是如何實現的呢?實現 原理非常簡單:
public class CountDownLatch {
//同樣是通過內部類實現AbstractQueuedSynchronizer
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) { //這裡直接使用AQS的state作為計數器(可見state能被玩出各種花樣),也就是說一開始就加了count把共享鎖,當執行緒呼叫countdown時,就解一層鎖
setState(count);
}
int getCount() {
return getState();
}
//採用共享鎖機制,因為可以被不同的執行緒countdown,所以實現的tryAcquireShared和tryReleaseShared
//獲取這把共享鎖其實就是去等待state被其他執行緒減到0
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// 每次執行都會將state值-1,直到為0
for (;;) {
int c = getState();
if (c == 0)
return false; //如果已經是0了,那就false
int nextc = c-1;
if (compareAndSetState(c, nextc)) //CAS設定state值,失敗直接下一輪迴圈
return nextc == 0; //返回c-1之後,是不是0,如果是那就true,否則false,也就是說只有剛好減到0的時候才會返回true
}
}
}
private final Sync sync;
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0"); //count那肯定不能小於0啊
this.sync = new Sync(count); //構造Sync物件,將count作為state初始值
}
//通過acquireSharedInterruptibly方法獲取共享鎖,但是如果state不為0,那麼會被持續阻塞,詳細原理下面講
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//同上,但是會超時
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
//countDown其實就是解鎖一次
public void countDown() {
sync.releaseShared(1);
}
//獲取當前的計數,也就是AQS中state的值
public long getCount() {
return sync.getCount();
}
//這個就不說了
public String toString() {
return super.toString() + "[Count = " + sync.getCount() + "]";
}
}
在深入講解之前,我們先大致瞭解一下CountDownLatch的基本實現思路:
- 利用共享鎖實現
- 在一開始的時候就是已經上了count層鎖的狀態,也就是
state = count
await()
就是加共享鎖,但是必須state
為0
才能加鎖成功,否則按照AQS的機制,會進入等待佇列阻塞,加鎖成功後結束阻塞countDown()
就是解1
層鎖,也就是靠這個方法一點一點把state
的值減到0
由於我們前面只對獨佔鎖進行了講解,沒有對共享鎖進行講解,這裡還是稍微提一下它:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) //上來就呼叫tryAcquireShared嘗試以共享模式獲取鎖,小於0則失敗,上面判斷的是state==0返回1,否則-1,也就是說如果計數器不為0,那麼這裡會判斷成功
doAcquireShared(arg); //計數器不為0的時候,按照它的機制,那麼會阻塞,所以我們來看看doAcquireShared中是怎麼進行阻塞的
}
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED); //向等待佇列中新增一個新的共享模式結點
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { //無限迴圈
final Node p = node.predecessor(); //獲取當前節點的前驅的結點
if (p == head) { //如果p就是頭結點,那麼說明當前結點就是第一個等待節點
int r = tryAcquireShared(arg); //會再次嘗試獲取共享鎖
if (r >= 0) { //要是獲取成功
setHeadAndPropagate(node, r); //那麼就將當前節點設定為新的頭結點,並且會繼續喚醒後繼節點
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) && //和獨佔模式下一樣的操作,這裡不多說了
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node); //如果最後都還是沒獲取到,那麼就cancel
}
}
//其實感覺大體上和獨佔模式的獲取有點像,但是它多了個傳播機制,會繼續喚醒後續節點
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // 取出頭結點並將當前節點設定為新的頭結點
setHead(node);
//因為一個執行緒成功獲取到共享鎖之後,有可能剩下的等待中的節點也有機會拿到共享鎖
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) { //如果propagate大於0(表示共享鎖還能繼續獲取)或是h.waitStatus < 0,這是由於在其他執行緒釋放共享鎖時,doReleaseShared會將狀態設定為PROPAGATE表示可以傳播喚醒,後面會講
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared(); //繼續喚醒下一個等待節點
}
}
我們接著來看,它的countdown過程:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) { //直接嘗試釋放鎖,如果成功返回true(在CountDownLatch中只有state減到0的那一次,會返回true)
doReleaseShared(); //這裡也會呼叫doReleaseShared繼續喚醒後面的結點
return true;
}
return false; //其他情況false
//不過這裡countdown並沒有用到這些返回值
}
private void doReleaseShared() {
for (;;) { //無限迴圈
Node h = head; //獲取頭結點
if (h != null && h != tail) { //如果頭結點不為空且頭結點不是尾結點,那麼說明等待佇列中存在節點
int ws = h.waitStatus; //取一下頭結點的等待狀態
if (ws == Node.SIGNAL) { //如果是SIGNAL,那麼就CAS將頭結點的狀態設定為初始值
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; //失敗就開下一輪迴圈重來
unparkSuccessor(h); //和獨佔模式一樣,當鎖被釋放,都會喚醒頭結點的後繼節點,doAcquireShared迴圈繼續,如果成功,那麼根據setHeadAndPropagate,又會繼續呼叫當前方法,不斷地傳播下去,讓後面的執行緒一個一個地獲取到共享鎖,直到不能再繼續獲取為止
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //如果等待狀態是預設值0,那麼說明後繼節點已經被喚醒,直接將狀態設定為PROPAGATE,它代表在後續獲取資源的時候,夠向後面傳播
continue; //失敗就開下一輪迴圈重來
}
if (h == head) // 如果頭結點發生了變化,不會break,而是繼續迴圈,否則直接break退出
break;
}
}
可能看完之後還是有點亂,我們再來理一下:
- 共享鎖是執行緒共享的,同一時刻能有多個執行緒擁有共享鎖。
- 如果一個執行緒剛獲取了共享鎖,那麼在其之後等待的執行緒也很有可能能夠獲取到鎖,所以得傳播下去繼續嘗試喚醒後面的結點,不像獨佔鎖,獨佔的壓根不需要考慮這些。
- 如果一個執行緒剛釋放了鎖,不管是獨佔鎖還是共享鎖,都需要喚醒後續等待結點的執行緒。
回到CountDownLatch,再結合整個AQS共享鎖的實現機制,進行一次完整的推導,看明白還是比較簡單的。
迴圈屏障 CyclicBarrier
好比一場遊戲,我們必須等待房間內人數足夠之後才能開始,並且遊戲開始之後玩家需要同時進入遊戲以保證公平性。
假如現在遊戲房間內一共5人,但是遊戲開始需要10人,所以我們必須等待剩下5人到來之後才能開始遊戲,並且保證遊戲開始時所有玩家都是同時進入,那麼怎麼實現這個功能呢?
我們可以使用CyclicBarrier,翻譯過來就是迴圈屏障,那麼這個屏障正式為了解決這個問題而出現的。
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(10, //建立一個初始值為10的迴圈屏障
() -> System.out.println("飛機馬上就要起飛了,各位特種兵請準備!")); //人等夠之後執行的任務
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(() -> {
try {
Thread.sleep((long) (2000 * new Random().nextDouble()));
System.out.println("玩家 "+ finalI +" 進入房間進行等待... ("+barrier.getNumberWaiting()+"/10)");
barrier.await(); //呼叫await方法進行等待,直到等待的執行緒足夠多為止
//開始遊戲,所有玩家一起進入遊戲
System.out.println("玩家 "+ finalI +" 進入遊戲!");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
可以看到,迴圈屏障會不斷阻擋執行緒,直到被阻擋的執行緒足夠多時,才能一起衝破屏障,並且在衝破屏障時,我們也可以做一些其他的任務。這和人多力量大的道理是差不多的,當人足夠多時方能衝破阻礙,到達美好的明天。當然,屏障由於是可迴圈的,所以它在被衝破後,會重新開始計數,繼續阻擋後續的執行緒:
public static void main(String[] args) {
CyclicBarrier barrier = new CyclicBarrier(5); //建立一個初始值為5的迴圈屏障
for (int i = 0; i < 10; i++) { //建立5個執行緒
int finalI = i;
new Thread(() -> {
try {
Thread.sleep((long) (2000 * new Random().nextDouble()));
System.out.println("玩家 "+ finalI +" 進入房間進行等待... ("+barrier.getNumberWaiting()+"/5)");
barrier.await(); //呼叫await方法進行等待,直到等待執行緒到達5才會一起繼續執行
//人數到齊之後,可以開始遊戲了
System.out.println("玩家 "+ finalI +" 進入遊戲!");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
可以看到,通過使用迴圈屏障,我們可以對執行緒進行一波一波地放行,每一波都放行5個執行緒,當然除了自動重置之外,我們也可以呼叫reset()
方法來手動進行重置操作,同樣會重新計數:
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(5); //建立一個初始值為10的計數器鎖
for (int i = 0; i < 3; i++)
new Thread(() -> {
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
Thread.sleep(500); //等一下上面的執行緒開始執行
System.out.println("當前屏障前的等待執行緒數:"+barrier.getNumberWaiting());
barrier.reset();
System.out.println("重置後屏障前的等待執行緒數:"+barrier.getNumberWaiting());
}
可以看到,在呼叫reset()
之後,處於等待狀態下的執行緒,全部被中斷並且丟擲BrokenBarrierException異常,迴圈屏障等待執行緒數歸零。
那麼要是處於等待狀態下的執行緒被中斷了呢?
屏障的執行緒等待數量會不會自動減少?
public static void main(String[] args) throws InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(10);
Runnable r = () -> {
try {
barrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
};
Thread t = new Thread(r);
t.start();
t.interrupt();
new Thread(r).start();
}
可以看到,當await()
狀態下的執行緒被中斷,那麼屏障會直接變成損壞狀態,一旦屏障損壞,那麼這一輪就無法再做任何等待操作了。
也就是說,本來大家計劃一起合力衝破屏障,結果有一個人擺爛中途退出了,那麼所有人的努力都前功盡棄,這一輪的屏障也不可能再被衝破了(所以CyclicBarrier告訴我們,不要做那個害群之馬,要相信你的團隊,不然沒有好果汁吃),只能進行reset()
重置操作進行重置才能恢復正常。
乍一看,怎麼感覺和之前講的CountDownLatch有點像,好了,這裡就得區分一下了,千萬別搞混:
- CountDownLatch:
- 它只能使用一次,是一個一次性的工具
- 它是一個或多個執行緒用於等待其他執行緒完成的同步工具
- CyclicBarrier
- 它可以反覆使用,允許自動或手動重置計數
- 它是讓一定數量的執行緒在同一時間開始執行的同步工具
我們接著來看迴圈屏障的實現細節:
public class CyclicBarrier {
//內部類,存放broken標記,表示屏障是否損壞,損壞的屏障是無法正常工作的
private static class Generation {
boolean broken = false;
}
/** 內部維護一個可重入鎖 */
private final ReentrantLock lock = new ReentrantLock();
/** 再維護一個Condition */
private final Condition trip = lock.newCondition();
/** 這個就是屏障的最大阻擋容量,就是構造方法傳入的初始值 */
private final int parties;
/* 在屏障破裂時做的事情 */
private final Runnable barrierCommand;
/** 當前這一輪的Generation物件,每一輪都有一個新的,用於儲存broken標記 */
private Generation generation = new Generation();
//預設為最大阻擋容量,每來一個執行緒-1,和CountDownLatch挺像,當屏障破裂或是被重置時,都會將其重置為最大阻擋容量
private int count;
//構造方法
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
//開啟下一輪屏障,一般屏障被衝破之後,就自動重置了,進入到下一輪
private void nextGeneration() {
// 喚醒所有等待狀態的執行緒
trip.signalAll();
// 重置count的值
count = parties;
//建立新的Generation物件
generation = new Generation();
}
//破壞當前屏障,變為損壞狀態,之後就不能再使用了,除非重置
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
//開始等待
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // 因為這裡沒有使用定時機制,不可能發生異常,如果發生怕是出了錯誤
}
}
//可超時的等待
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
//這裡就是真正的等待流程了,讓我們細細道來
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock(); //加鎖,注意,因為多個執行緒都會呼叫await方法,因此只有一個執行緒能進,其他都被卡著了
try {
final Generation g = generation; //獲取當前這一輪屏障的Generation物件
if (g.broken)
throw new BrokenBarrierException(); //如果這一輪屏障已經損壞,那就沒辦法使用了
if (Thread.interrupted()) { //如果當前等待狀態的執行緒被中斷,那麼會直接破壞掉屏障,並丟擲中斷異常(破壞屏障的第1種情況)
breakBarrier();
throw new InterruptedException();
}
int index = --count; //如果上面都沒有出現不正常,那麼就走正常流程,首先count自減並賦值給index,index表示當前是等待的第幾個執行緒
if (index == 0) { // 如果自減之後就是0了,那麼說明來的執行緒已經足夠,可以衝破屏障了
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run(); //執行衝破屏障後的任務,如果這裡拋異常了,那麼會進finally
ranAction = true;
nextGeneration(); //一切正常,開啟下一輪屏障(方法進入之後會喚醒所有等待的執行緒,這樣所有的執行緒都可以同時繼續執行了)然後返回0,注意最下面finally中會解鎖,不然其他執行緒喚醒了也拿不到鎖啊
return 0;
} finally {
if (!ranAction) //如果是上面出現異常進來的,那麼也會直接破壞屏障(破壞屏障的第2種情況)
breakBarrier();
}
}
// 能走到這裡,那麼說明當前等待的執行緒數還不夠多,不足以衝破屏障
for (;;) { //無限迴圈,一直等,等到能衝破屏障或是出現異常為止
try {
if (!timed)
trip.await(); //如果不是定時的,那麼就直接永久等待
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos); //否則最多等一段時間
} catch (InterruptedException ie) { //等的時候會判斷是否被中斷(依然是破壞屏障的第1種情況)
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
Thread.currentThread().interrupt();
}
}
if (g.broken)
throw new BrokenBarrierException(); //如果執行緒被喚醒之後發現屏障已經被破壞,那麼直接拋異常
if (g != generation) //成功衝破屏障開啟下一輪,那麼直接返回當前是第幾個等待的執行緒。
return index;
if (timed && nanos <= 0L) { //執行緒等待超時,也會破壞屏障(破壞屏障的第3種情況)然後拋異常
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock(); //最後別忘了解鎖,不然其他執行緒拿不到鎖
}
}
//不多說了
public int getParties() {
return parties;
}
//判斷是否被破壞,也是加鎖訪問,因為有可能這時有其他執行緒正在執行dowait
public boolean isBroken() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return generation.broken;
} finally {
lock.unlock();
}
}
//重置操作,也要加鎖
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // 先破壞這一輪的執行緒,注意這個方法會先破壞再喚醒所有等待的執行緒,那麼所有等待的執行緒會直接拋BrokenBarrierException異常(詳情請看上方dowait倒數第13行)
nextGeneration(); // 開啟下一輪
} finally {
lock.unlock();
}
}
//獲取等待執行緒數量,也要加鎖
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count; //最大容量 - 當前剩餘容量 = 正在等待執行緒數
} finally {
lock.unlock();
}
}
}
看完了CyclicBarrier的原始碼之後,是不是感覺比CountDownLatch更簡單一些?
訊號量 Semaphore
還記得我們在《作業系統》中學習的訊號量機制嗎?它在解決程式之間的同步問題中起著非常大的作用。
訊號量(Semaphore),有時被稱為訊號燈,
是在多執行緒環境下使用的一種設施,是可以用來保證兩個或多個關鍵程式碼段不被併發呼叫。
在進入一個關鍵程式碼段之前,執行緒必須獲取一個訊號量;
一旦該關鍵程式碼段完成了,那麼該執行緒必須釋放訊號量。其它想進入該關鍵程式碼段的執行緒必須等待直到第一個執行緒釋放訊號量。
通過使用訊號量,我們可以決定某個資源同一時間能夠被訪問的最大執行緒數,它相當於對某個資源的訪問進行了流量控制。
簡單來說,它就是一個可以被N個執行緒佔用的排它鎖(因此也支援公平和非公平模式),我們可以在最開始設定Semaphore的許可證數量,每個執行緒都可以獲得1個或n個許可證,當許可證耗盡或不足以供其他執行緒獲取時,其他執行緒將被阻塞。
public static void main(String[] args) throws ExecutionException, InterruptedException {
//每一個Semaphore都會在一開始獲得指定的許可證數數量,也就是許可證配額
Semaphore semaphore = new Semaphore(2); //許可證配額設定為2
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
semaphore.acquire(); //申請一個許可證
System.out.println("許可證申請成功!");
semaphore.release(); //歸還一個許可證
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//每一個Semaphore都會在一開始獲得指定的許可證數數量,也就是許可證配額
Semaphore semaphore = new Semaphore(3); //許可證配額設定為3
for (int i = 0; i < 2; i++)
new Thread(() -> {
try {
semaphore.acquire(2); //一次性申請兩個許可證
System.out.println("許可證申請成功!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
我們也可以通過Semaphore獲取一些常規資訊:
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(3); //只配置一個許可證,5個執行緒進行爭搶,不內卷還想要許可證?
for (int i = 0; i < 5; i++)
new Thread(semaphore::acquireUninterruptibly).start(); //可以以不響應中斷(主要是能簡寫一行,方便)
Thread.sleep(500);
System.out.println("剩餘許可證數量:"+semaphore.availablePermits());
System.out.println("是否存線上程等待許可證:"+(semaphore.hasQueuedThreads() ? "是" : "否"));
System.out.println("等待許可證執行緒數量:"+semaphore.getQueueLength());
}
我們可以手動回收掉所有的許可證:
public static void main(String[] args) throws InterruptedException {
Semaphore semaphore = new Semaphore(3);
new Thread(semaphore::acquireUninterruptibly).start();
Thread.sleep(500);
System.out.println("收回剩餘許可數量:"+semaphore.drainPermits()); //直接回收掉剩餘的許可證
}
這裡我們模擬一下,比如現在有10個執行緒同時進行任務,任務要求是執行某個方法,但是這個方法最多同時只能由5個執行緒執行,這裡我們使用訊號量就非常合適。
資料交換 Exchanger
執行緒之間的資料傳遞也可以這麼簡單。
使用Exchanger,它能夠實現執行緒之間的資料交換:
public static void main(String[] args) throws InterruptedException {
Exchanger<String> exchanger = new Exchanger<>();
new Thread(() -> {
try {
System.out.println("收到主執行緒傳遞的交換資料:"+exchanger.exchange("AAAA"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
System.out.println("收到子執行緒傳遞的交換資料:"+exchanger.exchange("BBBB"));
}
在呼叫exchange
方法後,當前執行緒會等待其他執行緒呼叫同一個exchanger物件的exchange
方法,當另一個執行緒也呼叫之後,方法會返回對方執行緒傳入的引數。
Fork/Join框架
在JDK7時,出現了一個新的框架用於並行執行任務,它的目的是為了把大型任務拆分為多個小任務,最後彙總多個小任務的結果,得到整大任務的結果,並且這些小任務都是同時在進行,大大提高運算效率。Fork就是拆分,Join就是合併。
我們來演示一下實際的情況,比如一個算式:18x7+36x8+9x77+8x53,可以拆分為四個小任務:18x7、36x8、9x77、8x53,最後我們只需要將這四個任務的結果加起來,就是我們原本算式的結果了。
它不僅僅只是拆分任務並使用多執行緒,而且還可以利用工作竊取演算法,提高執行緒的利用率。
工作竊取演算法:是指某個執行緒從其他佇列裡竊取任務來執行。
一個大任務分割為若干個互不依賴的子任務,為了減少執行緒間的競爭,把這些子任務分別放到不同的佇列裡,併為每個佇列建立一個單獨的執行緒來執行佇列裡的任務,執行緒和佇列一一對應。
但是有的執行緒會先把自己佇列裡的任務幹完,而其他執行緒對應的佇列裡還有任務待處理。幹完活的執行緒與其等著,不如幫其他執行緒幹活,於是它就去其他執行緒的佇列裡竊取一個任務來執行。
現在我們來看看如何使用它,這裡以計算1-1000的和為例,我們可以將其拆分為8個小段的數相加,比如1-125、126-250... ,最後再彙總即可,它也是依靠執行緒池來實現的:
public class Main {
public static void main(String[] args) throws InterruptedException, ExecutionException {
ForkJoinPool pool = new ForkJoinPool();
System.out.println(pool.submit(new SubTask(1, 1000)).get());
}
//繼承RecursiveTask,這樣才可以作為一個任務,泛型就是計算結果型別
private static class SubTask extends RecursiveTask<Integer> {
private final int start; //比如我們要計算一個範圍內所有數的和,那麼就需要限定一下範圍,這裡用了兩個int存放
private final int end;
public SubTask(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
if(end - start > 125) { //每個任務最多計算125個數的和,如果大於繼續拆分,小於就可以開始算了
SubTask subTask1 = new SubTask(start, (end + start) / 2);
subTask1.fork(); //會繼續劃分子任務執行
SubTask subTask2 = new SubTask((end + start) / 2 + 1, end);
subTask2.fork(); //會繼續劃分子任務執行
return subTask1.join() + subTask2.join(); //越玩越有遞迴那味了
} else {
System.out.println(Thread.currentThread().getName()+" 開始計算 "+start+"-"+end+" 的值!");
int res = 0;
for (int i = start; i <= end; i++) {
res += i;
}
return res; //返回的結果會作為join的結果
}
}
}
}
ForkJoinPool-1-worker-2 開始計算 1-125 的值!
ForkJoinPool-1-worker-2 開始計算 126-250 的值!
ForkJoinPool-1-worker-0 開始計算 376-500 的值!
ForkJoinPool-1-worker-6 開始計算 751-875 的值!
ForkJoinPool-1-worker-3 開始計算 626-750 的值!
ForkJoinPool-1-worker-5 開始計算 501-625 的值!
ForkJoinPool-1-worker-4 開始計算 251-375 的值!
ForkJoinPool-1-worker-7 開始計算 876-1000 的值!
500500
可以看到,結果非常正確,但是整個計算任務實際上是拆分為了8個子任務同時完成的,結合多執行緒,原本的單執行緒任務,在多執行緒的加持下速度成倍提升。
包括Arrays工具類提供的並行排序也是利用了ForkJoinPool來實現:
public static void parallelSort(byte[] a) {
int n = a.length, p, g;
if (n <= MIN_ARRAY_SORT_GRAN ||
(p = ForkJoinPool.getCommonPoolParallelism()) == 1)
DualPivotQuicksort.sort(a, 0, n - 1);
else
new ArraysParallelSortHelpers.FJByte.Sorter
(null, a, new byte[n], 0, n, 0,
((g = n / (p << 2)) <= MIN_ARRAY_SORT_GRAN) ?
MIN_ARRAY_SORT_GRAN : g).invoke();
}
並行排序的效能在多核心CPU環境下,肯定是優於普通排序的,並且排序規模越大優勢越顯著。