JAVA多線程高併發基礎知識點
本文中的代碼java版本:jdk11.
1. 線程基礎
1.1. 線程概念
線程是操作系統能夠進行運算調度的最小單位(程序執行流的最小單元)。
它被包含在進程之中,是進程中的實際運作單位。
一條線程指的是進程中一個單一順序的控制流,一個進程中可以併發多個線程,每條線程並行執行不同的任務。
1.2. 線程安全
- 原子性:線程共享內存同一時間只有一個線程可以操作(synchronized關鍵字,互斥鎖)。
- 可見性:某個線程的操作可以立即被其他線程觀察到(volatile關鍵字,強制刷新線程內該變量內存)。
- 有序性:線程內指令順序執行(現代CPU指令重排,亂序執行);在java中指令重排不影響單線程內的最終結果,但不保證多線程。
1.3. 創建並啓動線程的方法
- 實現 Runnable 接口
class ByRunnable implements Runnable{
@Override
public void run() {
System.out.println("實現Runnable並實現run方法");
}
}
Thread thread = new Thread(new ByRunnable());
thread.start();//調用start以開啓新線程
thread.join();//阻止主線程在新線程之前結束
- 繼承 Thread 類
class ByThread extends Thread{
@Override
public void run() {
System.out.println("繼承Thread並重寫run方法");
}
}
Thread thread = new ByThread();
thread.start();
thread.join();
- 守護線程
Thread thread = new Thread(()->System.out.println("lambda thread"));
//將線程設置爲守護線程
//JVM會在所有非守護線程結束後自動退出,
//守護線程不會隨着JVM退出而結束,
//因此守護線程如果持有需要關閉的資源,可能會因無法正常關閉資源(JVM退出)而造成嚴重後果,
//如打開文件可能會導致數據丟失.
thread.setDaemon(true);
thread.start();
1.4. 線程狀態
使用Thread.currentThread().getState();
可以獲得當前線程狀態,thread.getState();
獲得某一線程的狀態。
- 初始(NEW):新創建了一個線程對象,但還沒有調用start()方法。
- 運行(RUNNABLE):Java線程中將就緒(ready)和運行中(running)兩種狀態籠統的稱爲“運行”。 線程對象創建後,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取CPU的使用權,此時處於就緒狀態(ready)。就緒狀態的線程在獲得CPU時間片後變爲運行中狀態(running)。
- 阻塞(BLOCKED):表示線程阻塞於鎖。
- 等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。
- 超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間後自行返回。
- 終止(TERMINATED):表示該線程已經執行完畢。
1.5. 線程超時等待(TIMED_WAITING)相關方法
-
線程睡眠(sleep),當持有鎖時不釋放鎖!且sleep的線程無法從外部喚醒,但可被中斷!
try { //睡眠1000毫秒,在此期間不參與CPU資源競爭(線程調度) Thread.sleep(1000); } catch (InterruptedException e) { //可能在睡眠結束前線程就被中斷了 e.printStackTrace(); }
-
線程讓出(yield)
//給CPU一個提示,讓出本次線程調度,但參與下一次調度(進入線程等待隊列),CPU可能會忽略本次讓出 Thread.yield();
-
線程加入(join)
Thread t = new Thread(()->{ System.out.println("new Thread....."); }); t.start(); //將線程t加入當前線程,即告訴當前線程等待線程t結束或在1000毫秒後再繼續 t.join(1000);
-
線程超時等待(wait),只有持有鎖的對象才能調用這個方法且會釋放鎖!
//使獲得鎖的obj對象所在線程進入等待,等待1000毫秒後自動被喚醒,或者被notify(notifyAll)喚醒 obj.wait(1000);
-
線程超時禁用(park)
//等待到當前時間的1000毫秒結束禁用 LockSupport.parkUntil(System.currentTimeMillis()+1000); //禁用當前線程10000納秒 LockSupport.parkNanos(10000);
1.6. 線程等待(WAITING)相關方法
-
wait、notify、notifyAll:wait使當前獲得鎖的線程等待並釋放鎖,notify喚醒隨機一個等待的線程來競爭鎖及notifyAll喚醒所有等待的線程來競爭鎖,但notify操作不釋放鎖(只有持有鎖的對象才能調用這些方法,且notify和notifyAll只能喚醒在同一個鎖對象下wait的線程)
//模擬倉庫 ConcurrentLinkedQueue<String> queue = new ConcurrentLinkedQueue<>(); //模擬生產者 Thread producer = new Thread(()->{ for(int i=0;;++i){ try { while (queue.size() < 5) { Thread.sleep(100); queue.add("prod"+(++i)); } } catch (InterruptedException e) { e.printStackTrace(); } synchronized (queue) { //隨機喚醒一個等待中的線程 queue.notify(); if(queue.size() >= 5) { try { //倉庫滿了使當前線程等待 queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); //模擬消費者 Thread consumer = new Thread(()->{ for(;;){ try { while (queue.size() > 0 ) { Thread.sleep(10); System.out.println(queue.poll()); } } catch (InterruptedException e){ e.printStackTrace(); } synchronized (queue) { //喚醒所有其他線程,此時最多隻有一個線程(生產者)在等待 queue.notifyAll(); if(queue.size()<=0) { try { //商品消費完了,使當前線程等待 queue.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } } } }); producer.start(); consumer.start(); producer.join(); consumer.join();
-
park和unpark:park使線程等待及unpark使線程結束等待,park時當持有鎖時不釋放鎖!可以在外部被unpark喚醒!
Thread t = new Thread(()->{ System.out.println("new....."); LockSupport.park();//讓線程進入等待 System.out.println("over wait"); }); t.start(); Thread.sleep(1000); System.out.println("main sleep 1000"); LockSupport.unpark(t);//喚醒指定線程t /*控制檯輸出: new..... main sleep 1000 over wait */
2. synchronized關鍵字
- synchronized是同步、互斥、獨佔、可重入的鎖
- synchronized鎖住的是對象,當沒有傳入對象時:
- 當對靜態方法加鎖時鎖住的是當前類的class對象。
- 當對實例方法枷鎖時鎖住的時當前實例對象(this)。
- 在方法內使用synchronized時必須傳入對象
synchronized(obj){/*sync code*/}
並鎖住其代碼塊。
- synchronized鎖住的對象纔可以使用wait、notify、notifyAll。
- synchronized存在鎖升級的概念
- 當始終同時只有同一個線程競爭時,鎖時處於偏向鎖狀態(markword只記錄線程標識而不嘗試加鎖且不主動釋放記錄的線程標識,此時默認不會有其他線程競爭鎖)。
- 在獲得鎖的線程未結束時有線程來競爭鎖,鎖升級爲輕量級鎖(自旋鎖,線程不調用系統函數進入CPU等待隊列)。
- 當競爭變大(10次)或者鎖持續時間變長時,鎖升級爲重量級鎖(調用系統函數使線程進入CPU等待隊列)。
3. volatile關鍵字
-
保證線程可見性
- 在java中有共享內存,也有線程內工作內存,當持有共享內存的值時先將共享內存中的值複製到工作內存,每次修改值時先修改工作內存,寫回到共享內存的時機由CPU控制(線程之間不可見),可能出現A線程改變某一個線程共享變量的值後B線程無法及時看到更新後的值.當使用volatile關鍵字後,每次修改該變量都會及時回寫到共享內存並通知其他線程.
- 保證線程可見性的緩存一致性協議:Intel:MESI.
-
禁止指令重排序
-
指令不會被CPU和編譯器優化亂序執行,而是按照程序流線性執行.
-
volatile關鍵字在Double Check Lock(DCL)單例的應用 :
public class Test { private static volatile Test INSTANCE; public int i; private Test(){i=100;} //懶漢單例 public static final Test instance(){ if (INSTANCE==null) { synchronized (Test.class) { //Double Check 雙重檢測 if (INSTANCE==null) { //new一個對象分三步: //1.申請內存(對象成員變量賦初值); //2.初始化成員變量(按照代碼要求賦值); //3.將new對象賦值給變量. //new的過程不是原子的,可以指令重排序. //初始化單例時,volatile保證不會亂序執行,其他線程不會得到未完全初始化的單例. //即如果指令重排序了,先賦值了對象變量,再初始化對象內成員變量, //另一個線程得到的單例可能是未完全初始化的,對象的變量i可能是初值0 INSTANCE = new Test(); } } } return INSTANCE; } }
-
volatile不可避免的會降低程序執行效率.
4. Atomic原子操作類及CAS
4.1. CAS
全稱: Compare And Swap (比較並交換) 或者說 Compare And Set.
以CAS做底層的鎖是一種輕量級樂觀鎖,當前線程不會進入等待隊列.默認很快就會得到鎖.
CAS是一種無鎖算法(不與內核通信,不放棄CPU,循環執行比較直到預期值,完成變量賦值).
CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B. 當且僅當預期值A和內存值V相同時,將內存值V修改爲B,否則什麼都不做.
AtomicInteger::incrementAndGet底層的CAS
//代碼位於Unsafe.class
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
int expected,
int x) {
return compareAndSetInt(o, offset, expected, x);
}
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
CAS僞代碼實現
cas(value,expected,newValue){
if(value == expected) value = newValue;
else{
//try again or fail
}
}
CAS中的ABA問題
A線程對變量I的預期值爲0,由於從內存中獲取I需要時間,此時其他線程先將變量I改爲其他值後又改會0,雖然此時依然滿足A線程的預期值,但不是原來的預期值了,此時就發生了ABA問題,如果此時的偷樑換柱影響業務,一般的解決方式是加版本號.
4.2. 原子類
java原生提供了原子操作的對象,以用於線程間共享內存,atom類型在操作內存時主要使用了CAS模式.
通常情況下修改內存需要的時間都非常短暫,因此大多數情況下,在多個線程操作同一個變量時,原類型操作內存(CAS)的效率要高於使用同步代碼塊(synchronized)的效率
AtomicReference
包裹一個類型,使該變量的操作具有原子性,主要用於線程間共用變量
AtomicReference<Integer> ar = new AtomicReference<>();
new Thread(()->{
ar.set(1234);
}).start();
new Thread(()->{
System.out.println(ar.get());
}).start();
AtomicInteger
原子int類型,內部對數據的操作利用CAS模式
AtomicInteger ai = new AtomicInteger();
ai.incrementAndGet();
LongAdder
jdk1.8提供的用於超高併發情況下的Long類型計數器,以代替AtomicInteger或AtomicLong,在高併發下計算效率最高,並提供了更方便的操作方法.
LongAdder比AtomicLong更快的原因是採用了分段思想,使用了一個非volatile的Cell數組來存儲累積線程作爲分段鎖,當Cell數組不爲空時,
進行兩次CAS,第一次CAS當前操作的累積的Cell數組中的某個元素,第二次再CAS被volatile修飾的long值,
避免了直接修改long值時,許多線程CAS競爭時頻繁空轉,加快了CPU效率
LongAdder a = new LongAdder();
a.add(2);
int i = a.intValue();//返回當前的總和值並截斷爲int類型,返回的結果是原子操作後的
System.out.println(i);
a.increment();
System.out.println(a.intValue());
long l = a.sum();//當前的總和值,返回的結果是非原子的
System.out.println(l);
LongAdder::add
/**
* Adds the given value.
* @param x the value to add
*/
public void add(long x) {
Cell[] cs; long b, v; int m; Cell c;
if ((cs = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||
(c = cs[getProbe() & m]) == null ||
//c.cas:第一次CAS,這個c是cs數組中的一個元素,cs是一個分段鎖
!(uncontended = c.cas(v = c.value, v + x)))
//這裏面會用CAS實際add真正的long值
longAccumulate(x, null, uncontended);
}
}
其他
所有java原生提供的原子類型都是在java.util.concurrent.atomic
包下提供的
4.3. Unsafe類
獲取unSafe實例
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe)theUnsafe.get(Unsafe.class);
利用Unsafe直接操作內存
long l = unsafe.allocateMemory(8);//分配內存
unsafe.setMemory(l,0L,(byte)0);//操作內存
unsafe.freeMemory(l);//釋放內存
利用Unsafe直接生成並操作對象
class My{
int a = 111;
}
//直接生成實例
My o = (My) unsafe.allocateInstance(My.class);
long offset = unsafe.objectFieldOffset(My.class.getDeclaredField("a"));
//直接通過偏移量操作對象屬性
unsafe.getAndSetInt(o,offset,1234);
System.out.println(o.a);//輸出 1234
利用Unsafe操作CAS
unsafe.compareAndSwapInt(obj,offset,intVal);
unsafe.compareAndSwapLong(obj,offset,longVal);
5. 各種JUC包下的同步鎖
5.0 死鎖
死鎖的四個必要條件:
- 互斥:資源唯一,且線程互斥;
- 不可剝奪:已獲得資源後不可剝奪資源,只能主動釋放;
- 請求與保持:已獲得資源的線程嘗試獲得其他唯一資源,而該資源已被其他線程佔有;
- 循環等待:存在一種資源的循環等待鏈,鏈中每一個線程已獲得的資源同時被鏈中下一個線程所請求.
避免死鎖的核心思想: 打破死鎖的四個必要條件
- 加鎖順序
- 加鎖時限
- 死鎖檢測
避免死鎖的常用方法:
- 有序資源分配法
- 銀行家算法
解除死鎖的常用方法:
- 設置線程還原點
- 撤銷線程
- 剝奪資源
5.1. ReentrantLock
ReentrantLock是可重入鎖,且需要手動申請鎖,手動釋放鎖
嘗試申請鎖,申請不到不阻塞線程而是以無鎖繼續執行,返回true表示申請到鎖: ReentrantLock::tryLock
阻塞申請鎖,未申請到時將阻塞線程:ReentrantLock::lock
阻塞申請可以被打斷的鎖:ReentrantLock::lockInterruptibly
,打斷鎖:Thread::interrupt
,這種方式申請的鎖可中斷線程解除死鎖(捕捉中斷異常釋放或在finally塊中釋放)
釋放已經申請的鎖:ReentrantLock::unlock
公平鎖與非公平鎖的區別:
- 非公平鎖:執行lock時直接爭搶鎖,
synchronized
關鍵字是非公平鎖。 - 公平鎖:執行lock時,先檢查鎖等待隊列,如果隊列不爲空,則加入鎖隊列,否則直接爭搶鎖。
package test;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ThreadTest {
private static final String thStr(){
return Thread.currentThread().getId()+" "+Thread.currentThread().getName();
}
public static class ReentrantLockTest {
private final static Lock lock1 = new ReentrantLock();//入參爲true爲公平鎖,等待鎖最久將獲得鎖
private final static Lock lock2 = new ReentrantLock();
private static final void doSomething() {
String str = thStr();
try {
System.out.println(str);
lock1.lock();//獲得鎖
System.out.println(str+" alloc lock");
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(str+" will free lock");
lock1.unlock();//釋放鎖一定要在finally塊中,以保證得到執行
}
}
//常規獲得鎖
public final static void testLock() {
for (int i=0;i<10;i++){
new Thread(()->{
doSomething();
}).start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//死鎖及解除死鎖
public static final void deadLock() {
//兩個線程相互獲取對方鎖
var t1 = new Thread(new DeadLockThread(lock1,lock2));
var t2 = new Thread(new DeadLockThread(lock2,lock1));
t1.start();
t2.start();
try {
TimeUnit.MILLISECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
t1.interrupt();//中斷線程1,使線程2獲得鎖(解除死鎖)
try {
TimeUnit.MILLISECONDS.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static final class DeadLockThread implements Runnable {
private Lock lock1,lock2;
public DeadLockThread(Lock lock1, Lock lock2) {
this.lock1 = lock1;
this.lock2 = lock2;
}
@Override
public void run() {
String str = thStr();
try {
lock1.lockInterruptibly();//獲得可被打斷的鎖(以實現可解除死鎖)
System.out.println(str+" acquire lock1");
TimeUnit.MILLISECONDS.sleep(100);
lock2.lockInterruptibly();
System.out.println(str+" acquire lock2");
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(str+" will free lock1");
lock1.unlock();
System.out.println(str+" will free lock2");
lock2.unlock();
}
}
}
//超時休眠重新嘗試獲取鎖
public static final void tryLockTest(){
ArrayList<Thread> threads = new ArrayList<>(8);
for(int i=0;i<4;i++){
threads.add(new Thread(()->{
tryLock();
}));
}
threads.forEach(thread -> thread.start());
threads.forEach(t-> {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
private static final void tryLock(){
String str = thStr();
try {
while (!lock1.tryLock(1,TimeUnit.SECONDS)){
System.out.println(str + " try acquire lock1 filed");
Thread.sleep(300);
}
System.out.println(str + " acquire lock1");
Thread.sleep(2000);
} catch (Exception e) {
e.printStackTrace();
} finally {
System.out.println(str+" attempt free lock1");
lock1.unlock();
}
}
//Condition等待和喚醒
public static final void testCondition(){
ArrayList<Thread> threads = new ArrayList<>();
for (int i=0;i<20;i++) {
if(i%2==0){
threads.add(new Thread(()->ConditionTest.get()));
}else{
threads.add(new Thread(()->ConditionTest.put()));
}
}
threads.forEach(t->t.start());
}
private static final class ConditionTest{
private static final Condition condP = lock1.newCondition();
private static final Condition condG = lock1.newCondition();
private static final ArrayList<String> list = new ArrayList<>();
private static final AtomicInteger ai = new AtomicInteger(0);
public static final void put(){
String str = thStr();
try {
lock1.lock();
System.out.println(str+" try put");
if(list.size()>=3){
System.out.println(str+" list size >=3 will await");
condP.await(300,TimeUnit.MILLISECONDS);//線程進入等待,同時釋放鎖
}
list.add("item "+ai.incrementAndGet());
if(list.size()>=1){
System.out.println(str+" now list size "+list.size());
condG.signalAll();//喚醒等待的鎖
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(str+" unlock put");
lock1.unlock();
}
}
public static final void get(){
String str = thStr();
try {
lock1.lock();
System.out.println(str+" try get");
if(list.size()<=0){
System.out.println(str+" list size <=0 will await");
condG.await();
}
try {
System.out.println(str+" "+list.remove(0));
} catch (RuntimeException e){
System.err.println(str+" list size is zero, will retry");
get();
}
if(list.size()<3)
condP.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(str+" unlock get");
lock1.unlock();
}
}
}
}
}
5.2. ReadWriteLock 和 StampedLock
讀寫鎖同時實現了共享鎖(讀)和排他鎖(寫).
讀鎖對所有讀線程共享,寫鎖對所有操作線程排他.
爲滿足強一致性,ReadWriteLock
讀鎖採用悲觀鎖,即如果有線程正在讀,寫線程需要等待讀線程釋放鎖後才能獲取寫鎖.
讀寫鎖可能造成活鎖(某些線程一直拿不到鎖).
讀鎖可以避免讀到寫入一半的數據,寫鎖避免寫入混亂.
在寫少讀多的場景下ReadWriteLock
性能會明顯高於ReentrantLock
.
StampedLock
是ReadWriteLock
的改進版,主要改進了讀鎖爲樂觀鎖,提高了併發性能,但代價是可能讀取到中間數據,需要業務自己判斷取捨.
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class Test {
public static void main(String[] args) throws Exception {
threadTest();
}
static void threadTest() throws Exception {
var l = new ReentrantReadWriteLock();
var rl = l.readLock();
var wl = l.writeLock();
var ts = new Thread[9];
for(var i=0;i<9;++i){
final var iF = i;
ts[i] = new Thread(()->{
for(var j=0;j<2;++j){
//寫用寫鎖
if(iF/4==0 && j==1)write(wl,iF+j);
//讀用讀鎖
else read(rl);
}
});
}
for (Thread t : ts) {
t.start();
t.join();
}
}
private static int i;
private static void read(Lock l){
try {
l.lock();
Thread.sleep(100);
System.out.println("read "+i);
} catch (Exception e) {
e.printStackTrace();
} finally {
l.unlock();
}
}
private static void write(Lock l, int v){
try {
l.lock();
Thread.sleep(50);
System.out.println("write "+v);
i = v;
} catch (Exception e) {
e.printStackTrace();
} finally {
l.unlock();
}
}
}
/*控制檯輸出:
read 0
write 1
read 1
write 2
read 2
write 3
read 3
write 4
read 4
read 4
read 4
read 4
read 4
read 4
read 4
read 4
read 4
read 4
*/
import java.util.concurrent.locks.StampedLock;
public class Test {
public static void main(String[] args) throws Exception {
threadTest();
}
static void threadTest() throws Exception {
var l = new StampedLock();
var ts = new Thread[9];
for(var i=0;i<9;++i){
final var iF = i;
ts[i] = new Thread(()->{
for(var j=0;j<2;++j){
if(iF%4==0)write(l,iF+j);
else read(l);
}
});
}
for (Thread t : ts) {
t.start();
t.join();
}
}
private static int i;
private static void read(StampedLock l){
var stamp = l.tryOptimisticRead();
// 檢查讀鎖後是否有其他寫鎖發生
if(l.validate(stamp)){
try {
//樂觀讀鎖
stamp = l.readLock();
Thread.sleep(100);
System.out.println("read "+i);
} catch (Exception e) {
e.printStackTrace();
} finally {
l.unlockRead(stamp);
}
}else{
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("has write");
}
}
private static void write(StampedLock l, int v){
//寫鎖
var stamp = l.writeLock();
try {
Thread.sleep(50);
System.out.println("write "+v);
i = v;
} catch (Exception e) {
e.printStackTrace();
} finally {
l.unlockWrite(stamp);
}
}
}
5.3. CountDownLatch
一個類似於門閂的同步工具類,可以阻塞當前線程,直到計數歸零或到達超時時間。
CountDownLatch只能countDown,這意味着只能阻塞一次。
如果在調用CountDownLatch::await
時計數已經歸零則不會阻塞當前線程,否則阻塞當前線程直到計數歸零。
使用CountDownLatch::countDown
使計數減一。
一個線程可以調用多次countDown
,是計數多次減一。
主要用來開啓多個線程處理任務,並在全部處理結束後通知主線程處理接下來的任務。
//創建一個從2開始計數的CountDownLatch
CountDownLatch cd = new CountDownLatch(2);
for(int i=0;i<2;++i){
new Thread(()->{
try {
Thread.sleep(1000);
}catch (Exception e){}
finally {
//當線程完成自己的任務後cd計數減一
cd.countDown();
}
}).start();
}
//阻塞當前線程直到cd計數歸零
cd.await();
System.out.println("main");
5.4. CyclicBarrier
一個類似於柵欄的同步工具類,當計數器到達上限時喚醒所有阻塞的線程(放行)並可以執行指定任務。
CyclicBarrier可以循環使用,當到達臨界值時放行後自動歸零並重用。
CyclicBarrier可以用來做同步(最後一個線程調用await
後才能放行全部線程)。
調用CyclicBarrier::await
使線程等待,直到等待的線程到達CyclicBarrier的臨界值,所有await的線程將被喚醒。
由於使用await
相較於CountDownLatch
來說一個線程在一次"推倒柵欄"之前只能使計數器加一一次。
//創建一個臨界值爲3的CyclicBarrier對象,並在計數到達3時執行指定任務,這個任務將先於被放行的線程執行
CyclicBarrier cb = new CyclicBarrier(3, ()->{
//這段代碼將使用最後一個執行await的線程執行,然後所有線程將執行其後的操作
System.out.println(Thread.currentThread().getName()+" ========== we go~ time:"+new Date().getTime());
});
for(int i=0;i<3;++i){
int iF= i;
new Thread(()->{
try {
Thread.sleep(iF*1000);
System.out.println(Thread.currentThread().getName()+" "+iF+" wait time:"+new Date().getTime());
//等待柵欄打開,這個動作會使計數器加一,當加到臨界值時全部等待的線程將開始執行
cb.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
} finally {
//cb.reset();//重置計數
//柵欄打開後將執行的任務,放行後柵欄重置,可以再次await
System.out.println(Thread.currentThread().getName()+" "+iF+" go time:"+new Date().getTime());
try {
Thread.sleep(4000-iF*1000);
System.out.println(Thread.currentThread().getName()+" "+"| "+iF+" wait2 time:"+new Date().getTime());
//再次等待柵欄打開
cb.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
} finally {
System.out.println(Thread.currentThread().getName()+" "+"| "+iF+" go2 time:"+new Date().getTime());
}
}
}).start();
}
/*控制檯輸出:
Thread-0 0 wait time:1616147361601
Thread-1 1 wait time:1616147362604
Thread-2 2 wait time:1616147363613
Thread-2 ========== we go~ time:1616147363613
Thread-0 0 go time:1616147363623
Thread-1 1 go time:1616147363623
Thread-2 2 go time:1616147363623
Thread-2 | 2 wait2 time:1616147365631
Thread-1 | 1 wait2 time:1616147366631
Thread-0 | 0 wait2 time:1616147367633
Thread-0 ========== we go~ time:1616147367633
Thread-0 | 0 go2 time:1616147367633
Thread-2 | 2 go2 time:1616147367633
Thread-1 | 1 go2 time:1616147367633
*/
5.5. Phaser
一個階段性同步工具類,當所有線程到達某一階段時進入下一階段,可以看作CyclicBarrier的升級版。
每當所有線程都執行了arrive
則進入下一階段。直到所有線程都註銷(deregister)了則流程結束。
每一個Phaser
都允許有父Phaser
,這樣可以將多個Phaser
串聯組成一個複雜的流程。
Phaser
可以用來做階段性的流程控制。
import java.util.concurrent.*;
public class Test {
public static void main(String[] args) throws Exception {
threadTest();
}
/**執行測試*/
static void threadTest() throws Exception {
var wp = new WeddingPhaser();
Thread pst[] = new Thread[7];
for(var i=0;i<5;++i) pst[i]=new Thread(new Person(wp,"賓客" + i));
pst[5]=new Thread(new Person(wp,"新郎"));
pst[6]=new Thread(new Person(wp,"新娘"));
//所有線程都建立完成了纔可以開始執行,否則流程將錯亂(某些成員可能在第二階段及其後才註冊)
for (Thread t : pst) t.start();
}
/**創建自己的Phaser並指定某一階段執行什麼任務*/
static class WeddingPhaser extends Phaser {
//每一次到達階段時將執行的任務,返回true時表示所有階段完成
@Override
protected boolean onAdvance(int phase, int registeredParties) {
switch (phase) {
case 0:
System.out.println("\n階段"+phase+"所有人到齊,共"+registeredParties);
return false;
case 1:
System.out.println("\n階段"+phase+"所有人用餐完畢,共"+registeredParties);
return false;
case 2:
System.out.println("\n階段"+phase+"所有賓客離場,共"+registeredParties);
return false;
case 3:
System.out.println("\n階段"+phase+"新郎新娘洞房,共"+registeredParties);
return true;
default:
return super.onAdvance(phase,registeredParties);
}
}
private void phase0(String name) throws Exception{
System.out.print(name+"到場 ");
Thread.sleep(1000);
this.arriveAndAwaitAdvance();
}
private void phase1(String name) throws Exception{
System.out.print(name+"用餐 ");
Thread.sleep(1000);
this.arriveAndAwaitAdvance();
}
private void phase2(String name) throws Exception{
if(name.startsWith("賓客")){
//所有賓客在此階段離場
System.out.print(name+"離場 ");
Thread.sleep(1000);
//到達最後階段,註銷自己
this.arriveAndDeregister();
}else{
//新郎新娘送客但不離場
System.out.print(name+"送客 ");
Thread.sleep(1000);
this.arriveAndAwaitAdvance();
}
}
private void phase3(String name) throws Exception{
if(name.startsWith("新")){
System.out.print(name+"洞房 ");
Thread.sleep(1000);
}else{
//賓客在上一階段就已經註銷了自己,但由於是異步的,因此這個階段可能還會有賓客線程未銷燬,依然執行
System.out.print(name+"到家 ");
}
//階段全部結束,註銷自己
this.arriveAndDeregister();
}
public void runPhase(String name){
try {
phase0(name);
phase1(name);
phase2(name);
phase3(name);
} catch (Exception e){}
}
}
/**參與Phaser的成員*/
static class Person implements Runnable {
final WeddingPhaser wp;
final String name;
Person(WeddingPhaser wp,String name){
this.wp = wp;
this.name = name;
this.wp.register();
}
@Override
public void run() {
wp.runPhase(name);
}
}
}
/*控制檯輸出:
賓客1到場 新郎到場 賓客3到場 賓客4到場 賓客2到場 賓客0到場 新娘到場
階段0所有人到齊,共7
新娘用餐 賓客3用餐 賓客1用餐 賓客0用餐 賓客4用餐 新郎用餐 賓客2用餐
階段1所有人用餐完畢,共7
新郎送客 賓客0離場 新娘送客 賓客4離場 賓客1離場 賓客3離場 賓客2離場 賓客4到家 賓客0到家
階段2所有賓客離場,共2
賓客3到家 新娘洞房 新郎洞房
階段3新郎新娘洞房,共0
*/
5.6. Semaphore
一個可以反覆使用的,限制最大活躍線程數量的便捷工具類,常用來做限流.
當信號量爲0時不再有線程可以獲取到信號量,直到有其他線程放棄信號量.
var s = new Semaphore(1);//同時只能活躍1個線程
for(var i=0;i<2;++i){
var iF = i;
var t = new Thread(()->{
for(var j=0;j<9;++j){
try {
s.acquire();//獲得信號量,沒獲得信號量的線程將等待
Thread.sleep(100);
System.out.println("t"+iF+" is running");
} catch (Exception e) {e.printStackTrace();} finally {
s.release();//釋放信號量
}
}
});
t.start();
System.out.println(t.getState());
}
5.7. Exchanger
簡單易用的線程通信工具類,交換兩個線程的數據,每當交換完成後就可以重用,如交易雙方,一手交錢一手交貨才完成交換.
var ec = new Exchanger<String>();
for(var i=0;i<2;++i){
var iF = i;
new Thread(()->{
String s = "t"+iF, s1 = "T"+iF;
try {
//交換數據,阻塞線程直到雙方都調用了這個方法
s = ec.exchange(s);
System.out.println("t"+iF+" var:"+s);
s1 = ec.exchange(s1);
System.out.println("t"+iF+" var1:"+s1);
} catch (InterruptedException e) {e.printStackTrace();}
}).start();
}
Thread.sleep(100);
/*控制檯輸出:
t0 var:t1
t1 var:t0
t0 var1:T1
t1 var1:T0
*/
5.8. LockSupport
一個用來支撐自定義鎖的工具類,也可以直接使用使當前線程等待
var t = new Thread(()->{
LockSupport.park();//使自己進入等待, 如果持有鎖,不會自動釋放鎖
System.out.println("等待結束");
System.out.println(new Date().getTime());
});
t.start();
System.out.println(new Date().getTime());
Thread.sleep(1000);
//使線程t允許運行,
//如果unpark先於park那麼該次park將會直接跳過,即該次park不會造成線程等待
LockSupport.unpark(t);
Thread.sleep(100);
/*控制檯輸出:
1616410337402
等待結束
1616410338411
*/
5.9. AbstractQueuedSynchronizer
AQS是鎖的核心.
AQS的核心是一個volatile int類型的變量state,以及監控它的一個雙向鏈表,鏈表的每個Node裝的線程.
每當線程嘗試獲得鎖時,先檢查state是否爲0,爲0則爭搶鎖,大於0時如果當前線程已經獲得鎖則重入否則嘗試獲得鎖失敗,進入鏈表尾部(CAS)等待獲得鎖.
6. 併發容器
6.1. 阻塞隊列
SynchronousQueue
//容量爲0的阻塞隊列,用於線程間交換數據,直接將數據送往另一個線程
public static SynchronousQueue synchronousQueue = new SynchronousQueue();
public static void main(String[] args) throws Exception{
new Thread(()->{
try {
//取出
System.out.println(synchronousQueue.take());
System.out.println(synchronousQueue.take());
System.out.println("OVER2");
} catch (Exception e) {
e.printStackTrace();
}
}).start();
//阻塞等待索取
synchronousQueue.put("synchronousQueue2");
synchronousQueue.put("synchronousQueue1");
System.out.println("OVER1");
}
TransferQueue
//提供獨特的transfer方法,阻塞當前線程,直到被transfer放入的數據被取出
public static TransferQueue transferQueue = new LinkedTransferQueue();
public static void main(String[] args) throws Exception{
for (int i = 0; i < 2; i++) {
new Thread(()->{
try {
//取出數據
System.out.println(transferQueue.take());
} catch (Exception e) {
e.printStackTrace();
}
}).start();
}
//阻塞到被取出
transferQueue.transfer("transferQueue1");
System.out.println("transferQueue1over");
transferQueue.transfer("transferQueue2");
System.out.println("transferQueue2over");
}
DelayQueue
//延時隊列
static class MyDelayed implements Delayed{
String msg;
long time;
public MyDelayed(String msg, long time) {
this.msg = msg;
this.time = time+System.currentTimeMillis();
}
@Override //到達執行時間的剩餘時間
public long getDelay(TimeUnit unit) {
return unit.convert(time-System.currentTimeMillis(),TimeUnit.MILLISECONDS);
}
@Override //確定優先級
public int compareTo(Delayed o) {
return (int)(getDelay(TimeUnit.MILLISECONDS)-o.getDelay(TimeUnit.MILLISECONDS));
}
}
public static DelayQueue<MyDelayed> delayQueue = new DelayQueue();
public static void main(String[] args) throws Exception{
delayQueue.add(new MyDelayed("asd3",2000l));
delayQueue.add(new MyDelayed("asd1",1000l));
delayQueue.add(new MyDelayed("asd2",1500l));
MyDelayed myDelayed;
while ((myDelayed=delayQueue.take())!=null)
System.out.println(myDelayed.msg+" ,current: "+System.currentTimeMillis());
}
ArrayBlockingQueue和LinkedBlockingQueue
用於在線程間傳遞任務
static void threadTest() throws Exception {
var q = new LinkedBlockingQueue<String>(4);
var t1 = new Thread(()->{
for(var i=0;i<100;++i){
try {
q.put("s"+i);
if(i==99)q.put("over");
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
var t2 = new Thread(()->{
for(;;){
try {
var s = q.poll();
System.out.println(s);
if(s!=null&&s.equals("over"))return;
Thread.sleep(51);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
6.2. 其他併發相關容器
HashTable和HashMap
Hashtable
是一個同步的Map
容器,它使用synchronized
關鍵字做同步,這個設計在高併發下性能低下,基本不再使用.
Hashtable
和HashMap
最大的區別是HashMap
完全不加鎖.
通過Collections.synchronizedMap(new HashMap<>())
將HashMap
變爲加鎖的版本.
Vector和ArrayList
Vector
是一個List
容器,它使用synchronized
關鍵字做同步,這個設計在高併發下性能低下,基本不再使用.
Vector
和ArrayList
最大的區別是ArrayList
完全不加鎖.
通過Collections.synchronizedList(new ArrayList<>())
將ArrayList
變爲加鎖的版本.
ConcurrentHashMap
高性能併發Map,內部採用了CAS
var map = new ConcurrentHashMap<String,String>();
map.put("a","A");
System.out.println(map.get("a"));
ConcurrentSkipListMap
高性能併發Map,內部採用了CAS,和ConcurrentHashMap
相比,ConcurrentSkipListMap
是有序的,且便利速度明顯快於ConcurrentHashMap
var map = new ConcurrentSkipListMap<String,String>();
map.put("a","A");
System.out.println(map.get("a"));
ConcurrentLinkedQueue
高性能併發Queue,內部採用了CAS
var tickets = new ConcurrentLinkedQueue<String>();
for(var i=0;i<100;++i){
tickets.add("ticket"+i);
}
var ts = new Thread[3];
for(var i=0;i<3;++i){
ts[i] = new Thread(()->{
for(;;){
var tk = tickets.poll();//取出,如果使用ArrayList,下面則可能輸出空
if(tk!=null) System.out.println(tk);
else return;
}
});
}
for (Thread t : ts) t.start();
for (Thread t : ts) t.join();
CopyOnWriteList
寫時複製List.
讀取數據時不加鎖,寫數據時加鎖,先拷貝原內存到一個新的內存,並添加一個位置存儲新的數據,寫完後將指向原內存的引用指向新內存.
典型的以空間換時間.
在寫少讀多的場景下性能極高.
var l = new CopyOnWriteArrayList<String>();
l.add("asd");
System.out.println(l.get(0));
7. 線程併發
併發(concurrent)和並行(parallel)的關係:併發是指任務提交(不一定並行),並行是指任務執行,並行是併發的子集.
7.1. 線程池
ThreadPoolExecutor
一個持有一定量線程的池子,併發情況下避免頻繁創建銷燬線程,並可以提供定製化拒絕策略.
一般不使用默認線程池,他們各自有着缺陷.
常用的定時任務線程池quartz
.
//完整構造線程池
public ThreadPoolExecutor(int corePoolSize, //核心線程數(核心線程不會被銷燬)
int maximumPoolSize, //最大線程數
long keepAliveTime, //超過核心線程數的線程的最大空閒生存時間,其後將可能被銷燬
TimeUnit unit, //keepAliveTime的單位
BlockingQueue<Runnable> workQueue, //線程隊列,當線程數超過核心線程數時入隊
ThreadFactory threadFactory, //線程工廠
RejectedExecutionHandler handler) //當線程數滿,隊列滿時的拒絕策略
{
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
ThreadPoolExecutor::execute執行的三個步驟
//ThreadPoolExecutor::execute
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
/*
* 分三步進行:
* 1. 如果運行的線程少於corePoolSize,
* 嘗試以command作爲第一個task開啓一個一個新核心線程.
* 2. 如果成功將command入隊workQueue,
* 雙重檢測確保線程池正RUNNING,
* (可能有其他線程執行了shutdown).
* 如果線程池已經shutdown,則回滾入隊操作,
* 並執行拒絕策略
* 3. 如果無法入隊,直接添加新的工作線程並執行command,
* 如果操作失敗了,則說明線程池可能已經shutdown或飽和了,
* 則執行拒絕策略
*/
//獲取ctl快照
int c = ctl.get();
//第一步
//判斷工作線程數是否少於設定的核心線程數值
if (workerCountOf(c) < corePoolSize) {
//添加核心工作線程
if (addWorker(command, true))
return;
//重新獲取ctl快照(ctl可能已被其他線程修改)
c = ctl.get();
}
//第二部
//如果線程池正RUNNING,將command加入workQueue
if (isRunning(c) && workQueue.offer(command)) {
//重新獲取ctl快照
int recheck = ctl.get();
//雙重檢測,確保線程池沒有shutdown,如果shutdown了則將command出隊workQueue
if (! isRunning(recheck) && remove(command))
//執行拒絕策略
reject(command);
//判斷此時線程池正RUNNING,且可用工作線程爲0,
else if (workerCountOf(recheck) == 0)
//添加新的非核心線程,並從workQueue中取出首個command運行
addWorker(null, false);
}
//隊列可能已滿從而失敗的情況下,直接添加非核心工作線程,並將command作爲task運行
else if (!addWorker(command, false))
//執行addWorker失敗(線程池關閉或飽和)則執行拒絕策略
reject(command);
}
線程池中線程的5種狀態:RUNNING<SHUTDOWN<STOP<TIDYING<TERMINATED.
ForkJoinPool
- 分解並彙總任務.
- 用很少線程執行很多任務(子任務),而ThreadPoolExecutor做不到子任務.
- CPU密集型線程池.
- 使用
Executors::newWorkStealingPool
可以便捷地創建FJP線程池. - jdk8提供的流式API中
Collection::parallelStream
(並行流)內部就是使用的FJP.
static void testFJP() throws Exception {
//實現一個RecursiveTask,如果實現RecursiveAction則是沒有返回值的任務
class AddTask extends RecursiveTask<Long> {
public final int start,end;
public final int MAX_NUM = 1000;
public AddTask(int start, int end) {
this.start = start;this.end = end;
}
@Override
protected Long compute() {
var len = end-start;
if(len<=MAX_NUM){
var sum = 0L;
for(var i=start;i<end;++i) sum+=i;
return sum;
}else{
var middle = start+len/2;
var t1 = new AddTask(start,middle);
var t2 = new AddTask(middle,end);
t1.fork();t2.fork();//fork分出子任務
return t1.join()+t2.join();//將子任務join,當前線程等待子任務完成任務
}
}
}
//使用FJP執行計算密集任務,充分發揮CPU性能
System.out.println(new ForkJoinPool().submit(new AddTask(0, 9999)).get());
}
7.2. ThreadLocal
ThreadLocal
可以在當前線程存儲一個變量,因此ThreadLocal具有線程隔離的效果.
其內部維護了一個ThreadLocalMap
,key爲線程,value爲set
設置的值.
如spring的聲明式事務,就是將數據庫連接存入ThreadLocal,以保證在同一個線程內的操作是屬於同一個連接,從而實現事務.
var s = new ThreadLocal<String>();
s.set("asd");
System.out.println(s.get());
7.3. 強軟弱虛引用
1. 強引用
普通的引用都是強引用.
如var o = new Object()
就創建了一個強引用.
強引用只有其不再被使用時,其內存纔會被回收.
class MyObj {
@Override //被回收前執行的方法
protected void finalize() throws Throwable {System.out.println("我沒了");}
}
var m = new MyObj();
System.gc();//第一次GC,m指向的對象不會被回收
Thread.sleep(1111);
System.out.println("睡了1111");
m=null;//使m指向的對象不再被使用
System.gc();//第二次GC,回收m指向的對象
System.exit(0);
/*控制檯輸出:
睡了1111
我沒了
*/
2. 軟引用
當內存不夠時,即時該變量依然被引用還是會被回收.
創建軟引用需要使用對象SoftReference
.
軟引用可以用來做緩存.
//測試前需要將堆內存設置小一點,如 -Xms:20M -Xmx:20M
var s = new SoftReference<byte[]>(new byte[1024*1024*10]);
System.out.println(s.get());
System.gc();//不回收
System.out.println(s.get());
Thread.sleep(1000);
System.out.println("睡了1000");
System.gc();//不回收
var y = new byte[1024*1024*15];//填充內存,此時內存已不足以放下y,軟引用將被回收以放下y
System.out.println(s.get());//輸出null
System.exit(0);
3. 弱引用
只要發生gc,弱引用將被回收.
創建弱引用需要使用對象WeakReference
.
一般用在容器中,只要該對象的強引用不存在了,該對象就可以被回收了.
如ThreadLocal內部的Entry就是使用的WeakReference,即ThreadLocal持有某個變量時不影響其被回收,避免了內存泄漏.
var s = new WeakReference<byte[]>(new byte[1024]);
System.out.println(s.get());
System.gc();//只要發生gc,弱引用將被回收
System.out.println(s.get());//輸出null
System.exit(0);
4. 虛引用
只要發生gc,虛引用指向的對象一定被回收,同時向queue中發送通知.
無法通過虛引用對象拿到其內的對象,它的唯一作用是被回收時發送一個通知.
創建虛引用需要使用對象PhantomReference
.
主要用在JVM中,和普通程序員無關.
如:當使用堆外內存時,使用該堆外內存的對象被回收時發送一個通知,通知某一個對象對該堆外對象進行處理.
class MyCls {
protected void finalize() throws Throwable{System.out.println("It is over.");}
}
var r = new ReferenceQueue<MyCls>();
var p = new PhantomReference<MyCls>(new MyCls(),r);
System.out.println(p.get());//get始終爲null
var t = new Thread(()->{
for(;;){
try {Thread.sleep(100);} catch (InterruptedException e) {}
var s = r.poll();
if (s!=null) {
System.out.println("回收");
return;
}
System.gc();//gc將回收p內對象,併發送消息
}
});
t.start();
t.join();
/*控制檯輸出:
null
It is over.
回收
*/
7.4. VarHandle
jdk9提供的java.lang.invoke.VarHandle
,是一個指向變量的引用,可以對對象普通屬性進行原子操作,比反射快(無需運行時檢測,直接操作二進制碼).
VarHandle
極大的提升了JUC下工具的效率,如使用VarHandle
優化了AbstractQueuedSynchronizer
更快的操作內部的鏈表.
public class XXX{
long x = 100L;
private static VarHandle varHandle;
static {
try {
varHandle = MethodHandles.lookup().findVarHandle(XXX.class,"x",long.class);
} catch (Exception e) { e.printStackTrace(); }
}
public static void main(String[] args) throws Exception {
var me = new XXX();
System.out.println("x="+varHandle.get(me));//100
varHandle.set(me,1000l);
System.out.println("x="+me.x);//1000
varHandle.compareAndSet(me,100l,10000l);//CAS
System.out.println("x="+me.x);//1000
}
}
7.5. ExecutorService和Callable及FutureTask
ThreadPoolExecutor
實現了ExecutorService
接口
Callable
是一個允許有返回值的任務
Callable
一般配合ExecutorService
使用,異步執行任務,在另一個線程同步等待結果.
常用在異步執行多個任務,在主線程阻塞等待所有異步任務返回結果.
Callable<String> callable = ()->"返回值";
ExecutorService executorService = Executors.newFixedThreadPool(1);
Future<String> submit = executorService.submit(callable);//異步執行任務
System.out.println(submit.get());//阻塞,直到獲得返回值
FutureTask
是一個簡化的版本,既是一個Task
,也是一個Future
.
var futureTask = new FutureTask<String>(()->"返回了一個值");
new Thread(futureTask).start();
System.out.println(futureTask.get());//阻塞
7.5. CompletableFuture
JDK8提供便捷的函數式(鏈式)異步工具類,內部使用了ForkJoinPool
.
期待JDK提供async
和await
關鍵字更方便的實現異步編程!
常用方法:
CompletableFuture::runAsync
:異步執行一個Runnable
.
CompletableFuture.runAsync(()-> System.out.print("異步任務."));
System.out.print("同步任務.");
//控制檯輸出: 同步任務.異步任務.
CompletableFuture::supplyAsync
:異步執行一個有返回值的任務
CompletableFuture::thenAcceptAsync
:接收上一個CompletableFuture
的返回值,並異步執行任務,thenAccept
是其同步版本.
CompletableFuture.supplyAsync(()->"異步任務返回").thenAcceptAsync(System.out::println);
CompletableFuture::anyOf
:任意一個任務完成執行接下來的任務.
CompletableFuture.anyOf(
CompletableFuture.runAsync(()-> {
try {Thread.sleep(100);} catch (InterruptedException e) {}
System.out.print("執行任務1.");
}),
CompletableFuture.runAsync(()-> System.out.print("執行任務2."))
).thenRun(()-> System.out.print("執行完成."));
//控制檯輸出: 執行任務2.執行完成.執行任務1.
CompletableFuture::allOf
:所有任務完成執行接下來的任務,用法同anyOf
.
CompletableFuture.allOf(
CompletableFuture.supplyAsync(()->"ret1").thenAccept(System.out::println),
CompletableFuture.runAsync(()-> System.out.println("run2"))
).join();//使當前線程阻塞,直到上面兩個任務都完成
CompletableFuture::delayedExecutor
返回一個定時執行任務的Executor
.
CompletableFuture.delayedExecutor(1,TimeUnit.SECONDS).execute(()-> System.out.println("1秒後"));
8. 練習題
8.1. 線程交替運行
兩個線程分別交替打印1-25和A-Z.
var lock = new Object();
var t1 = new Thread(()->{
for(var i='A';i<='Z';++i){
synchronized (lock){
lock.notify();
System.out.println("字母"+i);
try {
if(i!='Z')lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
var t2 = new Thread(()->{
for(var i=0;i<26;++i){
synchronized (lock) {
lock.notify();
System.out.println("數字"+i);
try {
if(i!=25)lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
8.2. 實現阻塞隊列
簡單實現阻塞隊列,用以支撐生產者消費者模型
//阻塞隊列
static class SyncQueue<T> {
private final List<T> list;
private final int cap;
private volatile int size = 0;
SyncQueue(int cap){
list = new LinkedList();
this.cap = cap;
}
public int getSize(){ return size; }
public synchronized void put(T val){
//使用while避免在被喚醒後即時已經到達cap也被放行
while (list.size() >= cap) doWait();
list.add(val);++size;
this.notifyAll();
}
public synchronized T get(){
while (list.size() <= 0) doWait();
T val = list.remove(0);--size;
this.notifyAll();
return val;
}
private void doWait(){
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
//生產者消費者模型
static void threadTest() throws Exception {
var q = new SyncQueue<Integer>(10);
var ts = new Thread[4];
for(var i=0;i<4;++i){
if(i%2!=0)
ts[i]=new Thread(()->{
for(var j=0;j<=20;++j){
var s = q.getSize();q.put(s);
System.out.println(Thread.currentThread().getName()+" put "+s);
}
});
else
ts[i]=new Thread(()->{
for(var j=0;j<=20;++j) System.out.println(Thread.currentThread().getName()+" get "+q.get());
});
}
for (Thread t : ts) t.start();
for (Thread t : ts) t.join();
}
分離阻塞及喚醒消費者生產者的阻塞隊列
class SyncQueue<T> {
private final List<T> list;
private final int cap;
private volatile int size = 0;
private final ReentrantLock lock;
//使用Condition分離put和get
private final Condition conditionPut,conditionSet;
SyncQueue(int cap){
list = new LinkedList();
this.cap = cap;
lock = new ReentrantLock();
conditionPut = lock.newCondition();
conditionSet = lock.newCondition();
}
public int getSize(){ return size; }
public void put(T val){
try {
lock.lock();
while (list.size() >= cap) conditionPut.await();
list.add(val);++size;
conditionSet.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public T get(){
try {
lock.lock();
while (list.size() <= 0) conditionSet.await();
T val = list.remove(0);--size;
conditionPut.signalAll();
return val;
} catch (InterruptedException e) {
e.printStackTrace();
return null;
} finally {
lock.unlock();
}
}
}