JUC全稱為java.util.concurrent,其中,concurrent這個包里包含了很多和多執行緒并發相關的操作,同樣也是面試中的高頻考點,下面博主就帶大家學習學習這部分內容吧!

JUC
- 一. ReentrantLock
- 1. 理解
- 2. 用法
- 3. 與synchronized區別
- 4. 總結
- 二. 原子類
- 1. 理解
- 2. 常見的原子類
- 3. 常見的方法
- 三. 執行緒池
- 1. 為什么要引入執行緒池
- 2. 引入執行緒池的好處
- 3. 創建執行緒池的方法
- (1)ThreadPoolExecutor
- (2) Executors
- 四. 信號量Semaphore
- 1. 定義
- 2. 作用
- 3. 用法示例
- 五. CountDownLatch
- 1. 理解
- 2. 用法
- 六. 高頻面試題
- 1. 行程間通信有哪幾種方式
- 2. 執行緒同步的方式有哪些
- 3. 為什么有了synchronized還需要JUC底下的 lock
- 4. 簡單說一下什么是信號量
- 5. 解釋一下 ThreadPoolExecutor 構造方法的引數的含義
一. ReentrantLock
1. 理解
- 之前我們討論的可重入鎖,翻譯成英文就是ReentrantLock,大部分情況下這個英文單詞要理解成這一鎖特性,但少數情況下要理解成一個類
- 和 synchronized 定位類似,都是用來實作互斥效果,用來保證執行緒安全,同時這個鎖是可重入的
2. 用法
下面我們來看一段代碼實作兩個執行緒分別對一個變數count累加操作:
public class Test {
static class Counter{
public int count=0;
public void increase(){
count++;
}
}
public static void main(String[] args) {
Counter counter=new Counter();
Thread t1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter.count);
}
}
經過之前的學習,我們認為此方法列印count是執行緒不安全的,不會每次都很準確地列印10000:
第一次運行

第二次運行

之前我們學過的解決方法是使用synchronized保證執行緒的安全性,代碼如下:
static class Counter{
public int count=0;
synchronized public void increase(){
count++;
}
}
改動部分如上圖所示(其他部分一樣),列印結果如下:
但此時我們可以通過創建ReentrantLock這一物件對其實作加鎖,完整代碼如下:
import java.util.concurrent.locks.ReentrantLock;
public class Test {
static class Counter {
public int count;
public ReentrantLock locker = new ReentrantLock();
public void increase() {
locker.lock();
count++;
locker.unlock();
}
}
public static void main(String[] args) {
Counter counter=new Counter();
Thread t1 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
Thread t2 = new Thread() {
@Override
public void run() {
for (int i = 0; i < 50000; i++) {
counter.increase();
}
}
};
t1.start();
t2.start();
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter.count);
}
}
列印結果如下:

3. 與synchronized區別
那么,與synchronized同樣都能對其實作加鎖功能,這兩者有什么區別呢?
ReentrantLock把加鎖和解鎖拆成了兩個方法,確實存在遺忘解鎖的風險,但可以讓代碼變得更加靈活,可以把加鎖和解鎖的代碼分別放到兩個方法之中synchronized在申請鎖失敗時,代碼會死等,而ReentrantLock可以通過trylock這個方法等待一段時間就放棄,不會浪費時間synchronized是非公平鎖,而ReentrantLock默認是非公平鎖,但可以通過構造方法傳入一個 true 開啟公平鎖模式ReentrantLock有更強大的喚醒機制,synchronized是通過 Object 的 wait / notify 方法實作等待喚醒程序的,每次喚醒的是一個隨機等待的執行緒,而ReentrantLock搭配 Condition 類實作等待-喚醒,可以更精確控制喚醒某個指定的執行緒,
4. 總結
- 大部分情況下使用
synchronized就足夠了 - 鎖競爭激烈的時候,使用
ReentrantLock, 搭配trylock方法可以更靈活地控制加鎖的行為,而不是死等, - 如果需要使用公平鎖, 使用
ReentrantLock
二. 原子類
1. 理解
保證執行緒安全不一定非得加鎖,當然也可以用原子類,從java1.5開始,jdk提供了java.util.concurrent.atomic包,這個包內包含一系列的原子操作類,提供了一種用法簡單,性能高效,執行緒安全的更新一個變數的方式,其內部通常以CAS方式實作,因此性能通常比加鎖實作i++要高很多,具體使用方法如下(上述例子)
public AtomicInteger count = new AtomicInteger(0);
public void increase() {
count.getAndIncrement();
}
這里只展示改動后的代碼,其列印結果如下:

2. 常見的原子類
- AtomicBoolean
- AtomicInteger
- AtomicIntegerArray
- AtomicLong
- AtomicReference
- AtomicStampedReference
3. 常見的方法
以 AtomicInteger 舉例,常見方法有
- addAndGet(int delta); 相當于 i += delta;
- decrementAndGet(); 相當于–i;
- getAndDecrement(); 相當于i–;
- incrementAndGet(); 相當于++i;
- getAndIncrement(); 相當于i++;

三. 執行緒池
1. 為什么要引入執行緒池
解決并發編程的方案一般是靠多行程的,但是行程開銷的資源是非常大的,因此我們進一步地引入了多執行緒,雖然創建銷毀執行緒比創建銷毀行程看起來似乎更輕量了,但是在頻繁創建毀執行緒的時還是會比較低效,執行緒池就是為了解決這個問題,如果某個執行緒不再使用了,并不是真正把執行緒釋放,而是放到一個 "池子"中,當我們需要使用多執行緒的時候,直接從之前創建好的池子中取出一個就行了,當我們不用的時候,直接把這個執行緒放回池子中即可,
2. 引入執行緒池的好處
- 當我們不用執行緒池的時候,頻繁地創建或者銷毀執行緒涉及到用戶態和內核態的來回切換,從用戶態切換到內核態會創建出對應的PCB(行程控制塊,英文是Processing Control Block),這樣會消耗大量的系統資源,而且效率還會比較低,
- 當我們引入執行緒池后,相當于只在用戶態完成各種操作,這樣代碼執行效率和系統開銷會大大優化
3. 創建執行緒池的方法
(1)ThreadPoolExecutor
使用Java標準庫中的ThreadPoolExecutor方式創建,但需注意里面各自的引數代表的含義,使用起來相對而言比較復雜,
構造方法

為了更好地理解每個引數的具體含義,大家可以利用空閑時間去jdk的官方檔案學習學習,對自己是非常有幫助的:
(2) Executors
使用 Executors 這個類創建,這個類相當于一個工廠類,通過這個工廠類中的一些方法,就可以創建出不同風格的執行緒池實體了,
部分方法
- Executors.newFixedThreadPool:創建一個固定大小的執行緒池
- Executors.newCachedThreadPool:創建一個可快取的執行緒池,若執行緒數超過處理所需,快取一段時間后會回收,若執行緒數不夠,則新建執行緒,
- Executors.newSingleThreadExecutor:創建出只包含一個執行緒數的執行緒池,它可以保證先進先出的執行順序,
- Executors.newScheduledThreadPool:創建一個可以執行延遲任務的執行緒池(放入的任務能夠過一會再執行)
- Executors.newSingleThreadScheduledExecutor:創建出具有一個單執行緒并且可以執行延遲任務的執行緒池
用法示例:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Test {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
}
運行結果:

四. 信號量Semaphore
1. 定義
信號量Semaphore一般用來表示可用資源的個數,相當于一個計數器,可類比生活中停車場牌子上面顯示的停車場剩余車位數量,
- 當有車開進去的時候, 就相當于申請一個可用資源,可用車位就 -1 (這個稱為信號量的 P 操作)
- 當有車開出來的時候, 就相當于釋放一個可用資源, 可用車位就 +1 (這個稱為信號量的 V 操作)
- 如果計數器的值已經為 0 了,還嘗試申請資源,就會阻塞等待,直到有其他執行緒釋放資源(計數器的值是大于等于0的)
2. 作用
- 在創建信號量的時候,可以給定一個初始值(可用資源個數),當可用資源個數用完時,就會阻塞等待,以確保執行緒安全
- 若把信號量的初始值設成1,則計數器的值只能取0或1了,此時把這個信號量稱為二元信號量,和鎖的功能類似,有加鎖(沒法申請資源)和解鎖狀態(可以申請資源)
3. 用法示例
下面我們創建15個執行緒,給定初始資源量為3個,然后先嘗試申請資源(acquire),申請完資源后再休眠1秒,然后釋放資源(release):
mport java.util.concurrent.Semaphore;
public class Test {
public static void main(String[] args) {
Semaphore semaphore = new Semaphore(3);
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
System.out.println("準備申請資源");
semaphore.acquire();
System.out.println("申請資源成功");
// 申請到資源之后休眠1秒
Thread.sleep(1000);
semaphore.release();
// 釋放資源
System.out.println("釋放資源完畢");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
// 創建15個執行緒,讓這 15 個執行緒來分別去嘗試申請資源
for (int i = 0; i < 15; i++) {
Thread t = new Thread(runnable);
t.start();
}
}
}
運行結果如下:
可以看到,由于資源數為3,所以前3個執行緒申請資源后很容易成功,而之后的執行緒就沒有資源可以申請了,只能等到前3個執行緒把資源釋放出來后再申請
信號量相當于是鎖的升級版本,鎖只能控制一個資源的有無,而信號量可以控制很多個資源的有無
五. CountDownLatch
1. 理解
用于同時等待N個任務結束,就好比百米賽跑一樣,只有當所有選手都到位之后,哨聲響了之后才能同時出發開始跑步,當所有選手都通過終點時才會公布成績,
2. 用法
我們創建10個執行緒同時開始執行一個任務,每個任務執行完后記錄一下,都呼叫 latch.countDown()方法,在CountDownLatch 內部的計數器同時自減,再創建一個主執行緒,其中使用 latch.await(); 阻塞等待至所有任務執行完畢(此時計數器為0)
用法示例
import java.util.concurrent.CountDownLatch;
public class Test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(10);
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("任務開始");
try {
Thread.sleep((long) (Math.random() * 10000));//生成亂數
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown();
System.out.println("任務完成!");
}
};
for (int i = 0; i < 10; i++) {
Thread t = new Thread(runnable);
t.start();
}
latch.await();
System.out.println("所有任務結束");
}
}
列印結果如下:
六. 高頻面試題
1. 行程間通信有哪幾種方式
- 網路(socket)
- 檔案
- 管道
- 訊息佇列(作業系統內核提供的方式)
- 信號量(作業系統內核提供的方式,有別于本篇講的Java標準庫提供的方式)
- 信號
2. 執行緒同步的方式有哪些
首先,執行緒同步就是協同步調,按預定的先后次序進行運行,即靠哪個執行緒先獲得到CUP的執行權誰就先執行
- 阻塞佇列
- synchronized
- ReentrantLock
- Semaphore
- volatile等等
3. 為什么有了synchronized還需要JUC底下的 lock
詳見與synchronized區別部分(學會用自己的話總結)
4. 簡單說一下什么是信號量
詳見信號量的定義部分(學會用自己的話總結)
5. 解釋一下 ThreadPoolExecutor 構造方法的引數的含義
詳見上文中所涉及的jdk官方檔案,這個引數雖然比較多,但大致上可以通過英文推測出其大意,此時就需要我們平常多記記了!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/325574.html
標籤:java
上一篇:2017 課程筆記
