本着重新學習(看到什麼複習什麼)的原則,這一篇講的是JAVA的JUC。看了諸位大神的解釋後詳細的查了一些東西,記錄下來,也感謝各位在網絡上的分享!!!
blog推薦:
https://blog.csdn.net/weixin_44460333/article/details/86770169
https://www.cnblogs.com/aobing/p/12057282.html
1.什麼是JUC:
在JAVA1.5之後,提供了java.util.concurrent包(也成爲JUC)去處理多線程間併發編程問題,很多的工具包的出現讓使用者能夠在使用多線程編程時能夠更有效,也更快捷。JUC內提供的工具類可以分爲以下幾類:
(1).locks:顯式鎖(互斥鎖和讀寫鎖)(AbstractQueuedSynchronizer,ReentantLock,ReentrantReadWriteLock)
(2).atomic:原子變量(AtomicBoolean,AtomicInteger,AtomicReference)
(3).executors:線程池(ThreadPoolExecutor,ForkJoinPool)
(4).collections:併發容器(ConcurrentHashMap,CopyOnWriteArrayList)
(5).tools:同步工具,信號量(Semaphore),閉鎖(CountDownLatch),柵欄(CyclicBarrier)
2.什麼是原子變量:
原子變量,即所有相關操作都應是原子操作。也就是說,對於原子變量的操作都是一步原子操作完成的。我們在進行多線程編程時會經常遇到多線程訪問共享數據問題,而對於該問題的解決辦法也有很多,如volatile關鍵字與synchronized關鍵字。volatile關鍵字是保證了在多線程訪問共享數據時,能夠在主存中數據可見。這是因爲,原本每個線程會在使用變量時會在線程內存中創建一個該變量的拷貝,而在某線程修改數據過程中,這個修改操作是多線程間彼此不可見的,所以可能會發生主存中的數據仍未發生改變時被其他線程獲取到,也就會發生線程安全的問題。synchronized關鍵字則能保證某數據在被多線程中某一線程修改時,不會再被其他線程獲取到,也就是加鎖。但是兩者均有其不足。使用synchronized關鍵字去保證數據同步的方式實際上就是使用同步鎖機制,需要對鎖和線程的狀態進行判斷(如:該鎖是否被獲取,是否被釋放,線程是否處於阻塞狀態,是否再次處於就緒狀態等),這樣的判斷操作效率很低。而使用volatile關鍵字雖然使用上降低了判斷各種狀態的內存消耗,但是會在某些場景下不能保持變量的原子性。(重排序)
volatile和synchronized的區別:
(1).volatile關鍵字只是保證了所有數據在讀取變量時都是從主存中讀取,synchronized關鍵字則是鎖定該變量,即保證在單一時間只有一個線程訪問該變量,其餘線程處於阻塞狀態。也就是說volatile關鍵字不具備互斥性。
(2).volatile關鍵字只能作用於變量之上,而synchronized關鍵字則能使用在變量,方法或類上
(3).volatile關鍵字只能保證變量的內存可見性,但是不能保證變量的原子性,而synchronized關鍵字能保證變量的內存可見性和原子性。針對原子性的理解又是最經典的i++操作,在內存中i++操作需要三步,即獲取,更改,賦值,而這三步順序執行纔會保證i++操作的正常執行。
package com.day_7.excercise_1;
public class TryVolatile1 {
public static void main(String[] args) {
VolatileClass volatileClass = new VolatileClass();
new Thread(volatileClass).start();
while (true) {
// 2.
// synchronized (volatileClass) {
// if (volatileClass.isFlag()) {
// System.out.println("===============");
// break;
// }
// }
// 1.A
if (volatileClass.isFlag()) {
System.out.println("===============");
break;
}
}
}
}
class VolatileClass implements Runnable{
// 3.
private volatile boolean flag = false;
// 1.
// private boolean flag = false;
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("flag:"+flag);
}
public boolean isFlag() {
return flag;
}
public void setFlag(boolean flag) {
this.flag = flag;
}
}
由上可見,在使用場景1時,由於線程間的操作彼此不可見,所以雖然在VolatileClass創建的線程中對flag的值進行了修改,但是主線程中獲取到的仍是主存中的flag的拷貝,所以無法正常退出循環。而在使用場景2或者場景3和場景1.A時,可以看到循環正常退出了,也就是說volatile關鍵字和synchronized關鍵字都能保證數據的內存可見性。
package com.day_7.excercise_1;
import java.util.concurrent.atomic.AtomicInteger;
public class TryAtomic1 {
public static void main(String[] args) {
AtomicDemo atomicDemo = new AtomicDemo();
for (int i = 0; i < 10; i++) {
new Thread(atomicDemo).start();
}
}
}
class AtomicDemo implements Runnable{
// 1.
// private volatile int i = 0;
// 2.
private AtomicInteger i = new AtomicInteger();
public int getI() {
// 1.
// return i++;
// 2.
return i.getAndIncrement();
}
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":"+getI());
}
}
由上可見,在使用場景1時,針對i++的操作結果並不能保證每一次獲取到的數值都是不同的。所以volatile關鍵字不能保證原子變量的原子性,所以我們可以使用JUC中提供的Atomic類進行操作。在使用場景2時,可見操作每次獲取到的值都是不同的,不重複也證明了其能夠保證原子性。
Atomic相關:
在JUC中提供了諸如AtomicBoolean,AtomicInteger,AtomicReference等數據類型類,在其中封裝的值都是使用volatile關鍵字修飾,也就保證了內存可見性,並且其中使用了CAS(Compare-And-Swap)算法保證了數據的原子性。
CAS:
CAS算法是在操作系統層面提供了對於併發共享數據訪問的支持,其中包含三個操作數(內存值V,預估值A,更新值B),當且僅當V=A時才把B的值賦予V,否則將不進行操作。CAS比普通鎖效率高的原因是因爲使用CAS算法獲取不成功時,不會阻塞線程,而是可以再次進行嘗試更新。缺點在於會一直循環嘗試。
例如:上圖中線程1和線程2同時訪問內存中的i變量,線程1獲取(i = 0)即V=0,並且此時A=0,所以可以進行賦值操作,即將B賦值給V,而同一時間線程2訪問獲取i的值時,可能在獲取V的值時V=0(i=0),但是在再次獲取A的值時,已經被線程1進行了修改,那麼此時A=1,由CAS算法可知,V!=A,所以不進行任何操作,也就不進行改變。下面是一個簡單的cas實現:
package com.day_7.excercise_1;
public class TryCAS1 {
public static void main(String[] args) {
final CompareAndSwap cas = new CompareAndSwap();
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
@Override
public void run() {
int expectValue = cas.get();
boolean b = cas.compareAndSet(expectValue, (int)Math.random()*101);
System.out.println(b);
}
}).start();
}
}
}
class CompareAndSwap {
private int value;
// 獲取內存值
public synchronized int get() {
return value;
}
// 比較
public synchronized int compareAndSwap(int expectValue, int newValue) {
int oldValue = value;
if (oldValue == expectValue) {
this.value = newValue;
}
return oldValue;
}
// 設置
public synchronized boolean compareAndSet(int expectValue, int newValue) {
return expectValue == compareAndSwap(expectValue, newValue);
}
}
3.什麼是併發容器:
在JUC中也提供了很多保證線程安全的併發容器,用於處理單純的容器類在處理多線程併發操作時的問題。如在使用本就不能保證線程安全的HashMap進行put操作時會出現的數據丟失或者數據覆蓋的問題甚至有可能會導致死循環,使用了synchronized關鍵字的HashTable雖然保證了線程安全,但是其效率也正是由於使用synchronized關鍵字,故而形成了線程互斥,數據只能單一線程持有修改,而其他線程只能進行阻塞或者輪詢,效率較低。所以在JUC中出現了ConcurrentHashMap類。
package com.day_7.excercise_1;
import java.util.HashMap;
import java.util.Map;
public class TryHashMapDieLoop {
public static void main(String[] args) {
HashMapThread thread0 = new HashMapThread();
HashMapThread thread1 = new HashMapThread();
HashMapThread thread2 = new HashMapThread();
HashMapThread thread3 = new HashMapThread();
HashMapThread thread4 = new HashMapThread();
thread0.start();
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
class HashMapThread extends Thread {
private static int intNum = 0;
private static Map<Integer, Integer> map = new HashMap<>();
@Override
public void run() {
while (intNum < 1000000) {
map.put(intNum, intNum);
System.out.println(map.size());
intNum++;
}
}
}
在JDK1.7時,ConcurrentHashMap使用的還是Segment分段鎖和ReentrantLock鎖機制,而在JDK1.8便修改爲使用CAS和synchronized關鍵字來保證其併發安全性了。由此可見,現在synchronized關鍵字在使用時安全性更高一些,並且提高了查詢遍歷鏈表的效率。
此外在JUC中還提供了CopyOnWriteArrayList/CopyOnWriteArraySet(寫入並複製),在添加操作多的場景下效率較低,因爲每次添加時都會進行復制,開銷很大。併發迭代操作多時可選擇。
4.什麼是閉鎖:
CountDownLatch(閉鎖):是一個同步輔助類,在完成一組正在其他線程中執行的操作之前,它允許一個或多個線程一直等待。每當一個線程完成其操作,計數器就會減1,當計數器歸零時表示所有線程均已完成任務,而後在閉鎖上等待的線程可以繼續執行。其作用或者說是應用場景就在於:
(1).閉鎖可以延遲線程的監督直到其到達終止狀態,可以用來確保某些活動直到其他活動都完成才繼續執行;
(2).確保某個計算在其需要的所有資源都被初始化之後才繼續執行;
(3).確保某個服務在其依賴的所有其他服務都已經啓動之後才啓動;
(4).等待直到某個操作所有參與者都準備就緒再繼續執行。
package com.day_7.excercise_1;
import java.util.concurrent.CountDownLatch;
public class TryCountDownLatch {
public static void main(String[] args) {
// 相當於是維護一個閾值,在閾值遞減直至爲0時會繼續進行接下來的操作
final CountDownLatch countDownLatch = new CountDownLatch(5);
CountDownLatchClass countDownLatchClass = new CountDownLatchClass(countDownLatch);
long start = System.currentTimeMillis();
for (int i = 0; i < 5; i++) {
new Thread(countDownLatchClass).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("耗費時間爲:"+(end-start));
}
}
class CountDownLatchClass implements Runnable{
private CountDownLatch countDownLatch;
public CountDownLatchClass(CountDownLatch countDownLatch) {
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
synchronized (this) {
try {
for (int i = 0; i < 50000; i++) {
if(i%2==0) {
System.out.println(i);
}
}
} catch (Exception e) {
e.printStackTrace();
}finally {
// 閉鎖閾值遞減1
countDownLatch.countDown();
}
}
}
}
如上所示,在使用主線程進行時間計算時,實質上是要等待所有其他線程完成後,才能得到本次程序運行的總時長,也就說明在其他線程做完之前,主線程應該處於等待狀態。這便是閉鎖的最佳使用場景,所以我們在使用CountDownLatch創建一個對象時,可以賦值其需要維護的計數器的閾值,並在閾值遞減至0時,喚醒主線程。這個示例的場景可以拓展到很多場景,如我們在計算某一個具體操作之前,需要將其相關的所有操作全部執行成功,才能執行最終公式,或者我們在進行前後端交互時,我們需要對所有的數據庫信息進行整合後,對這個結果集進行其他操作時,都可以並行的使用多線程,而後通過閉鎖將某個關鍵線程等待,在適當時間喚醒。
5.什麼是等待喚醒機制:
等待喚醒機制最常見的示例就是生產者/消費者問題,當生產者不斷地生成數據,而消費者已經沒有能力接受數據時,就會出現數據丟失的問題;當消費者不斷的接收數據,而生產者已經不再發送數據時,就會出現重複數據的問題。並且當消費者不能獲取到數據時,若不使用等待喚醒機制,則需要使用while(false)(false表示條件不滿足)的無限循環進行輪詢,同理,當生產者生產的數據不能加入到消費者的隊列中,也就是說消費者隊列是滿足狀態時,生產者也會使用類似的無限循環進行輪詢。這些輪詢的過程都是非常消耗CPU資源的,但是我們可以模擬一個場景,我們在食堂點餐後,等餐的時間中實際上不需要無限的詢問,而是等待被叫號也好被告知已經做好了也好,這樣我們在等待的過程中不是忙碌狀態,也就可以有空閒的資源去做更多的事情,CPU也是同理。
與synchronized關鍵字相關聯的等待喚醒機制方法是wait(),notify(),notifyAll()。則在使用這三個方法時必須在synchronized關鍵字的代碼塊或者方法中,並且由於可能存在虛假喚醒的問題,所以應該使用在while循環中。現假設我們有多個線程和一個生產者消費者隊列,虛假喚醒的例子是這樣的:
(1).在隊列中存在一個任務1,線程1從隊列中獲取到這個任務,此時隊列爲空
(2).線程2通過判斷隊列中任務個數可知當前隊列爲空,不存在任務,所以線程2阻塞,等待隊列非空
(3).隊列中爲空,所以生產者又生產了一個任務2,並且使用了notify進行喚醒
(4).處於等待狀態的線程2可以獲取任務,但是,與此同時,剛剛獲取任務1的線程1可能已經做完任務1,此時也可以獲取任務,則某一個線程獲取任務並完成任務後,隊列中又爲空,此時等待狀態的就變成了線程1和線程2,並且會在下一次獲取到任務後,同樣兩者均等待。
在上述示例中,能夠在(4)操作中被喚醒後獲取到任務的就是正常執行,而另一個就是虛假喚醒。
package com.day_7.excercise_1;
public class TryComsumerProductor1 {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Comsumer comsumer = new Comsumer(clerk);
new Thread(productor,"生產者A").start();
new Thread(comsumer,"消費者B").start();
new Thread(productor,"生產者C").start();
new Thread(comsumer,"消費者D").start();
}
}
class Clerk{
private int product = 0;
public synchronized void get() {
// if (product>=10) {
// A
// if (product>=1) {
// B
while(product>=10) {
System.out.println("產品已滿");
try {
// 當不能正確接收產品時,就等待
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
// else {
System.out.println(Thread.currentThread().getName()+":"+(++product));
// 當已經有產品被成功生產,就通知所有線程
this.notifyAll();
// }
}
public synchronized void sale() {
// if (product<=0) {
// B
while(product<=0) {
System.out.println("產品已缺貨");
try {
this.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
// else {
System.out.println(Thread.currentThread().getName()+":"+(--product));
this.notifyAll();
// }
}
}
class Productor implements Runnable{
private Clerk clerk;
public Productor(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
// A
try {
Thread.sleep(200);
} catch (Exception e) {
e.printStackTrace();
}
clerk.get();
}
}
}
class Comsumer implements Runnable{
private Clerk clerk;
public Comsumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk.sale();
}
}
}
如上可以看到,定義了Comsumer和Productor,並且在使用if判斷時,會出現虛假喚醒的情況。而在使用while循環時則沒有這種問題出現。在JUC則使用Lock和Condition來實現等待喚醒機制。對應的方法爲wait(),signal(),signalAll()。
package com.day_7.excercise_1;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TryComsumerProductor2 {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Productor productor = new Productor(clerk);
Comsumer comsumer = new Comsumer(clerk);
new Thread(productor, "生產者A").start();
new Thread(comsumer, "消費者B").start();
new Thread(productor, "生產者C").start();
new Thread(comsumer, "消費者D").start();
}
}
class Clerk1 {
private int product = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void get() {
lock.lock();
try {
while (product >= 10) {
System.out.println("產品已滿");
try {
// 當不能正確接收產品時,就等待
condition.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + (++product));
// 當已經有產品被成功生產,就通知所有線程
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void sale() {
lock.lock();
try {
while (product <= 0) {
System.out.println("產品已缺貨");
try {
condition.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ":" + (--product));
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
class Productor1 implements Runnable {
private Clerk1 clerk1;
public Productor1(Clerk1 clerk1) {
this.clerk1 = clerk1;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
// A
// try {
// Thread.sleep(200);
// } catch (Exception e) {
// e.printStackTrace();
// }
clerk1.get();
}
}
}
class Comsumer1 implements Runnable {
private Clerk1 clerk1;
public Comsumer1(Clerk1 clerk1) {
this.clerk1 = clerk1;
}
@Override
public void run() {
for (int i = 0; i < 20; i++) {
clerk1.sale();
}
}
}
6.什麼是Condition:
Condition接口提供了類似於Object的監視器方法,可以與Lock配合使用(或者可以說condition對象是依賴於Lock對象的,創建方法就是Lock對象.newCondition()),實現等待喚醒機制。在AQS中有內部類ConditionObject實現了Condition接口,每個Condition對象中維護了一個FIFO的等待隊列。
7.什麼是線程池:
多線程執行能夠最大限度的提高程序運行速度,但是與數據庫也擁有連接池一樣,對於創建一個連接或者線程,再經過使用後將其銷燬的過程也是非常耗費CPU資源的。所以我們可以使用線程池來避免某些問題的出現(爲什麼有線程還要有線程池?):
(1).避免了線程的創建和銷燬帶來的性能消耗:這就避免了重複的創建和銷燬的過程,也就使得CPU資源的消耗降低了。因爲在線程池中的線程可以重複使用。
(2).避免大量線程因爲互相搶佔系統資源而導致的阻塞現象:這就避免了由於不斷創建線程,但是可能獲取資源的線程只有一個的可能性下,會導致的多線程阻塞狀態。
(3).能夠對線程進行簡單的管理並提供定時執行,間隔執行等功能
此外,要明確的是,因爲多線程中每一個線程同樣都是在使用CPU資源,所以線程並不是越多越好。效率上也有着區別。
(1).不使用線程池
package com.day_7.excercise_1;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class TryThreadNoPool {
public static void main(String[] args) throws Exception {
Long start = System.currentTimeMillis();
final List<Integer> list = new ArrayList<Integer>();
final Random random = new Random();
for(int i = 0;i<10000;i++) {
Thread thread = new Thread() {
public void run() {
list.add(random.nextInt());
}
};
thread.start();
// 主線程等待一起完成
thread.join();
}
System.out.println("總耗時:"+(System.currentTimeMillis()-start));
System.out.println("總大小:"+list.size());
}
}
(2).使用線程池
package com.day_7.excercise_1;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class TryThreadWithPool {
public static void main(String[] args) throws Exception {
Long start = System.currentTimeMillis();
final List<Integer> list = new ArrayList<Integer>();
ExecutorService executorService = Executors.newSingleThreadExecutor();
final Random random = new Random();
for(int i = 0;i<10000;i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
list.add(random.nextInt());
}
});
}
executorService.shutdown();
executorService.awaitTermination(1, TimeUnit.DAYS);
System.out.println("總耗時:"+(System.currentTimeMillis()-start));
System.out.println("總大小:"+list.size());
}
}
在線程池的創建上,Executors工具類中提供了很多的方法,如下:
方法名 | 功能 | 備註 | 適用場景 |
---|---|---|---|
newFixedThreadPool | 固定大小的線程池 | 在線程池中一旦有線程處理完手中的任務就會處理新的任務 | 適用於負載較重,需要滿足資源管理限制線程數量的場景 |
newSingleThreadExecutor | 只有一個線程的線程池 | 只會創建一個線程,會按照任務提交入隊列順序執行 | 適用於需要保證任務順序執行的場景 |
newCachedThreadPool | 不限線程數上限的線程池 | 首先會創建足夠多的線程,線程在程序運行過程中可以循環使用,僅在可以循環使用線程執行任務時,纔不創建新的線程 | 適用於負載較輕,需要執行很多短期異步任務的場景 |
...... | ...... |
package com.day_7.excercise_1;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TryExecutor1 {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.submit(()->System.out.println("線程池"));
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println("線程池");
}
});
executorService.submit(new Runnable() {
@Override
public void run() {
System.out.println("線程池");
}
});
executorService.shutdown();
}
}
我們來看一下ThreadPoolExecutor的繼承和實現流程,還有調用的部分過程。
ThreadPoolExecutor—>AbstractExecutorService(接口實現類)—>ExecutorService(接口提交)—>Executor(執行接口)
public interface Executor {
void execute(Runnable command);
}
public interface ExecutorService extends Executor {
......
}
public abstract class AbstractExecutorService implements ExecutorService {
......
// 方法來自ExecutorService接口
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
// 執行來自Executor接口
execute(ftask);
return ftask;
}
......
}
public class ThreadPoolExecutor extends AbstractExecutorService {
......
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}
......
}
在ThreadPoolExecutor的構造方法中提供了很多參數來配置線程池:
(1).corePoolSize:(the number of threads to keep in the pool, even if they are idle, unless {@code allowCoreThreadTimeOut} is set)線程池內被一直保持存活狀態的的核心線程數量。這些線程即使是閒置的,也會存活在線程池彙總,除非設置allowCoreThreadTimeOut參數。
(2).maximumPoolSize:(maximumPoolSize the maximum number of threads to allow in the pool)線程池內最大線程數量。
(3).keepAliveTime:(when the number of threads is greater than the core, this is the maximum time that excess idle threads will wait for new tasks before terminating.)非核心線程的最大閒置時長,超過這個時長便會被回收。
(4).unit:(the time unit for the {@code keepAliveTime} argument)用於指定keepAliveTime參數的時間單位,通過TimeUnit枚舉類來確定該單位。
(5).workQueue:(the queue to use for holding tasks before they are executed. This queue will hold only the {@code Runnable} tasks submitted by the {@code execute} method.)用於存儲任務的任務隊列。只用來接收創建的Runnable線程對象,因爲泛型類型已經被指定(BlockingQueue<Runnable> workQueue)。
(6).threadFactory:(the factory to use when the executor creates a new thread)線爲線程池創建新線程的線程工廠,該接口中只有一個創建線程的方法(Thread newThread(Runnable r))。
(7).RejectedExecutionHandler:(the handler to use when execution is blocked because the thread bounds and queue capacities are reached)當該ThreadPoolExecutor已經被關閉或者線程池內的任務隊列已經飽和時的通知策略。
addWorker方法意在檢查能否在當前池內狀態和綁定核心線程和最大線程的數量情況下添加新的線程進行工作。如果可能,則創建並啓動一個新的工作線程,並且會在線程創建失敗時回滾。方法參數firstTask爲傳入的任務,core爲是否使用核心線程池作爲綁定或是使用最大線程數量作爲綁定,方法返回布爾值。
所以對於擁有以上7個參數的場景可以簡述爲:
1.首先使用核心線程內的線程;
2.在覈心線程均在工作時,將任務存在等待隊列中;
3.當等待隊列未滿,且核心線程可以滿足任務的情況下,不創建新的線程;
4.當等待隊列已滿,且核心線程不可以滿足任務的情況下,創建新的線程進行任務,直至達到最大線程數量;
5.當等待隊列已滿,且核心線程不可以滿足任務的情況下,並且已經達到最大線程數量時,執行拒絕策略;
1.構造方法:初始化
// 核心線程池內線程數量,最大線程池內數量,保持時長(創建到銷燬的時間),時間單位,工作隊列,線程工廠類,拒絕策略
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
2.submit方法:
// java.util.concurrent.ThreadPoolExecutor.execute(Runnable)
public void execute(Runnable command) {
// 判斷傳入的任務是否爲空
if (command == null)
throw new NullPointerException();
// 原子操作,獲取狀態
int c = ctl.get();
// 判斷當前運行的線程數量<corePoolSize 創建新的線程執行新添加的任務,並返回
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// private final BlockingQueue<Runnable> workQueue;
// 進入該判斷分支說明當前運行線程數量>=corePoolSize ,判斷當前線程池狀態是否正在運行和能否將該任務加入任務隊列等待執行
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
// 再次判斷線程池狀態,若已經不是運行狀態,則應該拒絕新任務,並從隊列中刪除任務(double check)
if (! isRunning(recheck) && remove(command))
reject(command);
// 如果當前線程數量未達到線程最大規定線程數量,則會啓動一個非核心線程執行任務
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 如果仍然無法創建線程持有任務,就拒絕這個任務
else if (!addWorker(command, false))
reject(command);
}
3.addWorker方法:
private boolean addWorker(Runnable firstTask, boolean core) {
retry:
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 判斷能否進行CAS操作,使得worker數量+1
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
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)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
// private final HashSet<Worker> workers = new HashSet<Worker>();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
我們常用的有兩種基礎線程池:ThreadPoolExecutor和ScheduledThreadPoolExecutor,前者是線程池的核心實現類,用來執行被提交的任務,並且由於提供了很多方便調用的線程池創建方法,所以真正創建線程池的構造方法被固定並隱藏,雖說確實可以直接通過構造方法進行線程池的創建,卻不具備後者的延遲執行等功能;後者時候一個支持線程調度的線程池實現類,能夠在給定的延遲後運行命令,或者定時執行。
繼承了AbstractExecutorService的ForkJoinPool是一種特殊的線程池。它支持將一個任務拆分成多個小任務並行計算,而後將多個小任務的結果合成爲總的計算結果。通過調用submit或invoke方法執行額任務,方法接收泛型的ForkJoinTask。ForkJoinTask擁有兩個抽象子類,RecusiveAction和RecusiveTask。前者代表沒有返回值的任務,而後者代表有返回值的任務。ForkJoinTask使用遞歸來實現子任務的方法調用,但是要注意的是,任務拆分機制需要人爲進行干預,也就是說要想得到更優質的執行效果需要多次嘗試。
package com.day_7.excercise_1;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
import java.util.stream.LongStream;
import org.junit.Test;
public class TryForkJoinPool {
public static void main(String[] args) {
Instant start = Instant.now();
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> forkJoinTask = new ForkJoinDemo(0L, 10000000L);
Long sum = pool.invoke(forkJoinTask);
System.out.println(sum);
Instant end = Instant.now();
System.out.println(Duration.between(start, end).toMillis());//783
}
@Test
public void test1(){
Instant start = Instant.now();
Long sum = 0L;
for (Long i = 0L; i <= 1000000L; i++) {
sum+=i;
}
Instant end = Instant.now();
System.out.println(Duration.between(start, end).toMillis());//112
}
//JAVA8新特性
@Test
public void test2(){
Instant start = Instant.now();
Long sum = LongStream.rangeClosed(0L, 10000000L).parallel().reduce(0L, Long::sum);
System.out.println(sum);
Instant end = Instant.now();
System.out.println(Duration.between(start, end).toMillis());//287
}
}
//RecursiveTask有返回值
//RecursiveAction沒有返回值
class ForkJoinDemo extends RecursiveTask<Long>{
/**
* THURSHOLD設置拆分到什麼情況下才不進行拆分,即臨界值
*/
private static final long serialVersionUID = 1L;
private Long start;
private Long end;
// 臨界值
private static final long THURSHOLD = 1000L;
public ForkJoinDemo(Long start, Long end) {
super();
this.start = start;
this.end = end;
}
@Override
protected Long compute() {
long length = end-start;
if(length<=THURSHOLD) {
long sum = 0L;
for (long i = start; i <= end ; i++) {
sum += i;
}
return sum;
}else {
long middle = (start+end)/2;
ForkJoinDemo left = new ForkJoinDemo(start, middle);
// 進行拆分,同時壓入線程隊列
left.fork();
ForkJoinDemo right = new ForkJoinDemo(middle+1, end);
right.fork();
return left.join()+right.join();
}
}
}
8.什麼是Lock:
JAVA併發編程關於鎖的實現方式有兩種:基於synchronized關鍵字的鎖實現(JVM內置鎖,隱式鎖:由JVM自動加鎖解鎖),基於Lock接口的鎖實現(顯式鎖,需要手動加鎖加鎖)。基於Lock接口的實現類包括上一篇中的ReentrantLock可重入鎖,ReadWriteLock讀寫鎖和StampedLock戳鎖等。
讀寫鎖,即多線程都在做同樣的寫操作(寫寫/讀寫)時需要互斥,而讀讀操作不需要互斥。同時維護兩個鎖,讀鎖(多個讀線程併發執行)和寫鎖(寫鎖是單線程獨佔的,同步鎖)。如下,我們可以看到,在使用get方法進行讀操作時,很快就能完成,但是在使用set方法進行寫操作時,則是每隔1s執行一次。
package com.day_7.excercise_1;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
//
public class TryReadWriteLock {
public static void main(String[] args) {
ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
// 寫
// for (int i = 0; i < 10; i++) {
// new Thread(new Runnable() {
// @Override
// public void run() {
// readWriteLockDemo.set((int)(Math.random()*101));
// }
// },"Write").start();
// }
// 讀
for (int i = 0; i < 100; i++) {
new Thread(new Runnable() {
@Override
public void run() {
readWriteLockDemo.get();
}
},"Read").start();
}
}
}
class ReadWriteLockDemo{
private int number = 1;
private ReadWriteLock lock = new ReentrantReadWriteLock();
public void get() {
lock.readLock().lock();
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+":"+number);
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.readLock().unlock();
}
}
public void set(int number) {
lock.writeLock().lock();
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+":"+number);
this.number = number;
} catch (Exception e) {
e.printStackTrace();
}finally {
lock.writeLock().unlock();
}
}
}
學習了很多關於JUC的知識,明確了很多內容,非常感謝特別香的視頻課程和文首的兩篇文章,很詳盡也很易懂,需要多看看加深印象。萬分感謝大家在網絡上的資源貢獻!