為什么 要建設(shè)網(wǎng)站掃一掃識(shí)別圖片
文章目錄
- 一 . synchronized 原理
- 1.1 synchronized 使用的鎖策略
- 1.2 synchronized 是怎樣自適應(yīng)的? (鎖膨脹 / 升級(jí) 的過(guò)程)
- 1.3 synchronized 其他的優(yōu)化操作
- 鎖消除
- 鎖粗化
- 1.4 常見面試題
- 二 . JUC (java.util.concurrent)
- 2.1 Callable 接口
- 2.2 ReentrantLock
- 2.3 原子類
- 2.4 線程池
- ExecutorService 和 Executors
大家好 , 這篇文章給大家分享多線程中 synchronized 的原理以及 JUC 相關(guān)問(wèn)題
注意這塊的 synchronized 是小寫的 , 一定要注意拼寫
推薦大家跳轉(zhuǎn)到 此鏈接 查看效果更佳~
上一篇文章的鏈接我也給大家貼到這里了
點(diǎn)擊即可跳轉(zhuǎn)到文章專欄~
一 . synchronized 原理
注意這塊的 synchronized 是小寫的 , 一定要注意拼寫
1.1 synchronized 使用的鎖策略
- 既是悲觀鎖 , 也是樂(lè)觀鎖 (自適應(yīng)鎖)
- 既是輕量級(jí)鎖 , 也是重量級(jí)鎖 (自適應(yīng)鎖)
- 輕量級(jí)鎖部分基于自旋鎖實(shí)現(xiàn) , 重量級(jí)鎖部分基于掛起等待鎖來(lái)實(shí)現(xiàn)
- 不是讀寫鎖
- 是非公平鎖
- 是可重入鎖
1.2 synchronized 是怎樣自適應(yīng)的? (鎖膨脹 / 升級(jí) 的過(guò)程)
synchronized 在加鎖的時(shí)候要經(jīng)歷幾個(gè)階段 :
- 無(wú)鎖 (沒(méi)加鎖)
- 偏向鎖 (剛開始加鎖 , 未產(chǎn)生競(jìng)爭(zhēng)的時(shí)候)
- 輕量級(jí)鎖 (產(chǎn)生鎖競(jìng)爭(zhēng)了)
- 重量級(jí)鎖 (鎖競(jìng)爭(zhēng)的更激烈了)
其中 , 我們?cè)俜治鲆幌率裁词瞧蜴i
偏向鎖 , 不是"真正加鎖" , 只是用個(gè)標(biāo)記表示 “這個(gè)鎖是我的了” , 在遇到其他線程來(lái)競(jìng)爭(zhēng)鎖之前 , 都始終保持這個(gè)狀態(tài) .
直到真的有人來(lái)競(jìng)爭(zhēng)了,此時(shí)才真的加鎖
這個(gè)過(guò)程類似于單例模式中的"懶漢模式" , 必要的時(shí)候再加鎖 , 節(jié)省開銷
舉個(gè)栗子 :
我是一個(gè)漂亮的妹子 , 遇到了一個(gè)小哥哥 , 對(duì)他各個(gè)方面都很滿意 , 我們的感情就很快升溫
但是我就不和他確定關(guān)系 , 造成若即若離的感覺(jué) , 這樣的話后面如果我膩歪了 , 隨時(shí)伸腿就踹了 , 成本很低
這就是偏向鎖狀態(tài)
突然 , 我又發(fā)現(xiàn)另外一個(gè)妹子也在接近小哥哥 , 這個(gè)時(shí)候我趁著他們倆剛認(rèn)識(shí) , 我就趕緊和小哥哥確立男女朋友關(guān)系 , 并且發(fā)朋友圈官宣 , 另外的這個(gè)妹子就上一邊等著去
這就是偏向鎖在遇到鎖競(jìng)爭(zhēng)的時(shí)候 , 再真正進(jìn)行加鎖
如果沒(méi)有額外的妹子(線程)過(guò)來(lái)競(jìng)爭(zhēng) , 從始至終都是在偏向鎖的狀態(tài) , 也就省去了加鎖以及解鎖的開銷了 , 這就更加的輕量
1.3 synchronized 其他的優(yōu)化操作
鎖消除
鎖消除.編譯器自動(dòng)判定 , 如果認(rèn)為這個(gè)代碼沒(méi)必要加鎖 , 就不加了 .
這個(gè)操作不是所有情況下都會(huì)觸發(fā) , 大部分情況下不能觸發(fā)
比如 :
StringBuffer sb = new StringBuffer();
sb.append("a");
sb.append("b");
sb.append("c");
sb.append("d");
此處的這幾個(gè) append 方法 , 內(nèi)部都是帶有 synchronized 的
如果上述代碼都是在同一個(gè)線程中運(yùn)行的 , 此時(shí)就沒(méi)必要再去加鎖了
JVM 就悄悄地把鎖去掉了
鎖粗化
先了解鎖的粒度 : synchronized 包含的代碼范圍是大還是小 , 范圍越大 , 粒度越粗 ; 范圍越小 , 粒度越細(xì)
鎖的粒度細(xì)了 , 能夠更好的提高線程的并發(fā) , 但會(huì)也會(huì)增加 “加鎖解鎖” 的次數(shù)
1.4 常見面試題
- 能夠理解 synchronized 基本執(zhí)行過(guò)程 , 理解鎖對(duì)象 , 理解鎖競(jìng)爭(zhēng)
- 能夠知道 synchronized 的基本策略
- 能夠理解 synchronized 內(nèi)部的一些鎖優(yōu)化的過(guò)程 ( 鎖升級(jí) , 鎖消除 , 鎖粗化 )
- 什么是偏向鎖
二 . JUC (java.util.concurrent)
concurrent 中文叫做并發(fā)
java.util.concurrent 這個(gè)包里就存放了很多和多線程開發(fā)相關(guān)的類
2.1 Callable 接口
和我們之前學(xué)習(xí)過(guò)得 Runnable 非常類似 , 都是可以在創(chuàng)建線程的時(shí)候 , 來(lái)指定一個(gè) “具體的任務(wù)”
而 Callable 指定的任務(wù)是帶有返回值的 , Runnable 是不帶返回值的
Callable 里面會(huì)提供一個(gè) call 方法 , call 方法是帶有返回值的 , 我們可以借助它很容易的獲得到任務(wù)的執(zhí)行結(jié)果
舉個(gè)栗子 : 創(chuàng)建線程計(jì)算 1 + 2 + 3 + … + 1000, 不使用 Callable 版本
static class Result {public int sum = 0;public Object lock = new Object();
}public static void main(String[] args) throws InterruptedException {Result result = new Result();// 創(chuàng)建一個(gè)線程去計(jì)算 1~100 之間的值// 但是我們通過(guò) run 方法沒(méi)辦法返回值// 就需要把結(jié)果寫入到 Result 類當(dāng)中的 sum Thread t = new Thread() {@Overridepublic void run() {int sum = 0;for (int i = 1; i <= 1000; i++) {sum += i;}// 賦值操作需要加鎖synchronized (result.lock) {result.sum = sum;result.lock.notify();}}};t.start();// 在主線程這里,再去針對(duì) result 結(jié)果進(jìn)行等待// 上面的 result 結(jié)果計(jì)算好之后,上面的 notify 就會(huì)喚醒下面的 wait// 打印 sum 的值synchronized (result.lock) {while (result.sum == 0) {result.lock.wait();}System.out.println(result.sum);}
}
上述代碼需要一個(gè)輔助類 Result , 還需要使用一系列的加鎖和 wait notify 操作 , 代碼復(fù)雜 , 容易出錯(cuò) .
我們可以使用 Callable 接口
import java.util.concurrent.Callable;public class Demo28 {public static void main(String[] args) {// 創(chuàng)建 Callable 接口,它是帶有泛型參數(shù)的// 這個(gè)泛型參數(shù)實(shí)際就是 call 方法的返回值// new 一個(gè)匿名內(nèi)部類Callable<Integer> callable = new Callable<Integer>() {// 這里的 Object 要改成 Integer@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 1000; i++) {sum += i;}return sum;}};}
}
接下來(lái) , 我們就可以新建線程執(zhí)行這個(gè)任務(wù)了
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo28 {public static void main(String[] args) throws ExecutionException, InterruptedException {// 創(chuàng)建 Callable 接口,它是帶有泛型參數(shù)的// 這個(gè)泛型參數(shù)實(shí)際就是 call 方法的返回值// new 一個(gè)匿名內(nèi)部類Callable<Integer> callable = new Callable<Integer>() {// 這里的 Object 要改成 Integer@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 1000; i++) {sum += i;}return sum;}};// 套上一層,目的是為了獲取到后續(xù)的結(jié)果FutureTask<Integer> task = new FutureTask<>(callable);Thread t = new Thread(task);t.start();// 在線程 t 執(zhí)行結(jié)束之前,get 會(huì)阻塞等待,直到 t 執(zhí)行完了,結(jié)果算完了// get 才能返回.返回值就是 call 方法 return 的內(nèi)容System.out.println(task.get());}
}
這里的 FutureTask 就好比 :
我們?nèi)ゲ宛^吃飯 , 人很多的時(shí)候 , 老板會(huì)給你個(gè)小票 , 后續(xù)就可以憑小票來(lái)取餐
到目前為止 , 我們已經(jīng)學(xué)習(xí)過(guò)好幾種創(chuàng)建線程的方式了
- 繼承 Thread
- 使用 Runnable
- 使用 lambda
- 使用 Callable
- 使用線程池
2.2 ReentrantLock
ReentrantLock 代表可重入鎖
synchronized 已經(jīng)是可重入鎖了 , 為什么還要再弄一個(gè) ReentrantLock 呢 ?
- synchronized 是單純的關(guān)鍵字 , 以代碼塊為單位進(jìn)行加鎖解鎖 .
ReentrantLock則是一個(gè)類 , 提供 lock 方法加鎖 , unlock 方法解鎖
import java.util.concurrent.locks.ReentrantLock;public class Demo29 {public static void main(String[] args) {ReentrantLock locker = new ReentrantLock();// 加鎖locker.lock();// 其他代碼邏輯// 解鎖locker.unlock();}
}
但這種方式還存在一些問(wèn)題
假如中間的其他代碼邏輯出現(xiàn)了問(wèn)題 , 拋出了異常 , 后面的 unlock() 就執(zhí)行不到了
所以我們一般把加鎖解鎖操作放到 try catch finally 中
import java.util.concurrent.locks.ReentrantLock;public class Demo29 {public static void main(String[] args) {ReentrantLock locker = new ReentrantLock();try {// 加鎖locker.lock();// 其他代碼邏輯} finally {// 解鎖locker.unlock();}}
}
- ReentrantLock 會(huì)提供一個(gè)"公平鎖"版本 , 在構(gòu)造實(shí)例的時(shí)候 , 可以通過(guò)構(gòu)造方法指定一個(gè)參數(shù) , 切換到公平鎖模式
ReentrantLock locker = new ReentrantLock(true);
synchronized 只是一個(gè)非公平鎖
- ReentrantLock 還提供了一個(gè)特殊的加鎖操作 : tryLock()
默認(rèn)的 lock 是加鎖失敗 , 就阻塞
而 tryLock 加鎖失敗 , 則不阻塞 , 直接往下執(zhí)行 , 并且返回 false
除了立即失敗之外 , tryLock 還能設(shè)定一定的等待時(shí)間 (等一會(huì)再失敗)
- ReentrantLock 提供了更強(qiáng)大的 等待/喚醒 機(jī)制
synchronized 搭配的是 Object.wait / notify , 喚醒的時(shí)候 , 隨機(jī)喚醒其中一個(gè)
ReentrantLock 搭配了 Condition 類來(lái)實(shí)現(xiàn)等待喚醒 , 可以做到能隨機(jī)喚醒一個(gè) , 也能指定線程喚醒
大部分情況下 , 使用鎖還是 synchronized 為主 .
特殊場(chǎng)景下 , 才使用 ReentrantLock
2.3 原子類
原子類內(nèi)部用的是 CAS 實(shí)現(xiàn),所以性能要比加鎖實(shí)現(xiàn) i++ 高很多
我們常用的是 AtomicInteger
他的常用方法有
addAndGet(int delta); i += delta;
decrementAndGet(); --i;
getAndDecrement(); i--;
incrementAndGet(); ++i;
getAndIncrement(); i++;
2.4 線程池
雖然創(chuàng)建銷毀線程比創(chuàng)建銷毀進(jìn)程更輕量 , 但是在頻繁創(chuàng)建銷毀線程的時(shí)候還是會(huì)比較低效.
線程池就是為了解決這個(gè)問(wèn)題 . 如果某個(gè)線程不再使用了 , 并不是真正把線程釋放 , 而是放到一個(gè) "池子"中 , 下次如果需要用到線程就直接從池子中取 , 不必通過(guò)系統(tǒng)來(lái)創(chuàng)建了.
ExecutorService 和 Executors
ExecutorService 是一個(gè)線程實(shí)例 , Executors 是一個(gè)工廠類
Executors 創(chuàng)建線程池的幾種方式
- newFixedThreadPool : 創(chuàng)建固定線程數(shù)的線程池
- newCachedThreadPool : 創(chuàng)建線程數(shù)目動(dòng)態(tài)增長(zhǎng)的線程池.
- newSingleThreadExecutor : 創(chuàng)建只包含單個(gè)線程的線程池.
- newScheduledThreadPool : 設(shè)定 延遲時(shí)間后執(zhí)行命令 , 或者定期執(zhí)行命令 . 是進(jìn)階版的 Timer.
Executors 本質(zhì)上是 ThreadPoolExecutor 類的封裝 , 這個(gè)類是標(biāo)準(zhǔn)庫(kù)中最核心的線池類
打開我們的 Java 文檔
我們來(lái)看第四個(gè)構(gòu)造方法
實(shí)際工作中 , 一般建議大家 , 使用線程池的時(shí)候 , 盡量還是用 ThreadPoolExecutor 復(fù)雜版本的 , 這里的參數(shù)都顯式的手動(dòng)傳參 , 這樣就可以更好的掌控代碼
當(dāng)我們使用線程池的時(shí)候 , 線程數(shù)目設(shè)置成多少合適 ?
只要你回答出具體的數(shù)字 , 一定都是錯(cuò)的 .
不同的場(chǎng)景 , 不同的程序 , 不同的主機(jī)配置 , 都會(huì)有差異
面試中我們回答不了具體設(shè)置幾個(gè)線程 , 但是可以回答 : 找到合適線程數(shù)的方法 -> 壓測(cè)(性能測(cè)試)
針對(duì)當(dāng)前的程序進(jìn)行性能測(cè)試 , 分別設(shè)置不同的線程數(shù)目 , 分別進(jìn)行測(cè)試
在測(cè)試過(guò)程中 , 會(huì)記錄程序的時(shí)間、CPU占用、內(nèi)存占用…
根據(jù)壓測(cè)結(jié)果 , 來(lái)選擇咱們覺(jué)得最適合當(dāng)前場(chǎng)景的數(shù)目
關(guān)于 JUC , 我們后續(xù)還會(huì)再增加一些內(nèi)容 , 大家敬請(qǐng)期待~
如果對(duì)你有幫助的話 , 請(qǐng)一鍵三連嗷~