多執行緒
目錄
- 多執行緒
- 1.認識執行緒及執行緒的創建
- <1>.執行緒的概念
- <2>.執行緒的特性
- <3>.執行緒的創建方式
- (1)繼承Thread類
- (2)實作Runnable介面
- (3)實作Callable介面
- 2.執行緒的常用方法
- <1>構造方法和屬性的獲取方法
- <2>常用方法
- (1)run()和 start()
- (2)interrupt()方法
- (3)join方法
- (4)獲取當前執行緒的參考currentThread();方法
- (5)休眠當前執行緒sleep();方法
- (6)執行緒讓步yield();方法
- 3.執行緒的生命周期和狀態轉換
- 4.執行緒間的通信
- 5.多執行緒的安全及解決
- <1>原子性
- <2>可見性
- <3>代碼的順序性
- <4>執行緒不安全問題的解決
- (1)synchronized 關鍵字
- (2)volatile 關鍵字
- 6.鎖體系
- <1>Synchronized加鎖方式
- (1)<1>Synchronized的加鎖方式及語法基礎
- (2)Synchronized的原理及實作
- (3)JVM對Synchronized的優化
- 1)對鎖的優化
- 2)鎖粗話
- 3)鎖消除
- <2>常見的鎖策略及CAS
- (1)樂觀鎖和悲觀鎖
- (2)自旋鎖
- (3)可重入鎖
- <3>Lock體系
- (1)Lock介面
- 1)使用Lock鎖實作執行緒同步
- 2)Lock加鎖的四種方式
- (2)AQS簡單認識
- (3)ReentrantLock
- 1)ReentrantLock基本概念
- 2)自己實作一個簡單的ReentrantLock
- 3)ReentrantLock部分原始碼分析
- 4)ReadWriteLock鎖
- <4>.Lock鎖和同步鎖(synchronized)的區別與對比
- <5>死鎖
- 7.多執行緒案例
- <1>生產者消費者問題
- <2>單例模式
- <3>阻塞式佇列
- <4>執行緒池
- 8.JUC包
- <1>Semaphore(信號量 )
- <2>CountDownLatch
- <3>CyclicBarrier(回圈柵欄)
- <4>ConcurrentHashMap
- (1)ConcurrentHashMap加鎖方式
- (2)fail-fast和fail-safe
- 9.ThreadLocal
- <1>基礎API
- <2>ThreadLocal和synchronized的區別
- <3>ThreadLocal的內部結構
- <4>ThrealLocal記憶體泄漏
- 10.Java多執行緒相關面試題
- <1>、volatile、ThreadLocal的使用場景和原理;
- <2>、ThreadLocal什么時候會出現OOM的情況?為什么?
- <3>、synchronized、volatile區別
- <4>ThreadLocal的作用以及為什么發生記憶體泄漏和解決方法
- <5>.如何中斷一個執行緒
- Java小白到初中級必備:[Java基礎知識必備資源](https://download.csdn.net/download/qq_45704528/20832780)
1.認識執行緒及執行緒的創建
<1>.執行緒的概念
執行緒和行程的區別:
行程是系統分配資源的最小單位,執行緒是系統調度的最小單位,
一個行程內的執行緒之間是可以共享資源的,
每個行程至少有一個執行緒存在,即主執行緒,
注:
每個行程至少有一個執行緒存在,即主執行緒(系統級別的,C語言的主執行緒)
java級別的主執行緒(自己寫的入口函式main方法(可以沒有這個執行緒)
對java行程來說,至少有一個非守護執行緒還沒終止,行程就不會結束
<2>.執行緒的特性
在后面執行緒的安全性會詳細介紹
1.原子性:即一個操作或者多個操作 要么全部執行并且執行的程序不會被任何因素打斷,要么就都不執行,
2.可見性:當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值,
3.有序性:程式執行的順序按照代碼的先后順序執行,
<3>.執行緒的創建方式
(1)繼承Thread類
class MyThread extends Thread{
@Override
public void run() {
System.out.println("繼承Thread類創建執行緒");
}
}
public static void main(String[] args) {
//1.繼承Thread類創建執行緒
MyThread t=new MyThread();
t.start();
}
(2)實作Runnable介面
- 將MyRunnable物件作為任務傳入Thread中
class MyRunnable implements Runnable{
@Override
public void run() {
System.out.println("繼承Runnable介面,創建描述任務物件,實作多執行緒");
}
}
public static void main(String[] args) {
//2.實作Runnable介面
Thread t1=new Thread(new MyRunnable());
t1.start();
}
2.使用匿名內部類實作
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("使用Runnable介面,創建匿名內部類實作");
}
});
t2.start();
(3)實作Callable介面
實作Callable重現call方法,允許拋出例外,允許帶有回傳值,回傳資料型別為介面上的泛型
class MyCallable implements Callable<String> {
//允許拋出例外,允許帶有回傳值,回傳資料型別為介面上的泛型
@Override
public String call() throws Exception {
System.out.println("實作了Callable介面");
return "這不是一個執行緒類,而是一個任務類";
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
//方法三:實作Callable介面,是一個任務類
//FutureTask底層也實作了Runnable介面
FutureTask<String> task=new FutureTask<>(new MyCallable());
new Thread(task).start();
System.out.println(task.get());
}
2.執行緒的常用方法
<1>構造方法和屬性的獲取方法
構造方法:

屬性的獲取方法:

<2>常用方法
(1)run()和 start()
start();方法:啟動執行緒
run();方法:覆寫 run 方法是提供給執行緒要做的事情的指令清單
start()和run()的區別:見代碼
public class Thread_Run_VS_Start {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
while (true){
}
}
}).run();
/**
* main執行緒直接呼叫Thread物件的run方法會直接在main執行緒
* 運行Thread物件的run()方法---->傳入的runnable物件.run()
* 結果,main執行緒直接運行while(true)
*
* start()是啟動一個執行緒,呼叫新執行緒的while(true)方法
* 對比通過start()呼叫的結果區別
*/
new Thread(new Runnable() {
@Override
public void run() {
while (true){
}
}
}).start();
}
}
(2)interrupt()方法

通過interrupt()方法,通知執行緒中的中斷標志位,由false變為true,但是執行緒什么時候中斷,需要執行緒自己的代碼實作
通過執行緒中的中斷標志位實作,比起自己手動設定中斷標志位,可以避免執行緒處于阻塞狀態下,無法中斷的情況
對interrupt,isInterrupt,interrupted的理解:
實體方法:
(1)interrupt:置執行緒的中斷狀態
如果呼叫該方法的執行緒處于阻塞狀態(休眠等),會拋出InterruptedException例外
并且會重置Thread.interrupted;回傳當前標志位,并重置
(2)isInterrupt:執行緒是否中斷,回傳boolean
靜態方法:
(3)interrupted:回傳執行緒的上次的中斷狀態,并清除中斷狀
public class Interrupt {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
//...執行任務,執行時間可能比較長
//運行到這里,在t的構造方法中不能參考t使用Thread.currentThread()方法,獲取當前代碼行所在執行緒的參考
for (int i = 0; i <10000&&!Thread.currentThread().isInterrupted() ; i++) {
System.out.println(i);
//模擬中斷執行緒
try {
Thread.sleep(1000);
//通過標志位自行實作,無法解決執行緒阻塞導致無法中斷
//Thread,sleep(100000)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t.start();//執行緒啟動,中斷標志位=false
System.out.println("t start");
//模擬,t執行了5秒,行程沒有結束,要中斷,停止t執行緒
Thread.sleep(5000);
//未設定時,isInterrupt為false
//如果t執行緒處于阻塞狀態(休眠等),會拋出InterruptedException例外
//并且會重置isInterrupt中斷標志位位false
t.interrupt();//告訴t執行緒,要中斷(設定t執行緒的中斷標志位為true),由t的代碼自行決定是否要中斷
//isInterrupt設定為true
//t.isInterrupted(); Interrupted是執行緒中的標志位
System.out.println("t stop");
//注:Thread.interrupted(); 回傳當前執行緒的中斷標志位,然后重置中斷標志位
}
}
(3)join方法
注意: join方法是實體方法
等待一個執行緒執行完畢,才執行下一個執行緒(呼叫該方法的執行緒等待

無參:t.join:當前執行緒無條件等待,直到t執行緒運行完畢

有參:t.join(1000)等待1秒,或者t執行緒結束,哪個條件滿足,當前執行緒繼續往下執行
//join方法:實體方法:
// 1.無參:t.join:當前執行緒無條件等待,直到t執行緒運行完畢
// 2.有參:t.join(1000)等待1秒,或者t執行緒結束,哪個條件滿足,當前執行緒繼續往下執行
public class Join {
public static void main(String[] args) throws InterruptedException {
Thread t=new Thread(new Runnable() {
@Override
public void run() {
System.out.println("1");
}
});
t.start();
t.join();//當前執行緒main執行緒無條件等待,直到t執行緒執行完畢,當前執行緒再往后執行
// t.join(1000);當前執行緒等到1秒,或者等t執行緒執行完畢
System.out.println("ok");
}
}
(4)獲取當前執行緒的參考currentThread();方法
靜態方法:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ud5ACR7u-1628311316793)(C:\Users\26905\AppData\Roaming\Typora\typora-user-images\image-20210709001304951.png)]
public class ThreadDemo {
public static void main(String[] args) {
Thread thread = Thread.currentThread();
System.out.println(thread.getName());
}
}
(5)休眠當前執行緒sleep();方法
讓執行緒等待一定時間后,繼續運行
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-Y6l240R4-1628311316794)(C:\Users\26905\AppData\Roaming\Typora\typora-user-images\image-20210709001410719.png)]
Thread.sleep(1000);
(6)執行緒讓步yield();方法
讓yield();所在代碼行的執行緒讓步,當其他執行緒先執行
public class Yield {
public static void main(String[] args) {
for(int i=0;i<20;i++){
final int n=i;
Thread t=new Thread(new Runnable() {
@Override
public void run() {
System.out.println(n);
}
});
t.start();
}
//判斷:如果活躍的執行緒數量大于1,main執行緒讓步
while (Thread.activeCount()>1){//記錄活躍執行緒的數量
Thread.yield();
}//注意:要用debug方式,因為run方式,idea后臺還會啟動一個執行緒
//實作ok在1到二十之后列印
System.out.println("ok");
}
}
3.執行緒的生命周期和狀態轉換
Java 語言中執行緒共有六種狀態,分別是:
NEW(初始化狀態)
RUNNABLE(可運行 / 運行狀態)
BLOCKED(阻塞狀態)
WAITING(無時限等待)
TIMED_WAITING(有時限等待)
TERMINATED(終止狀態)
生命周期和狀態轉換圖:

常見的API導致的狀態轉換:
1.執行緒的阻塞:
Thread.sleep(long);當前執行緒休眠
t.join/t.join(long);t執行緒加入當前執行緒,當前執行緒等待阻塞
synchronized:競爭物件鎖失敗的執行緒,進入阻塞態
2.執行緒的啟動:
start() ----->注意:run()只是任務的定義,start()才是啟動
3.執行緒的中斷:interrupt讓某個執行緒中斷,不是直接停止執行緒,而是一個“建議”,是否中斷,由執行緒代碼自己決定
4.執行緒間的通信
wait(0方法:執行緒等待
notify();方法:隨機喚醒一個執行緒
notifyAll():方法:喚醒所有等待的執行緒
注意:這三個方法都需要被Synchronized包裹x

執行緒間通信的案例:
有三個執行緒,每個執行緒只能列印A,B或C
要求:同時執行三個執行緒,按ABC順序列印,依次列印十次
ABC換行 ABC換行,,,,
public class SequencePrintHomeWork {
//有三個執行緒,每個執行緒只能列印A,B或C
//要求:同時執行三個執行緒,按ABC順序列印,依次列印十次
//ABC換行 ABC換行,,,,
//考察知識點:代碼設計,多執行緒通信
public static void main(String[] args) {
Thread a = new Thread(new Task("A"));
Thread b = new Thread(new Task("B"));
Thread c = new Thread(new Task("C"));
c.start();
b.start();
a.start();
}
private static class Task implements Runnable{
private String content;
//順序列印的內容:可以回圈列印
private static String[] ARR = {"A", "B", "C"};
private static int INDEX;//從陣列哪個索引列印
public Task(String content) {
this.content = content;
}
@Override
public void run() {
try {
for(int i=0; i<10; i++){
synchronized (ARR){//三個執行緒使用同一把鎖
//從陣列索引位置列印,如果當前執行緒要列印的內容不一致,釋放物件鎖等待
while(!content.equals(ARR[INDEX])){
ARR.wait();
}
//如果陣列要列印的內容和當前執行緒要列印的一致,
// 就列印,并把陣列索引切換到一個位置,通知其他執行緒
System.out.print(content);
if(INDEX==ARR.length-1){
System.out.println();
}
INDEX = (INDEX+1)%ARR.length;
ARR.notifyAll();
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
補充: wait()和sleep()的區別:
wait 之前需要請求鎖,而wait執行時會先釋放鎖,等被喚醒時再重新請求鎖,這個鎖是 wait 物件上的 monitor
lock
sleep 是無視鎖的存在的,即之前請求的鎖不會釋放,沒有鎖也不會請求,
wait 是 Object 的方法
sleep 是 Thread 的靜態方法
5.多執行緒的安全及解決
<1>原子性
對原子性的理解: 我們把一段代碼想象成一個房間,每個執行緒就是要進入這個房間的人,如果沒有任何機制保證,A進入房間之后,還沒有出來;B 是不是也可以進入房間,打斷 A 在房間里的隱私,這個就是不具備原子性的,
注意: 一條 java 陳述句不一定是原子的,也不一定只是一條指令
例如:

如果一個執行緒正在對一個變數操作,中途其他執行緒插入進來了,如果這個操作被打斷了,結果就可能是錯
<2>可見性
為了提高效率,JVM在執行程序中,會盡可能的將資料在作業記憶體中執行,但這樣會造成一個問題,共享變數在多執行緒之間不能及時看到改變,這個就是可見性問題,

可見性:
系統調度CPU執行執行緒內,某個方法,產生CPU視角的主存,作業記憶體
主存:執行緒共享
作業記憶體:執行緒私有記憶體+CPU高速快取/暫存器
對主存中共享資料的操作,存在主存到作業記憶體<====>從主存讀取,作業記憶體修改,寫回主存(拷貝)
<3>代碼的順序性
代碼的重排序:
一段代碼:
1.去前臺取下 U 盤
2. 去教室寫 10 分鐘作業
3. 去前臺取下快遞
如果是在單執行緒情況下,JVM、CPU指令集會對其進行優化,比如,按 1->3->2的方式執行,也是沒問題,可以少跑一次前臺,這種叫做指令重排序
代碼重排序會給多執行緒帶來什么問題:
剛才那個例子中,單執行緒情況是沒問題的,優化是正確的,但在多執行緒場景下就有問題了,什么問題呢,可能快遞是在你寫作業的10分鐘內被另一個執行緒放過來的,或者被人變過了,如果指令重排序了,代碼就會是錯誤的,

<4>執行緒不安全問題的解決
(1)synchronized 關鍵字
這里會在下面鎖體系中詳細說
(2)volatile 關鍵字
volatile 關鍵字的作用:
(1)保證可見性
(2)禁止指令重排序,建立記憶體屏障——單例模式說明
(3)不保證原子性
常見的使用場景:一般是讀寫分離的操作,提高性能
(1)寫操作不依賴共享變數,賦值是一個常量(依賴共享變數的賦值不是原子性操作)
(2)作用在讀,寫依賴其他手段(加鎖)
一個volatile的簡單例子:
public class Test {
private static boolean flag = true;
public static void main(String[] args) {
//創建一個執行緒并啟動
new Thread(new Runnable() {
int i=0;
@Override
public void run() {
while(flag){
//這個陳述句底層使用了synchronized,保證了可見性
//System.out.println("=============");
i++;
}
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//即使改了,上面的執行緒flag也不會改,會一直回圈
flag = false;
}
}
6.鎖體系
多執行緒中鎖的作用:保證執行緒的同步
<1>Synchronized加鎖方式
(1)<1>Synchronized的加鎖方式及語法基礎
如何解決上述原子性例子的問題:
是不是只要給房間加一把鎖,A 進去就把門鎖上,其他人是不是就進不來了,這樣就保證了這段代碼的原子性了,有時也把這個現象叫做同步互斥,表示操作是互相排斥的,
synchronized 關鍵字:
(1)作用:對一段代碼進行加鎖操作,讓某一段代碼滿足三個特性:原子性,可見性,有序性
(2)原理:多個執行緒間同步互斥(一段代碼在任意一個時間點,只有一個執行緒執行:加鎖,釋放鎖)
注意: 加鎖/釋放鎖是基于物件來進行加鎖和釋放鎖,不是把代碼鎖了
只有對同一個物件加鎖,才會讓執行緒產生同步互斥的效果:
那么怎樣才叫對同一個物件加鎖呢?
這里t代表類名,t1,t2是 new了兩個t increment是t中的一個方法(是靜態還是實體具體看)

synchronized處加鎖,拋出例外或代碼塊結束釋放鎖

具體程序:

synchronized 多個執行緒n同步互斥:
(1):一個時間只有一個執行緒執行(同步互斥)
(2):競爭失敗的執行緒,不停的在阻塞態和運行態切換(用戶態和內核態切換)
(3)同步執行緒數量越多,性能越低
一個簡單的小例子:
public class SafeThread {
//有一個遍歷COUNT=0;同時啟動20個執行緒,每個執行緒回圈1000次,每次回圈把COUNT++
//等待二十個子執行緒執行完畢之后,再main中列印COUNT的值
//(預期)count=20000
private static int COUNT=0;
//對當前類物件進行加鎖,執行緒間同步互斥
// public synchronized static void increment(){
// COUNT++;
// }
//使用不同的物件加鎖,沒有同步互斥的效果,并發并行
// public static void increment(){
// synchronized (new SafeThread()){
// COUNT++;
// }
// }
public static void main(String[] args) throws InterruptedException {
//盡量同時啟動,不讓new執行緒操作影響
Class clazz=SafeThread.class;
Thread[]threads=new Thread[20];
for (int i = 0; i <20 ; i++) {
threads[i]=new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j <1000 ; j++) {
//給SafeThread物件加一把鎖
synchronized (clazz){
COUNT++;
}
}
}
});
}
for (int i = 0; i <20 ; i++) {
threads[i].start();
}
//讓main執行緒等待20個子執行緒運行完畢
for (int i = 0; i <20 ; i++) {
threads[i].join();
}
System.out.println(COUNT);
}
}
synchronized加鎖的缺點:
a)如果獲取鎖的執行緒由于要等待IO或其他原因(如呼叫sleep方法)被阻塞了,但又沒有釋放鎖,其他執行緒只能干巴巴地等待,此時會影響程式執行效率,
b)只要獲取了synchronized鎖,不管是讀操作還是寫操作,都要上鎖,都會獨占,如果希望多個讀操作可以同時運行,但是一個寫操作運行,無法實作,
(2)Synchronized的原理及實作
1.Monitor機制:
(1)基于monitor物件的監視器:使用物件頭的鎖狀態來加鎖
(2)編譯為位元組碼指令為:1個monitoren+2個monitorexit
多出來的一個monitorexit:如果出現例外,第一個monitorexit無法正確釋放鎖,這個monitorexit進行鎖釋放
例如下列代碼:
public class Test1 {
public Test1() {
}
public static void main(String[] args) {
Class var1 = Test1.class;
synchronized(Test1.class) {
System.out.println("hello");
}
}
}
反編譯:

(3)monitor存在計數器實作synchronized的可重入性:進入+1,退出-1;
(3)JVM對Synchronized的優化
1)對鎖的優化
Synchronized是基于物件頭的鎖狀態來實作的,從低到高:(鎖只能升級不能降級)
(1)無鎖
(2)偏向鎖:對同一個物件多次加鎖(重入)
(3)輕量級鎖:基于CAS實作,同一個時間點,經常只有一個執行緒競爭鎖
(4)重量級鎖:基于系統的mutex鎖,同一個時間點,經常有多個執行緒競爭
特點:mutex是系統級別的加鎖,執行緒會由用戶態切換到內核態,切換的成本比較高(一個執行緒總是競爭失敗,就會不停的在用戶態和內核態之間切換,比較耗費資源,進一步,如果很多個競爭失敗的執行緒,性能就會有很大的影響)
2)鎖粗話
多個synchronized連續執行加鎖,釋放鎖,可以合并為一個
示例:StringBuffer靜態變數,在一個執行緒中多次append(靜態變數屬于方法區,jdk 1.8后是在堆里面,執行緒共享)
public class Test {
private static StringBuffer sb;
public static void main(String[] args) {
sb.append("1").append("2").append("3");
}
}
3)鎖消除
對不會逃逸到其他執行緒的變數,執行加鎖的操作,可以洗掉加鎖
示例:StringBuffer區域變數,在一個執行緒中多次append(區域變數屬于虛擬機堆疊,是執行緒私有的)
public class Test {
public static void main(String[] args) {
StringBuffer sb=new StringBuffer();
sb.append("1");
sb.append("2");
sb.append("3");
}
}
<2>常見的鎖策略及CAS
多執行緒中鎖型別的劃分:
API層面:synchronized加鎖 Lock加鎖
鎖的型別:偏向鎖,輕量級鎖,重量級鎖,自旋鎖,獨占鎖,共享鎖,公平鎖,非公平鎖等等
(1)樂觀鎖和悲觀鎖
樂觀鎖和悲觀鎖的設計思想(和語言是無關的,不是java多執行緒獨有的)
根據使用常見來闡述:
樂觀鎖:同一個時間點,經常只有一個執行緒來操作共享變數,適合使用樂觀鎖
悲觀鎖:同一個時間點,經常有多個執行緒來操作共享變數,適合使用悲觀鎖
樂觀鎖的實作原理:
通過直接操作共享變變數(不會阻塞),通過呼叫的api的回傳值,來知道操作是成功還是失敗的
java多執行緒的實作:基于CAS的方式實作(Compare and Swap)
令:主存中需要操作的變數為V,執行緒A的作業記憶體中,讀入A,修改為N
有另一個執行緒可能對主存中的V進行操作
此時:新的主存中操作的變數令為O,比較執行緒A中的V和此時主存中的O是否相等,如果相等,說明可以將N寫回主存,如果不相等,任務主存中的變數被B執行緒操作過,此時A中的N不寫入主存,執行緒A不做任何事情,

悲觀鎖的實作原理:類似于synchronized加鎖方式
**CAS中可能存在的問題(ABA問題) **
肯主存中原來的V值,被執行緒B加一,再減一,依然滿足上述執行緒A可以寫入N的條件
解決辦法:為主存中的變數加上一個版本好,在上訴A執行緒可寫入的基礎上,再比較一次版本好,即可解決,
CAS在java中是使用unsafe類來完成的,本質上是基于CPU提供的對變數原子性執行緒安全的修改操作
(2)自旋鎖
按照普通加鎖的方式處理,當執行緒在搶鎖失敗之后會進入阻塞狀態,放棄CPU,需要經過很久才能被再次調度,所以,引入讀寫鎖,當鎖競爭失敗之后,只需要很短時間,鎖就能再次被釋放,此時,讓競爭失敗的執行緒,進入自旋,不在用戶態和內核態之間切換,只要沒搶到鎖,就死等,
類似以下代碼:
<1>.無條件的自選:
while(搶鎖(lock)==失敗{}
自旋鎖的缺陷:如果之前的假設(鎖很快就能被釋放)沒有滿足,那么進入自旋的執行緒就一直在消耗CPU的資源,長期在做無用功
<2>.有條件的自旋:
如可中斷的自旋:自旋時執行緒判斷中斷標志位后再執行,或者限制自旋的次數,限制自旋的時間
自旋鎖,悲觀樂觀鎖,CAS的總結:
<1>.悲觀鎖是執行緒先加鎖,之后再修改變數的操作
<2>.樂觀鎖是執行緒直接嘗試修改變數(不會阻塞),在java多執行緒中是基于CAS 實作的,
<3>.CAS
概念:Compare and Swap比較并交換
實作/原理:基于unsafe來實作,本質上是基于CPU提供的介面保證執行緒安全修改變數,
使用(V,O,N):V為記憶體地址中存放的實際值,O為預期的值(舊值),N為更新的值(新值)
可能出現的問題:ABA問題(引入版本號解決)
<4>.自旋+CAS
適用的場景:同一個時間點,常常只有一個執行緒進行操作
不適應的場景:1.同一個時間點,常常有多個執行緒進行操作
2.CAS的操作時間時間太長,給了其他執行緒操作共享變數的機會,那么CAS的成功率會很低,經常做無用功
自旋的缺陷:執行緒一直處于運行態,會很耗費CPU的資源
(3)可重入鎖
允許同一個執行緒多次獲取同一把鎖
java中只要以Reentrant開頭命名的鎖都是可重入的鎖,現有的jdk提供的lock的實作類和synchronized加鎖,都是可重入鎖
例如:
public class Test2 {
public static synchronized void t1(){
t2();
}
public static synchronized void t2(){
}
public static void main(String[] args) {
t1();
}
}
<3>Lock體系

(1)Lock介面
1)使用Lock鎖實作執行緒同步
上代碼!
public class AccountRunnable implements Runnable {
private Account account = new Account();
//買一把鎖
Lock lock = new ReentrantLock(); //Re-entrant-Lock 可重入鎖
@Override
public void run() {
//此處省略300句
try{
//上鎖
lock.lock();
//判斷余額是否足夠,夠,取之;不夠,不取之;
if(account.getBalance()>=400){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
method1();
//取之
account.withDraw(400);
//輸出資訊
System.out.println(Thread.currentThread().getName()+
"取款成功,現在的余額是"+account.getBalance());
}else{
System.out.println("余額不足,"+Thread.currentThread().getName()
+"取款失敗,現在的余額是" +account.getBalance());
}
}finally {
//解鎖
lock.unlock();
}
//此處省略100句
}
}
這里要注意:釋放鎖時,要考慮是否出現例外,和上面synchronized加鎖相同,要進行兩次鎖釋放,這里將鎖放在finally代碼塊中
2)Lock加鎖的四種方式
形象記憶:男生追女生
1.lock():一直表白,直到成功
lock()方法是平常使用得最多的一個方法,就是用來獲取鎖,如果鎖已被其他執行緒獲取,則進行等待,
2.tryLock():表白一次,失敗就放棄
tryLock()方法是有回傳值的,它表示用來嘗試獲取鎖,如果獲取成功,則回傳true,如果獲取失敗(即鎖已被其他執行緒獲取),則回傳false,也就說這個方法無論如何都會立即回傳,拿不到鎖時不會一直在那等待,
3.tryLock(long time, TimeUnit unit) 在一定的時間內持續表白,如果時間到了則放棄
tryLock(long time, TimeUnit unit)方法和tryLock()方法是類似的,只不過區別在于這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就回傳false,如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則回傳true,
4.lockInterruptibly() 一直表白,當被通知她有男朋友了,才放棄
lockInterruptibly()方法比較特殊,當通過這個方法去獲取鎖時,如果執行緒正在等待獲取鎖,則這個執行緒能夠回應中斷,即中斷執行緒的等待狀態,也就使說,當這個執行緒使用lockInterruptibly()獲取鎖,當被interrupt中斷時,才會停止競爭鎖
(2)AQS簡單認識
AQS: AbstractQuenedSynchronizer抽象的佇列式同步器,是除了java自帶的synchronized關鍵字之外的鎖機制,這個類在java.util.concurrent.locks包.
AQS的核心思想是: 如果被請求的共享資源空閑,則將當前請求資源的執行緒設定為有效的作業執行緒,并將共享資源設定為鎖定狀態,如果被請求的共享資源被占用,那么就需要一套執行緒阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH佇列鎖實作的,即將暫時獲取不到鎖的執行緒加入到佇列中,
AQS的實作方式:

如圖示,AQS維護了一個volatile int state和一個FIFO執行緒等待佇列,多執行緒爭用資源被阻塞的時候就會進入這個佇列,state就是共享資源
AQS 定義了兩種資源共享方式:
1.Exclusive:獨占,只有一個執行緒能執行,如ReentrantLock
2.Share:共享,多個執行緒可以同時執行,如Semaphore、CountDownLatch、ReadWriteLock,CyclicBarrier
(3)ReentrantLock
1)ReentrantLock基本概念
ReentrantLock,意思是“可重入鎖”,ReentrantLock是唯一實作了Lock介面的非內部類,并且ReentrantLock提供了更多的方法,
ReentrantLock鎖在同一個時間點只能被一個執行緒鎖持有,
ReentraantLock是通過一個FIFO的等待佇列來管理獲取該鎖所有執行緒的,在“公平鎖”的機制下,執行緒依次排隊獲取鎖;而“非公平鎖”在鎖是可獲取狀態時,不管自己是不是在佇列的開頭都會獲取鎖,
當單個執行緒或執行緒交替執行時,他與佇列無關,只會在jdk級別解決,性能高
2)自己實作一個簡單的ReentrantLock
原理:自旋+park–unpark+CAS
public class Test2 {
volatile int status=0;
Queue parkQueue;//集合 陣列 list
void lock(){
while(!compareAndSet(0,1)){
//這里不能用sleep或yield實作
//sleep無法確定睡眠的時間
//yield只能用于兩個執行緒競爭,當有多個執行緒之后,t1搶不到鎖,yield會讓出cpu,但是可能下一次cpu還是調t1
park();
}
unlock();
}
void unlock(){
lock_notify();
}
void park(){
//將當期執行緒加入到等待佇列
parkQueue.add(currentThread);
//將當期執行緒釋放cpu 阻塞 睡眠
releaseCpu();
}
void lock_notify(){
//status=0
//得到要喚醒的執行緒頭部執行緒
Thread t=parkQueue.header();
//喚醒等待執行緒
unpark(t);
}
}
3)ReentrantLock部分原始碼分析
ReentrantLock鎖分為公平鎖和非公平鎖(創建不加引數時默認非公平鎖)
ReentrantLock提供了兩個構造器:
//非公平鎖
public ReentrantLock() {
sync = new NonfairSync();
}
//公平鎖
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
ReentrantLock的lock方式:

非公平鎖:
呼叫lock方法:
final void lock() {
if (compareAndSetState(0, 1))//首先用一個CAS操作,判斷state是否是0(表示當前鎖未被占用)
setExclusiveOwnerThread(Thread.currentThread());//設定當前占有鎖的執行緒為該執行緒
else
acquire(1);
}
首先用一個CAS操作,判斷state是否是0(表示當前鎖未被占用),如果是0則把它置為1,并且設定當前執行緒為該鎖的獨占執行緒,表示獲取鎖成功,當多個執行緒同時嘗試占用同一個鎖時,CAS操作只能保證一個執行緒操作成功,剩下的只能乖乖的去排隊,
“非公平”即體現在這里,如果占用鎖的執行緒剛釋放鎖,state置為0,而排隊等待鎖的執行緒還未喚醒時,新來的執行緒就直接搶占了該鎖,那么就“插隊”了,
下面說說acquire的程序
public final void acquire(int arg) {
//首先看看自己要不要排隊,如果不用排隊,獲取鎖,要排隊,加入AQS佇列
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
(1)嘗試去獲取鎖(看看自己要不要排隊)
非公平鎖tryAcquire的流程是:檢查state欄位,若為0,表示鎖未被占用,那么嘗試占用,若不為0,檢查當前鎖是否被自己占用,若被自己占用,則更新state欄位,表示重入鎖的次數,如果以上兩點都沒有成功,則獲取鎖失敗,回傳false,
tryAcquire(arg)
final boolean nonfairTryAcquire(int acquires) {
//獲取當前執行緒
final Thread current = Thread.currentThread();
//獲取state變數值
int c = getState();
if (c == 0) { //沒有執行緒占用鎖
if (compareAndSetState(0, acquires)) {
//占用鎖成功,設定獨占執行緒為當前執行緒
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) { //當前執行緒已經占用該鎖 重入鎖
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 更新state值為新的重入次數
setState(nextc);
return true;
}
//獲取鎖失敗
return false;
}
(2)入隊
根據java運算子短路,如果不需要排隊,方法直接回傳,如果需要排隊,進入addWaiter方法
公平鎖:
公平鎖和非公平鎖不同之處在于,公平鎖在獲取鎖的時候,不會先去檢查state狀態,而是直接執行aqcuire(1)
4)ReadWriteLock鎖
ReadWriteLock也是一個介面,在它里面只定義了兩個方法:
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
一個用來獲取讀鎖,一個用來獲取寫鎖,也就是說將檔案的讀寫操作分開,分成2個鎖來分配給執行緒,從而使得多個執行緒可以同時進行讀操作,
ReadWriteLock是一個介面,ReentrantReadWriteLock是它的實作類,該類中包括兩個內部類ReadLock和WriteLock,這兩個內部類實作了Lock介面,
認識ReadWriteLock鎖
public class TestLock {
public static void main(String[] args) {
//默認也是非公平鎖 也是可重入鎖
ReadWriteLock rwl = new ReentrantReadWriteLock();
//多次回傳的都是同一把讀鎖 同一把寫鎖
Lock readLock = rwl.readLock();
Lock readLock2 = rwl.readLock();
Lock writeLock = rwl.writeLock();
readLock.lock();
readLock.unlock();
System.out.println(readLock==readLock2);
}
}
注意:從結果中看到,從一個ReadWriteLock中多次獲取的ReadLock、WriteLock是同一把讀鎖,同一把寫鎖,
<4>.Lock鎖和同步鎖(synchronized)的區別與對比

對比:
1.概念:都是用于加鎖和保證執行緒安全的手段,知識Lock提供鎖物件,而synchronized是基于物件頭來實作的加鎖
2.語法:locak顯示的加鎖,釋放鎖;而synchronized是內建鎖(JVM內部構建的鎖),隱式的加鎖和釋放鎖,lock使用上來說更加靈活
3.功能:lock提供了多種獲取鎖的方式(獲取鎖,非阻塞式的獲取鎖,可中斷的獲取鎖,超時獲取鎖等),而synchronized只有一種方式:競爭鎖(競爭失敗,阻塞)
4.性能:同一個時間點,競爭鎖的執行緒數量越多,synchronized性能下降的越快(競爭失敗的執行緒,不停的再阻塞態與喚醒態之間切換,用戶態與內核態之間的切換),使用lock效果好,
<5>死鎖
先上代碼:
package threadadvanced.lesson1;
class Pen {
private String pen = "筆" ;
public String getPen() {
return pen;
}
}
class Book {
private String book = "本" ;
public String getBook() {
return book;
}
}
public class DeadLock {
private static Pen pen = new Pen() ;
private static Book book = new Book() ;
public static void main(String[] args) {
new DeadLock().deadLock();
}
public void deadLock() {
Thread thread1 = new Thread(new Runnable() { // 筆執行緒
@Override
public void run() {
synchronized (pen) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+" :我有筆,我就不給你");
synchronized (book) {
System.out.println(Thread.currentThread()+" :把你的本給我!");
}
}
}
},"Pen") ;
Thread thread2 = new Thread(new Runnable() { // 本子執行緒
@Override
public void run() {
synchronized (book) {
System.out.println(Thread.currentThread()+" :我有本子,我就不給你!");
synchronized (pen) {
System.out.println(Thread.currentThread()+" :把你的筆給我!");
}
}
}
},"Book") ;
thread1.start();
thread2.start();
}
}
出現死鎖:

jconsole檢查死鎖:

1.死鎖出現的原因:
至少兩個執行緒,互相持有對方需要的資源沒有釋放,再次申請對方以及持有的資源
2.出現死鎖的后果:
執行緒互相阻塞等待地方的資源,會一直處于阻塞等待的狀態
3.如何檢測死鎖:
使用jdk工具:jconsole(查看執行緒)---->jstack
4.解決死鎖的方法:
(1)資源一次性分配(破壞請求與保持條件)
(2)在滿足一定條件的時候,主動釋放資源
(3)資源的有序分配:系統為每一類資源賦予一個編號,每個執行緒按照編號遞請求資源,釋放則相反
7.多執行緒案例
<1>生產者消費者問題
示例:
面包店
10個生產者,每個每次生產3個
20個消費者,每個每次消費一個
進階版需求
面包師傅每個最多生產30次,面包店每天生產10303=900個面包
消費者也不是一直消費,把900個面包消費完結束
隱藏資訊:面包店每天生產面包的最大數量為900個
消費者把900個面包消費完結束
代碼示例:
/**
* 面包店
* 10個生產者,每個每次生產3個
* 20個消費者,每個每次消費一個
*
* 進階版需求
* 面包師傅每個最多生產30次,面包店每天生產10*30*3=900個面包
* 消費者也不是一直消費,把900個面包消費完結束
*
* 隱藏資訊:面包店每天生產面包的最大數量為900個
* 消費者把900個面包消費完結束
*/
public class AdvancedBreadShop {
//面包店庫存數
private static int COUNT;
//面包店生產面包的總數,不會消費的
private static int PRODUCE_NUMBER;
public static class Consumer implements Runnable{
private String name;
public Consumer(String name) {
this.name = name;
}
@Override
public void run() {
try {
while (true){
synchronized (AdvancedBreadShop.class){
if(PRODUCE_NUMBER==900&&COUNT==0){
System.out.println("今天面包已經賣完了");
break;
}else {
if(COUNT==0){
AdvancedBreadShop.class.wait();
}else {
System.out.printf("%s消費了一個面包\n",this.name);
COUNT--;
AdvancedBreadShop.class.notifyAll();
Thread.sleep(100);
}
}
}
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static class Producer implements Runnable{
private String name;
public Producer(String name) {
this.name = name;
}
@Override
public void run() {
try {
//生產者生產30次,結束回圈
for(int i=0;i<=30;i++) {
synchronized (AdvancedBreadShop.class){
if(i==30){
System.out.println("今天面包生產完了");
break;
}else {
if(COUNT>97){
AdvancedBreadShop.class.wait();
}else {
COUNT=COUNT+3;
PRODUCE_NUMBER=PRODUCE_NUMBER+3;
System.out.printf("%s生產了三個面包\n",this.name);
AdvancedBreadShop.class.notifyAll();
Thread.sleep(100);
}
}
}
Thread.sleep(100);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
Thread[] Consumers=new Thread[20];
Thread[] Producers=new Thread[10];
for (int i = 0; i <20 ; i++) {
Consumers[i]=new Thread(new Consumer(String.valueOf(i)));
}
for (int i = 0; i <10 ; i++) {
Producers[i]=new Thread(new Producer(String.valueOf(i)));
}
for (int i = 0; i <20 ; i++) {
Consumers[i].start();
}
for (int i = 0; i <10 ; i++) {
Producers[i].start();
}
}
}
<2>單例模式
1.餓漢式(執行緒安全)
public class Singleton {
private static Singleton instance=new Singleton();//物件的創建是類加載的時候進行
private Singleton() {}
public static Singleton getInstance(){
return instance;
}
}
缺點:
<1>類加載時就創建了物件,但是在很多場景下,獲取物件時,才需要實體化物件,這樣才能節省記憶體空間,節省new物件的操作時間
<2>new物件操作,沒有類加載,需要先執行類加載,在執行物件初始化的作業(成員變數,示例代碼快,構造方法),如果執行時,這些拋例外了,那么以后都不能再使用
2.懶漢式(單執行緒版)
public class Singleton {
private static Singleton instance=null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
instance=new Singleton();
}
return instance;
}
}
存在共享變數instanc,執行緒不安全
<3>懶漢式(多執行緒版,性能較低)
public class Singleton {
private static Singleton instance=null;
private Singleton(){}
public synchronized static Singleton getInstance(){
if(instance == null){
instance=new Singleton();
}
return instance;
}
}
<4>
基于單例模式下的懶漢模式(雙重校驗鎖實作)(多執行緒版,二次判斷,效率高)
代碼示例:
public class Singleton {
//volatile關鍵字修飾,保證的可見性和代碼的順序性
private static volatile Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
//判斷instance是否為空,競爭鎖的條件
if (instance == null) {
//保證執行緒安全,為Singleton.class加鎖
synchronized (Singleton.class) {
//再次判斷instance是否為空,防止多個執行緒進入第一個if后
//對synchronized鎖競爭失敗進入阻塞狀態后,再次進入運行態時
//new了多個Singleton,不符合單例模式
//保證執行緒安全
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
<3>阻塞式佇列
生產者消費者模式就是通過一個容器來解決生產者和消費者的強耦合問題,生產者和消費者彼此之間不直接通訊,而通過阻塞佇列來進行通訊,所以生產者生產完資料之后不用等待消費者處理,直接扔給阻塞佇列,消費者不找生產者要資料,而是直接從阻塞佇列里取,阻塞佇列就相當于一個緩沖區,平衡了生產者和消費者的處理能力,這個阻塞佇列就是用來給生產者和消費者解耦的,
阻塞式佇列代碼實作:
/**
* 實作阻塞佇列
* 1.執行緒安全問題:在多執行緒情況下,put,take不具有原子性,4個屬性,不具有可見性
* 2.put操作:如果存滿了,需要阻塞等待,take操作:如果是空,阻塞等待
* @param <T>
*/
public class MyBlockingQueue <T>{
//使用陣列實作回圈佇列
private Object[] queue;
//存放元素的索引
private int putIndex ;
//取元素的索引
private int takeIndex;
//當前存放元素的數量
private int size;
public MyBlockingQueue(int len){
queue=new Object[len];
}
//存放元素,需要考慮:
//1.putIndex超過陣列長度
//2.size達到陣列最大長度
public synchronized void put(T e) throws InterruptedException {
//不滿足執行條件時,一直阻塞等待
//當阻塞等待都被喚醒并再次競爭成功物件鎖,回復往下執行時,條件可能被其他執行緒修改
while (size==queue.length){
this.wait();
}
//存放到陣列中放元素的索引位置
queue[putIndex]=e;
putIndex=(putIndex+1)%queue.length;
size++;
notifyAll();
}
//取元素
public synchronized T take() throws InterruptedException {
while (size==0){
this.wait();
}
T t= (T) queue[takeIndex];
queue[takeIndex]=null;
takeIndex=(takeIndex+1)%queue.length;
size--;
notifyAll();
return t;
}
public int size(){
return size;
}
public static void main(String[] args) {
MyBlockingQueue<Integer>queue=new MyBlockingQueue<>(10);
//多執行緒的除錯方式:1.寫列印陳述句 2.jconsole
for (int i = 0; i <3 ; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
for (int j = 0; j <100 ; j++) {
queue.put(j);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
for (int i = 0; i <3 ; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
while (true){
int t= queue.take();
System.out.println(Thread.currentThread().getName()+":"+t);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
}
}
<4>執行緒池
執行緒池最大的好處就是減少每次啟動、銷毀執行緒的損耗
import java.util.concurrent.*;
public class ThreadPoolExecutorTest {
public static void main(String[] args) {
//以快遞公司,快遞員,快遞業務為模型
ThreadPoolExecutor pool=new ThreadPoolExecutor(
5,//核心執行緒數---->正式員工數
10,//最大執行緒數-->正式員工+臨時員工
60,//臨時工的最大等待時間(數量)
TimeUnit.SECONDS,//idle執行緒的空閑時間(時間單位)-->臨時工最大的存活時間,超過就解雇
new LinkedBlockingQueue<>(),//阻塞佇列,任務存放的地方--->快遞倉庫
new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(new Runnable() {
@Override
public void run() {
//r物件是執行緒池內部封裝過的作業任務類(Worker),會一直回圈等待的方式從阻塞佇列中拿取任務并執行
//所以不能呼叫r.run();方法
System.out.println(Thread.currentThread().getName()+"開始執行了");
}
});
}
},//創建執行緒的工廠類 執行緒池創建執行緒時,呼叫該工廠類的方法創建執行緒(滿足該工廠創建執行緒的要求)
//---->對應招聘員工的標準
/**
* 拒絕策略:達到最大執行緒數且阻塞佇列已滿,采取拒絕策略
* AbortPolicy:直接拋出RejectedExecutionException(不提供handler時的默認策略)
* CallerRunsPolicy:誰(某個執行緒)交給我(執行緒池)的任務,我拒絕執行,由誰自己去執行
* DiscardPolicy:交給我的任務直接丟棄掉
* DiscardOldestPolicy:阻塞佇列中最舊的任務丟棄
*/
new ThreadPoolExecutor.AbortPolicy()//拒絕策略-->達到最大執行緒數,且阻塞佇列已滿,采取的拒絕策略
);//執行緒池創建以后,只要有任務們就會自動執行
for (int i = 0; i <20 ; i++) {
//執行緒池執行任務:execute方法,submit方法--->提交執行一個任務
//區別:回傳值不同
pool.execute(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
//執行緒池有4個快捷的創建方式(實際作業不使用,作為面試了解)
//實際作業需要使用ThreadPoolExecutor,構造引數是我們自己指定,比較靈活
ExecutorService pool2=Executors.newSingleThreadExecutor();//創建單執行緒池
ExecutorService pool3=Executors.newCachedThreadPool();//快取的執行緒池
ExecutorService pool5=Executors.newFixedThreadPool(4);//固定大小執行緒池
ScheduledExecutorService pool4=Executors.newScheduledThreadPool(4);//計劃任務執行緒池
//兩秒中之后執行這個任務
pool4.schedule(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
}, 2, TimeUnit.SECONDS);
//一直執行任務
pool4.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
}, 2, 1,TimeUnit.SECONDS);//比如一個腦子,兩秒后開始叫我,然后每隔一秒叫我一次
}
}
8.JUC包
共享鎖:
<1>Semaphore(信號量 )
兩個重要的API
Semaphore semaphore=new Semaphore(0);
semaphore.acquire();//需要/占用一個信號量
semaphore.acquire(10);// 需要/占用10個型號量
semaphore.release();//釋放一個信號量
semaphore.release(10);//釋放是個信號量
兩種使用場景:
1.當一定量執行緒執行完畢后,主執行緒才能執行
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(0);//初始時沒有執行緒釋放,這里設定為0
for (int i = 0; i <10 ; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName()+"執行緒已經執行完畢,釋放資源");
semaphore.release();//釋放一個信號量
}).start();
}
try {
semaphore.acquire(10);//需要信號量達到10,才執行
System.out.println("main執行緒執行完畢");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
2.有限資源的爭奪
十個執行緒搶五個有限資源
public class SemaphoreDemo2 {
//有限資源的爭奪 這里定義5個有限資源
public static void main(String[] args) {
Semaphore semaphore=new Semaphore(5);//5個有限資源
for (int i = 0; i < 10; i++) {//10個執行緒搶5個資源
new Thread(()->{
try {
semaphore.acquire();//占用一個資源
System.out.println(Thread.currentThread().getName()+"占用了一個資源");
Thread.sleep(1000);
semaphore.release();//釋放了一個資源
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
}
}
<2>CountDownLatch
兩個重要API
CountDownLatch countDownLatch=new CountDownLatch(10);//10個執行緒
countDownLatch.countDown();//latch初始為10,呼叫該方法減一
countDownLatch.await();//一直等待,latch減到0時釋放
使用場景:
和上面信號量第一個一樣,能用CountDownLatch解決的,信號量也可以
等待10個執行緒執行完畢,再執行主執行緒
public class CountDownLatchDemo {
//案例:main執行緒在10個子執行緒執行完畢后才能執行
public static void main(String[] args) {
CountDownLatch countDownLatch=new CountDownLatch(10);//10個執行緒
for (int i = 0; i <10 ; i++) {
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"已經執行");
countDownLatch.countDown();//Latch減一
}).start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主執行緒執行完畢");
}
}
<3>CyclicBarrier(回圈柵欄)

CyclicBarrier cyclicBarrier=new CyclicBarrier(4);//初始化parties數
cyclicBarrier.await();//當await的執行緒小于4時,這些執行緒全部等待,達到4時,一起釋放
cyclicBarrier.reset();// 將parties回歸初始狀態
示例:
public class CyclicBarrierDemo {
//回圈柵欄
public static void main(String[] args) {
CyclicBarrier cyclicBarrier=new CyclicBarrier(4);
for (int i = 0; i <10 ; i++) {
new Thread(()->{
try {
cyclicBarrier.await();
Thread.sleep(1000);
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"已經執行");
}).start();
}
}
}
<4>ConcurrentHashMap
(1)ConcurrentHashMap加鎖方式

結點結構:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
1.底層的資料結構和HashMap jdk1.8版本一樣,都是基于陣列+鏈表+紅黑樹
2.支持多執行緒的并發操作,實作原理:CAS+Synchronized保證并發更新
3.put方法存盤元素時,通過key的hashCode計算出相應陣列的索引,如果沒有Node,CAS自旋直到插入成功;如果存在Node,則使用synchronized 鎖住該Node元素(鏈表或者紅黑樹)
4.鍵,值迭代器為弱一致性迭代器,創建迭代器之后,可以對元素進行更新
5.讀操作沒有加鎖,value是volatile修飾的,保證了可見性,所以是安全的
6.讀寫分離可以提高效率:多執行緒多不同的Node/Segment的插入/洗掉操作是可以并發,并行的,多同一個Node/Segment的作用是互斥的,讀操作都是無鎖操作,可以并發,并行執行
7.1.7對一段加鎖,使用ReentrantLock加鎖,1.8使用CAS和Synchronized加鎖
(2)fail-fast和fail-safe
HashMap的迭代器時fali-fast快速失敗的迭代器(強一致性的),
ConcurrentHashMap等執行緒安全的資料結構,使用的都是fail-safe安全失敗的迭代器(若一致性)
再fail-fast迭代器遍歷的時候,如果發生了修改(插入,洗掉,修改),那么再次遍歷下一個元素時,就會報錯,
而fail-safe迭代器,當一個位置進行了修改(插入,洗掉,修改)操作時,另外一個位置也可以進行,不會報錯.
9.ThreadLocal
<1>基礎API
常用API:

案例:
public class ThreadLocalDemo {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
ThreadLocalDemo demo=new ThreadLocalDemo();
for (int i = 0; i <10 ; i++) {
Thread thread=new Thread(()->{
demo.setContent(Thread.currentThread().getName()+"的資料");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---->"+demo.getContent());
});
thread.setName("執行緒"+i);
thread.start();
}
}
}
這里的content是這十個執行緒的共享變數,可能會出現以下結果:

而使用ThreadLocal時,就能實作content多個執行緒隔離
public class ThreadLocalDemo {
private String content;
ThreadLocal<String> threadLocal=new ThreadLocal<>();
public String getContent() {
return threadLocal.get();
}
public void setContent(String content) {
threadLocal.set(content);
}
public static void main(String[] args) {
ThreadLocalDemo demo=new ThreadLocalDemo();
for (int i = 0; i <10 ; i++) {
Thread thread=new Thread(()->{
demo.setContent(Thread.currentThread().getName()+"的資料");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"---->"+demo.getContent());
});
thread.setName("執行緒"+i);
thread.start();
}
}
}

<2>ThreadLocal和synchronized的區別

<3>ThreadLocal的內部結構

JDK早期的設計是ThreadLocal維護Thread,只有一個ThreadLocalMap,將Thread作為Map的key,存盤的值作為Value
而JDK8的設計是:每個執行緒都有一個ThreadLocalMap,Thread維護,ThreadLocal物件本身作為Key,值作為Value存盤
JDK8這樣設計的好處:
1.早期的設計時,是ThreadLocal維護ThreadLocalMap,有多少個執行緒就有多少個Entry,較多
而JDK8這樣設計,減少了Entry個數
2.當Thread銷毀時,ThreadLocalMap也會隨之銷毀,減少了記憶體的使用
ThreadLocalMap是ThreadLocal的內部類,沒有實作Map介面,用獨立的方式實作了Map的功能,其內部的Entry也是獨立實作的
<4>ThrealLocal記憶體泄漏
弱參考造成記憶體泄漏:

如圖:
1.弱參考原因: 假設ThreadLocal Ref用完被回收了,而ThreadLoca被弱參考Entry中的Key指向,也會被回收,此時Key就變為了null,但是如果沒有手動洗掉Entry以及CurrentThread依然運行的前提下,也存在這強參考連threadLocal ->currentThread->threadLocalMap->Entry->value,而value不會被回收,但key的值為null,所以value永遠都不會被訪問到,導致記憶體泄漏
2.解決記憶體泄漏:
<1> 沒有手動洗掉這個Entry:只要在使用完ThreadLocal,呼叫其remove方法洗掉對應的Entry,就能避免記憶體泄漏
<2>CurrentThread依然運行:由于ThreadLocalMap是Thread的一個屬性,被當前執行緒所參考,所以它的生命周期和Thread一樣長,那么在使用完ThreadLocal情況下,如果當前Thread也隨之指向結束,那么ThreadLocalMap自然也會被GC所回收,從根源上避免了記憶體泄漏
**綜上:**ThreadLocal記憶體泄漏的根源是:由于ThreadLocalMap的生命周期和Thread一樣長,如果沒有手動洗掉對應的key就會導致記憶體泄漏
10.Java多執行緒相關面試題
<1>、volatile、ThreadLocal的使用場景和原理;
volatile原理
volatile變數進行寫操作時,JVM 會向處理器發送一條 Lock 前綴的指令,將這個變數所在緩 存行的資料寫會到系統記憶體,
Lock 前綴指令實際上相當于一個記憶體屏障(也成記憶體柵欄),它確保指令重排序時不會把其 后面的指令排到記憶體屏障之前的位置,也不會把前面的指令排到記憶體屏障的后面;即在執行到內 存屏障這句指令時,在它前面的操作已經全部完成,

volatile的適用場景
1)狀態標志,如:初始化或請求停機
2)一次性安全發布,如:單列模式
3)獨立觀察,如:定期更新某個值
4)“volatile bean” 模式
- 開銷較低的“讀-寫鎖”策略,如:計數器
ThreadLocal原理
ThreadLocal是用來維護本執行緒的變數的,并不能解決共享變數的并發問題,ThreadLocal是 各執行緒將值存入該執行緒的map中,以ThreadLocal自身作為key,需要用時獲得的是該執行緒之前 存入的值,如果存入的是共享變數,那取出的也是共享變數,并發問題還是存在的,

ThreadLocal的適用場景
場景:資料庫連接、Session管理
<2>、ThreadLocal什么時候會出現OOM的情況?為什么?
ThreadLocal變數是維護在Thread內部的,這樣的話只要我們的執行緒不退出,物件的參考就會 一直存在,當執行緒退出時,Thread類會進行一些清理作業,其中就包含ThreadLocalMap, Thread呼叫exit方法如下:

ThreadLocal在沒有執行緒池使用的情況下,正常情況下不會存在記憶體泄露,但是如果使用了執行緒 池的話,就依賴于執行緒池的實作,如果執行緒池不銷毀執行緒的話,那么就會存在記憶體泄露,
<3>、synchronized、volatile區別
-
volatile主要應用在多個執行緒對實體變數更改的場合,重繪主記憶體共享變數的值從而使得各個 執行緒可以獲得最新的值,執行緒讀取變數的值需要從主存中讀取;synchronized則是鎖定當前變 量,只有當前執行緒可以訪問該變數,其他執行緒被阻塞住,另外,synchronized還會創建一個內 存屏障,記憶體屏障指令保證了所有CPU操作結果都會直接刷到主存中(即釋放鎖前),從而保證 了操作的記憶體可見性,同時也使得先獲得這個鎖的執行緒的所有操作
-
volatile僅能使用在變數級別;synchronized則可以使用在變數、方法、和類級別的, volatile不會造成執行緒的阻塞;synchronized可能會造成執行緒的阻塞,比如多個執行緒爭搶 synchronized鎖物件時,會出現阻塞,
-
volatile僅能實作變數的修改可見性,不能保證原子性;而synchronized則可以保證變數的 修改可見性和原子性,因為執行緒獲得鎖才能進入臨界區,從而保證臨界區中的所有陳述句全部得到 執行,
-
volatile標記的變數不會被編譯器優化,可以禁止進行指令重排;synchronized標記的變數 可以被編譯器優化,
8、synchronized鎖粒度、模擬死鎖場景;
synchronized:具有原子性,有序性和可見性
粒度:物件鎖、類鎖
<4>ThreadLocal的作用以及為什么發生記憶體泄漏和解決方法
作用:存放執行緒的本地變數,實作執行緒之間的隔離,它內部有靜態內部類ThreadlocalMap,Entry,實作了和Map類似的功能,單沒用實作Map介面,每個執行緒都有一個ThreadLocalMap的副本,Threadlocal繼承了弱參考
記憶體泄漏:
弱參考造成記憶體泄漏:

如圖:
1.弱參考原因: 假設ThreadLocal Ref用完被回收了,而ThreadLoca被弱參考Entry中的Key指向,也會被回收,此時Key就變為了null,但是如果沒有手動洗掉Entry以及CurrentThread依然運行的前提下,也存在這強參考連threadLocal ->currentThread->threadLocalMap->Entry->value,而value不會被回收,但key的值為null,所以value永遠都不會被訪問到,導致記憶體泄漏
2.解決記憶體泄漏:
<1> 沒有手動洗掉這個Entry:只要在使用完ThreadLocal,呼叫其remove方法洗掉對應的Entry,就能避免記憶體泄漏
<2>CurrentThread依然運行:由于ThreadLocalMap是Thread的一個屬性,被當前執行緒所參考,所以它的生命周期和Thread一樣長,那么在使用完ThreadLocal情況下,如果當前Thread也隨之指向結束,那么ThreadLocalMap自然也會被GC所回收,從根源上避免了記憶體泄漏
**綜上:**ThreadLocal記憶體泄漏的根源是:由于ThreadLocalMap的生命周期和Thread一樣長,如果沒有手動洗掉對應的key就會導致記憶體泄漏
<5>.如何中斷一個執行緒
1.使用interrupt方法,修改中斷標志位
2.可以在外部執行緒指定一個條件變數(比如主執行緒),使用volatile修飾,然后在子執行緒中回圈檢查這個變數
Java小白到初中級必備:Java基礎知識必備資源
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/292779.html
標籤:其他
上一篇:尋找那些神奇的自冪數---C語言
