java多執行緒&&併發面試108問(上)
歡迎關注
CSDN:程式設計師小羊
微信公眾號:程式設計師小羊
部落格園:程式設計師小羊
目錄
- 1、Java中實現多執行緒有幾種方法
- 2、繼承 Thread 類
- 3、實現 Runnable 介面。
- 4、ExecutorService、 Callable、 Future 有返回值執行緒
- 5、基於執行緒池的方式
- 6、4 種執行緒池
- 7、如何停止一個正在執行的執行緒
- 8、notify()和notifyAll()有什麼區別?
- 9、sleep()和wait() 有什麼區別?
- 10、volatile 是什麼?可以保證有序性嗎?
- 11、Thread 類中的start() 和 run() 方法有什麼區別?
- 12、為什麼wait, notify 和 notifyAll這些方法不在thread類裡面?
- 13、為什麼wait和notify方法要在同步塊中呼叫?
- 14、Java中interrupted 和 isInterruptedd方法的區別?
- 15、Java中synchronized 和 ReentrantLock 有什麼不同?
- 16、有三個執行緒T1,T2,T3,如何保證順序執行?
- 17、SynchronizedMap和ConcurrentHashMap有什麼區別?
- 18、什麼是執行緒安全
- 19、Thread類中的yield方法有什麼作用?
- 20、Java執行緒池中submit() 和 execute()方法有什麼區別?
- 21、說一說自己對於 synchronized 關鍵字的瞭解
- 22、說說自己是怎麼使用 synchronized 關鍵字,在專案中用到了嗎synchronized關鍵字最主要的三種使用方式
- 23、什麼是執行緒安全?Vector是一個執行緒安全類嗎?
- 24、volatile關鍵字的作用?
- 25、簡述一下你對執行緒池的理解
- 26、執行緒生命週期(狀態)
- 27、新建狀態(NEW)
- 28、就緒狀態(RUNNABLE)
- 29、執行狀態(RUNNING)
- 30、阻塞狀態(BLOCKED)
- 31、執行緒死亡(DEAD)
- 32、終止執行緒 4 種方式
- 33、start 與 run 區別
- 34、JAVA 後臺執行緒
- 35、什麼是樂觀鎖
- 36、什麼是悲觀鎖
- 37、什麼是自旋鎖
- 38、Synchronized 同步鎖
- 39、ReentrantLock
- 40、Condition 類和 Object 類鎖方法區別區別
- 41、tryLock 和 lock 和 lockInterruptibly 的區別
- 42、Semaphore 訊號量
- 43、Semaphore 與 ReentrantLock 區別
- 44、可重入鎖(遞迴鎖)
- 45、公平鎖與非公平鎖
- 46、ReadWriteLock 讀寫鎖
- 48、重量級鎖(Mutex Lock)
- 49、輕量級鎖
- 50、偏向鎖
- 51、分段鎖
- 52、鎖優化
1、Java中實現多執行緒有幾種方法
繼承Thread類;
實現Runnable介面;
實現Callable介面通過FutureTask包裝器來建立Thread執行緒;
使用ExecutorService、Callable、Future實現有返回結果的多執行緒(也就是使用了ExecutorService來管理前面的三種方式)。
2、繼承 Thread 類
Thread 類本質上是實現了 Runnable 介面的一個例項,代表一個執行緒的例項。 啟動執行緒的唯一方法就是通過 Thread 類的 start()例項方法。 start()方法是一個 native 方法,它將啟動一個新執行緒,並執行 run()方法。
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
myThread1.start();
3、實現 Runnable 介面。
如果自己的類已經 extends 另一個類,就無法直接 extends Thread,此時,可以實現一個Runnable 介面。
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
//啟動 MyThread,需要首先例項化一個 Thread,並傳入自己的 MyThread 例項:
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
//事實上,當傳入一個 Runnable target 引數給 Thread 後, Thread 的 run()方法就會呼叫
target.run()
public void run() {
if (target != null) {
target.run();
}
}
4、ExecutorService、 Callable、 Future 有返回值執行緒
有返回值的任務必須實現 Callable 介面,類似的,無返回值的任務必須 Runnable 介面。執行Callable 任務後,可以獲取一個 Future 的物件,在該物件上呼叫 get 就可以獲取到 Callable 任務返回的 Object 了,再結合線程池介面ExecutorService 就可以實現傳說中有返回結果的多執行緒 了。
//建立一個執行緒池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 建立多個有返回值的任務
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 執行任務並獲取 Future 物件Future f = pool.submit(c); list.add(f);
}
// 關閉執行緒池
pool.shutdown();
// 獲取所有併發任務的執行結果
for (Future f : list) {
// 從 Future 物件上獲取任務的返回值,並輸出到控制檯
System.out.println("res: " + f.get().toString());
}
5、基於執行緒池的方式
執行緒和資料庫連線這些資源都是非常寶貴的資源。那麼每次需要的時候建立,不需要的時候銷燬,是非常浪費資源的。那麼我們就可以使用快取的策略,也就是使用執行緒池。
// 建立執行緒池
ExecutorService threadPool = Executors.newFixedThreadPool(10); while(true) {
threadPool.execute(new Runnable() { // 提交多個執行緒任務,並執行
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running .."); try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
}
6、4 種執行緒池
Java 裡面執行緒池的頂級介面是 Executor,但是嚴格意義上講 Executor 並不是一個執行緒池,而只是一個執行執行緒的工具。真正的執行緒池介面是 ExecutorService。
newCachedThreadPool
建立一個可根據需要建立新執行緒的執行緒池,但是在以前構造的執行緒可用時將重用它們。對於執行很多短期非同步任務的程式而言,這些執行緒池通常可提高程式效能。 呼叫 execute 將重用以前構造的執行緒(如果執行緒可用)。如果現有執行緒沒有可用的,則建立一個新執行緒並新增到池中。終止並從快取中移除那些已有 60 秒鐘未被使用的執行緒。 因此,長時間保持空閒的執行緒池不會使用任何資源。
newFixedThreadPool
建立一個可重用固定執行緒數的執行緒池,以共享的無界佇列方式來執行這些執行緒。在任意點,在大多數 nThreads 執行緒會處於處理任務的活動狀態。如果在所有執行緒處於活動狀態時提交附加任務, 則在有可用執行緒之前,附加任務將在佇列中等待。如果在關閉前的執行期間由於失敗而導致任何執行緒終止,那麼一個新執行緒將代替它執行後續的任務(如果需要)。在某個執行緒被顯式地關閉之前,池中的執行緒將一直存在。
newScheduledThreadPool
建立一個執行緒池,它可安排在給定延遲後執行命令或者定期地執行。
ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); scheduledThreadPool.schedule(newRunnable(){
@Override
public void run() {
System.out.println("延遲三秒");
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){
@Override
public void run() {
System.out.println("延遲 1 秒後每三秒執行一次");
}
},1,3,TimeUnit.SECONDS);
newSingleThreadExecutor
Executors.newSingleThreadExecutor()返回一個執行緒池(這個執行緒池只有一個執行緒) ,這個執行緒池可以線上程死後(或發生異常時)重新啟動一個執行緒來替代原來的執行緒繼續執行下去!
7、如何停止一個正在執行的執行緒
- 使用退出標誌,使執行緒正常退出,也就是當run方法完成後執行緒終止。
- 使用stop方法強行終止,但是不推薦這個方法,因為stop和suspend及resume一樣都是過期作廢的方法。
- 使用interrupt方法中斷執行緒。
class MyThread extends Thread {
volatile boolean stop = false;
public void run() {
while (!stop) {
System.out.println(getName() + " is running"); try {
sleep(1000);
} catch (InterruptedException e) {
System.out.println("week up from blcok..."); stop = true; // 在異常處理程式碼中修改共享變數的狀態
}
}
System.out.println(getName() + " is exiting...");
}
}
class InterruptThreadDemo3 {
public static void main(String[] args) throws InterruptedException {
MyThread m1 = new MyThread();
System.out.println("Starting thread..."); m1.start();
Thread.sleep(3000);
System.out.println("Interrupt thread...: " + m1.getName()); m1.stop = true; // 設定共享變數為true
m1.interrupt(); // 阻塞時退出阻塞狀態
Thread.sleep(3000); // 主執行緒休眠3秒以便觀察執行緒m1的中斷情況
System.out.println("Stopping application...");
}
8、notify()和notifyAll()有什麼區別?
notify可能會導致死鎖,而notifyAll則不會
任何時候只有一個執行緒可以獲得鎖,也就是說只有一個執行緒可以執行synchronized 中的程式碼使用notifyall,可以喚醒
所有處於wait狀態的執行緒,使其重新進入鎖的爭奪佇列中,而notify只能喚醒一個。
wait() 應配合while迴圈使用,不應使用if,務必在wait()呼叫前後都檢查條件,如果不滿足,必須呼叫
notify()喚醒另外的執行緒來處理,自己繼續wait()直至條件滿足再往下執行。
notify() 是對notifyAll()的一個優化,但它有很精確的應用場景,並且要求正確使用。不然可能導致死鎖。正確的場景應該是 WaitSet中等待的是相同的條件,喚醒任一個都能正確處理接下來的事項,如果喚醒的執行緒無法正確處理,務必確保繼續notify()下一個執行緒,並且自身需要重新回到WaitSet中.
9、sleep()和wait() 有什麼區別?
- 對於 sleep()方法,我們首先要知道該方法是屬於 Thread 類中的。而 wait()方法,則是屬於
Object 類中的。 - sleep()方法導致了程式暫停執行指定的時間,讓出 cpu 該其他執行緒,但是他的監控狀態依然保持者,當指定的時間到了又會自動恢復執行狀態
- 在呼叫 sleep()方法的過程中, 執行緒不會釋放物件鎖。
- 而當呼叫 wait()方法的時候,執行緒會放棄物件鎖,進入等待此物件的等待鎖定池,只有針對此物件呼叫 notify()方法後本執行緒才進入物件鎖定池準備獲取物件鎖進入執行狀態。
10、volatile 是什麼?可以保證有序性嗎?
一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:
- 保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的,volatile關鍵字會強制將修改的值立即寫入主存。
- 禁止進行指令重排序。
volatile 不是原子性操作什麼叫保證部分有序性?
當程式執行到volatile變數的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對後面的操作可見;在其後面的操作肯定還沒有進行;
x = 2; //語句1
y = 0; // 語 句 2
flag = true; //語句3
x = 4; // 語 句 4
y = -1; // 語 句 5
由於flag變數為volatile變數,那麼在進行指令重排序的過程的時候,不會將語句3放到語句1、語句2前面,也不會講語句3放到語句4、語句5後面。但是要注意語句1和語句2的順序、語句4和語句5的順序是不作任何保證的。
使用 Volatile 一般用於 狀態標記量 和 單例模式的雙檢鎖
11、Thread 類中的start() 和 run() 方法有什麼區別?
start()方法被用來啟動新建立的執行緒,而且start()內部呼叫了run()方法,這和直接呼叫run()方法的效果不一樣。當你呼叫run()方法的時候,只會是在原來的執行緒中呼叫,沒有新的執行緒啟動,start()方法才會啟動新執行緒 。
12、為什麼wait, notify 和 notifyAll這些方法不在thread類裡面?
明顯的原因是JAVA提供的鎖是物件級的而不是執行緒級的,每個物件都有鎖,通過執行緒獲得。如果執行緒需 要等待某些鎖那麼呼叫物件中的wait()方法就有意義了。如果wait()方法定義在Thread類中,執行緒正在 等待的是哪個鎖就不明顯了。簡單的說,由於wait,notify和notifyAll都是鎖級別的操作,所以把他們 定義在Object類中因為鎖屬於物件 。
13、為什麼wait和notify方法要在同步塊中呼叫?
- 只有在呼叫執行緒擁有某個物件的獨佔鎖時,才能夠呼叫該物件的wait(),notify()和notifyAll()方法。
- 如果你不這麼做,你的程式碼會丟擲IllegalMonitorStateException異常。
- 還有一個原因是為了避免wait和notify之間產生競態條件。
wait()方法強制當前執行緒釋放物件鎖。這意味著在呼叫某物件的wait()方法之前,當前執行緒必須已經獲得該物件的鎖。因此,執行緒必須在某個物件的同步方法或同步程式碼塊中才能呼叫該物件的wait()方法。
在呼叫物件的notify()和notifyAll()方法之前,呼叫執行緒必須已經得到該物件的鎖。因此,必須在某個物件的同步方法或同步程式碼塊中才能呼叫該物件的notify()或notifyAll()方法。
呼叫wait()方法的原因通常是,呼叫執行緒希望某個特殊的狀態(或變數)被設定之後再繼續執行。呼叫notify()或notifyAll()方法的原因通常是,呼叫執行緒希望告訴其他等待中的執行緒:"特殊狀態已經被設定"。這個狀態作為執行緒間通訊的通道,它必須是一個可變的共享狀態(或變數)。
14、Java中interrupted 和 isInterruptedd方法的區別?
interrupted() 和 isInterrupted()的主要區別是前者會將中斷狀態清除而後者不會。Java多執行緒的中斷機制是用內部標識來實現的,呼叫Thread.interrupt()來中斷一個執行緒就會設定中斷標識為true。
當中斷執行緒呼叫靜態方法Thread.interrupted()來檢查中斷狀態時,中斷狀態會被清零。
而非靜態方法isInterrupted()用來查詢其它執行緒的中斷狀態且不會改變中斷狀態標識。簡單的說就是任何丟擲
InterruptedException異常的方法都會將中斷狀態清零。無論如何,一個執行緒的中斷狀態有有可能被其它執行緒呼叫中斷來改變 。
15、Java中synchronized 和 ReentrantLock 有什麼不同?
相似點:
這兩種同步方式有很多相似之處,它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說當如果一個執行緒獲得了物件鎖,進入了同步塊,其他訪問該同步塊的執行緒都必須阻塞在同步塊外面等待,而進行執行緒阻塞和喚醒的代價是比較高的.
區別:
這兩種方式最大區別就是對於Synchronized來說,它是java語言的關鍵字,是原生語法層面的互斥,需要jvm實現。而ReentrantLock它是JDK 1.5之後提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成。
Synchronized進過編譯,會在同步塊的前後分別形成monitorenter和monitorexit這個兩個位元組碼指令。在執行monitorenter指令時,首先要嘗試獲取物件鎖。如果這個物件沒被鎖定,或者當前執行緒已經擁有了那個物件鎖,把鎖的計算器加1,相應的,在執行monitorexit指令時會將鎖計算器就減1,當計 算器為0時,鎖就被釋放了。如果獲取物件鎖失敗,那當前執行緒就要阻塞,直到物件鎖被另一個執行緒釋放為止 。
由於ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高階功能,主要有以 下3項:
- 等待可中斷,持有鎖的執行緒長期不釋放的時候,正在等待的執行緒可以選擇放棄等待,這相當於Synchronized來說可以避免出現死鎖的情況。
- 公平鎖,多個執行緒等待同一個鎖時,必須按照申請鎖的時間順序獲得鎖,Synchronized鎖非公平鎖,
ReentrantLock預設的建構函式是建立的非公平鎖,可以通過引數true設為公平鎖,但公平鎖表現的效能不是很好。 - 鎖繫結多個條件,一個ReentrantLock物件可以同時繫結對個物件 。
16、有三個執行緒T1,T2,T3,如何保證順序執行?
在多執行緒中有多種方法讓執行緒按特定順序執行,你可以用執行緒類的join()方法在一個執行緒中啟動另一個執行緒,另外一個執行緒完成該執行緒繼續執行。為了確保三個執行緒的順序你應該先啟動最後一個(T3呼叫T2,T2呼叫T1),這樣T1就會先完成而T3最後完成。
實際上先啟動三個執行緒中哪一個都行,
因為在每個執行緒的run方法中用join方法限定了三個執行緒的執行順序
public class JoinTest2 {
// 1.現在有T1、T2、T3三個執行緒,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行
public static void main(String[] args) {
final Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("t1");
}
});
final Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t1執行緒,等待t1執行緒執行完
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2");
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 引用t2執行緒,等待t2執行緒執行完
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t3");
}
});
t3.start();// 這裡三個執行緒的啟動順序可以任意,大家可以試下! t2.start();
t1.start();
}
}
17、SynchronizedMap和ConcurrentHashMap有什麼區別?
SynchronizedMap()和Hashtable一樣,實現上在呼叫map所有方法時,都對整個map進行同步。而
ConcurrentHashMap的實現卻更加精細,它對map中的所有桶加了鎖。所以,只要有一個執行緒訪問
map,其他執行緒就無法進入map,而如果一個執行緒在訪問ConcurrentHashMap某個桶時,其他執行緒, 仍然可以對map執行某些操作。
所以,ConcurrentHashMap在效能以及安全性方面,明顯比Collections.synchronizedMap()更加有優 勢。同時,同步操作精確控制到桶,這樣,即使在遍歷map時,如果其他執行緒試圖對map進行資料修 改,也不會丟擲ConcurrentModificationException 。
18、什麼是執行緒安全
執行緒安全就是說多執行緒訪問同一程式碼,不會產生不確定的結果。
在多執行緒環境中,當各執行緒不共享資料的時候,即都是私有(private)成員,那麼一定是執行緒安全的。但這種情況並不多見,在多數情況下需要共享資料,這時就需要進行適當的同步控制了。
執行緒安全一般都涉及到synchronized, 就是一段程式碼同時只能有一個執行緒來操作 不然中間過程可能會產生不可預製的結果。
如果你的程式碼所在的程序中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。
19、Thread類中的yield方法有什麼作用?
Yield方法可以暫停當前正在執行的執行緒物件,讓其它有相同優先順序的執行緒執行。它是一個靜態方法而且只保證當前執行緒放棄CPU佔用而不能保證使其它執行緒一定能佔用CPU,執行yield()的執行緒有可能在進入到暫停狀態後馬上又被執行。
20、Java執行緒池中submit() 和 execute()方法有什麼區別?
兩個方法都可以向執行緒池提交任務,execute()方法的返回型別是void,它定義在Executor介面中, 而submit()方法可以返回持有計算結果的
Future物件,它定義在ExecutorService介面中,它擴充套件了Executor介面,其它執行緒池類像ThreadPoolExecutor和
ScheduledThreadPoolExecutor都有這些方法 。
21、說一說自己對於 synchronized 關鍵字的瞭解
synchronized關鍵字解決的是多個執行緒之間訪問資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者程式碼塊在任意時刻只能有一個執行緒執行。
另外,在 Java 早期版本中,synchronized屬於重量級鎖,效率低下,因為監視器鎖(monitor)是依賴於底層的作業系統的 Mutex Lock 來實現的,Java 的執行緒是對映到作業系統的原生執行緒之上的。
如果要掛起或者喚醒一個執行緒,都需要作業系統幫忙完成,而作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什麼早期的synchronized 效率低的原因。
慶幸的是在 Java 6 之後 Java 官方對從 JVM 層面對synchronized 較大優化,所以現在的 synchronized 鎖效率也優化得很不錯了。JDK1.6對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷 。
22、說說自己是怎麼使用 synchronized 關鍵字,在專案中用到了嗎synchronized關鍵字最主要的三種使用方式
修飾例項方法: 作用於當前物件例項加鎖,進入同步程式碼前要獲得當前物件例項的鎖
修飾靜態方法: 也就是給當前類加鎖,會作用於類的所有物件例項,因為靜態成員不屬於任何一個例項物件,是類成員( static 表明這是該類的一個靜態資源,不管new了多少個物件,只有一份)。所以如果一個執行緒A呼叫一個例項物件的非靜態 synchronized 方法,而執行緒B需要呼叫這個例項物件所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態synchronized 方法佔用的鎖是當前例項物件鎖。
修飾程式碼塊: 指定加鎖物件,對給定物件加鎖,進入同步程式碼庫前要獲得給定物件的鎖。
總結: synchronized 關鍵字加到 static 靜態方法和 synchronized(class)程式碼塊上都是是給 Class 類上鎖。synchronized 關鍵字加到例項方法上是給物件例項上鎖。儘量不要使用 synchronized(String a) 因為JVM中,字串常量池具有快取功能
23、什麼是執行緒安全?Vector是一個執行緒安全類嗎?
如果你的程式碼所在的程序中有多個執行緒在同時執行,而這些執行緒可能會同時執行這段程式碼。如果每次執行結果和單執行緒執行的結果是一樣的,而且其他的變數 的值也和預期的是一樣的,就是執行緒安全的。一個執行緒安全的計數器類的同一個例項物件在被多個執行緒使用的情況下也不會出現計算失誤。很顯然你可以將集合類分 成兩組,執行緒安全和非執行緒安全的。Vector 是用同步方法來實現執行緒安全的, 而和它相似的ArrayList不是執行緒安全的。
24、volatile關鍵字的作用?
一旦一個共享變數(類的成員變數、類的靜態成員變數)被volatile修飾之後,那麼就具備了兩層語義:
保證了不同執行緒對這個變數進行操作時的可見性,即一個執行緒修改了某個變數的值,這新值對其他執行緒來說是立即可見的。
禁止進行指令重排序。
- volatile本質是在告訴jvm當前變數在暫存器(工作記憶體)中的值是不確定的,需要從主存中讀取;synchronized則是鎖定當前變數, 只有當前執行緒可以訪問該變數,其他執行緒被阻塞住。
- volatile僅能使用在變數級別;synchronized則可以使用在變數、方法、和類級別的。
- volatile僅能實現變數的修改可見性,並不能保證原子性;synchronized則可以保證變數的修改可見性和原子性。
- volatile不會造成執行緒的阻塞;synchronized可能會造成執行緒的阻塞。
volatile標記的變數不會被編譯器優化;synchronized標記的變數可以被編譯器優化
25、簡述一下你對執行緒池的理解
如果問到了這樣的問題,可以展開的說一下執行緒池如何用、執行緒池的好處、執行緒池的啟動策略)合理利用執行緒池能夠帶來三個好處。
第一:降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行。
第三:提高執行緒的可管理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控
26、執行緒生命週期(狀態)
當執行緒被建立並啟動以後,它既不是一啟動就進入了執行狀態,也不是一直處於執行狀態。線上程的生命週期中,它要經過新建(New)、就緒(Runnable)、執行(Running)、阻塞(Blocked)和死亡(Dead)5 種狀態。尤其是當執行緒啟動以後,它不可能一直"霸佔"著 CPU 獨自執行,所以 CPU 需要在多條執行緒之間切換,於是執行緒狀態也會多次在執行、阻塞之間切換
27、新建狀態(NEW)
當程式使用 new 關鍵字建立了一個執行緒之後,該執行緒就處於新建狀態,此時僅由 JVM 為其分配記憶體,並初始化其成員變數的值
28、就緒狀態(RUNNABLE)
當執行緒物件呼叫了 start()方法之後,該執行緒處於就緒狀態。 Java 虛擬機器會為其建立方法呼叫棧和程式計數器,等待排程執行。
29、執行狀態(RUNNING)
如果處於就緒狀態的執行緒獲得了 CPU,開始執行 run()方法的執行緒執行體,則該執行緒處於執行狀態。
30、阻塞狀態(BLOCKED)
阻塞狀態是指執行緒因為某種原因放棄了 cpu 使用權,也即讓出了 cpu timeslice,暫時停止執行。直到執行緒進入可執行(runnable)狀態,才有機會再次獲得 cpu timeslice 轉到執行(running)狀態。阻塞的情況分三種:
等待阻塞(o.wait->等待對列) :
執行(running)的執行緒執行 o.wait()方法, JVM 會把該執行緒放入等待佇列(waitting queue)中。
同步阻塞(lock->鎖池)
執行(running)的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則 JVM 會把該執行緒放入鎖池(lock pool)中。
其他阻塞(sleep/join)
執行(running)的執行緒執行 Thread.sleep(long ms)或 t.join()方法,或者發出了 I/O 請求時,JVM 會把該執行緒置為阻塞狀態。當 sleep()狀態超時、 join()等待執行緒終止或者超時、或者 I/O處理完畢時,執行緒重新轉入可執行(runnable)狀態。
31、執行緒死亡(DEAD)
執行緒會以下面三種方式結束,結束後就是死亡狀態。正常結束
- run()或 call()方法執行完成,執行緒正常結束。異常結束
- 執行緒丟擲一個未捕獲的 Exception 或 Error。呼叫 stop
- 直接呼叫該執行緒的 stop()方法來結束該執行緒—該方法通常容易導致死鎖,不推薦使用。
32、終止執行緒 4 種方式
正常執行結束
程式執行結束,執行緒自動結束。
使用退出標誌退出執行緒
一般 run()方法執行完,執行緒就會正常結束,然而,常常有些執行緒是伺服執行緒。它們需要長時間的執行,只有在外部某些條件滿足的情況下,才能關閉這些執行緒。使用一個變數來控制迴圈,例如:最直接的方法就是設一個 boolean 型別的標誌,並通過設定這個標誌為 true 或
false 來控制 while迴圈是否退出,程式碼示例 :
public class ThreadSafe extends Thread {
public volatile boolean exit = false;
public void run() {
while (!exit){
//do something
}
}
}
定義了一個退出標誌 exit,當 exit 為 true 時, while 迴圈退出, exit 的預設值為 false.在定義 exit時,使用了一個 Java 關鍵字 volatile, 這個關鍵字的目的是使 exit 同步,也就是說在同一時刻只能由一個執行緒來修改 exit 的值。
Interrupt 方法結束執行緒
使 用 interrupt() 方 法 來 中 斷 線 程 有 兩 種 情 況 :
- 執行緒處於阻塞狀態: 如使用了 sleep,同步鎖的 wait,socket 中的 receiver,accept 等方法時,會使執行緒處於阻塞狀態。當呼叫執行緒的interrupt()方法時,會丟擲 InterruptException 異常。阻塞中的那個方法丟擲這個異常,通過程式碼捕獲該異常,然後 break 跳出迴圈狀態,從而讓我們有機會結束這個執行緒的執行。 通常很多人認為只要呼叫 interrupt 方法執行緒就會結束,實際上是錯的, 一定要先捕獲InterruptedException 異常之後通過 break 來跳出迴圈,才能正常結束 run 方法。
- 執行緒未處於阻塞狀態: 使用 isInterrupted()判斷執行緒的中斷標誌來退出迴圈。當使用interrupt()方法時,中斷標誌就會置 true,和使用自定義的標誌來控制迴圈是一樣的道理。
public class ThreadSafe extends Thread {
public void run() {
while (!isInterrupted()){ //非阻塞過程中通過判斷中斷標誌來退出
try{
Thread.sleep(5*1000);
//阻塞過程捕獲中斷異常來退出
}catch(InterruptedException e){
e.printStackTrace();
break;//捕獲到異常之後,執行 break 跳出迴圈
}
}
}
}
- stop 方法終止執行緒(執行緒不安全)
程式中可以直接使用 thread.stop()來強行終止執行緒,但是 stop 方法是很危險的,就象突然關閉計算機電源,而不是按正常程式關機一樣, 可能會產生不可預料的結果,不安全主要是:thread.stop()呼叫之後,建立子執行緒的執行緒就會丟擲 ThreadDeatherror 的錯誤,並且會釋放子執行緒所持有的所有鎖。一般任何進行加鎖的程式碼塊,都是為了保護資料的一致性,如果在呼叫thread.stop()後導致了該執行緒所持有的所有鎖的突然釋放(不可控制),那麼被保護資料就有可能呈現不一致性,其他執行緒在使用這些被破壞的資料時,有可能導致一些很奇怪的應用程式錯誤。因此,並不推薦使用 stop 方法來終止執行緒。
33、start 與 run 區別
- start() 方法來啟動執行緒,真正實現了多執行緒執行。這時無需等待 run 方法體程式碼執行完畢,可以直接繼續執行下面的程式碼。
- 通過呼叫 Thread 類的 start()方法來啟動一個執行緒, 這時此執行緒是處於就緒狀態, 並沒有執行。
- 方法 run()稱為執行緒體,它包含了要執行的這個執行緒的內容,執行緒就進入了執行狀態,開始執行 run 函式當中的程式碼。 Run 方法執行結束, 此執行緒終止。然後 CPU 再排程其它執行緒。
34、JAVA 後臺執行緒
- 定義:守護執行緒--也稱“服務執行緒”, 他是後臺執行緒, 它有一個特性,即為使用者執行緒 提供 公共服務, 在沒有使用者執行緒可服務時會自動離開。
- 優先順序:守護執行緒的優先順序比較低,用於為系統中的其它物件和執行緒提供服務。
- 設定:通過 setDaemon(true)來設定執行緒為“守護執行緒”;將一個使用者執行緒設定為守護執行緒的方式是在 執行緒物件建立 之前 用執行緒物件的
setDaemon 方法。 - 在 Daemon 執行緒中產生的新執行緒也是 Daemon 的。
- 執行緒則是 JVM 級別的,以 Tomcat 為例,如果你在 Web 應用中啟動一個執行緒,這個執行緒的生命週期並不會和 Web 應用程式保持同步。也就是說,即使你停止了 Web 應用,這個執行緒依舊是活躍的。
- example: 垃圾回收執行緒就是一個經典的守護執行緒,當我們的程式中不再有任何執行的Thread, 程式就不會再產生垃圾,垃圾回收器也就無事可做, 所以當垃圾回收執行緒是 JVM 上僅剩的執行緒時,垃圾回收執行緒會自動離開。它始終在低級別的狀態中執行,用於實時監控和管理系統 中的可回收資源。
- 生命週期:守護程序(Daemon)是執行在後臺的一種特殊程序。它獨立於控制終端並且週期性地執行某種任務或等待處理某些發生的事件。也就是說守護執行緒不依賴於終端,但是依賴於系統,與系統“同生共死”。當 JVM 中所有的執行緒都是守護執行緒的時候, JVM 就可以退出了;如果還有一個或以上的非守護執行緒則 JVM 不會退出
35、什麼是樂觀鎖
樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到併發寫的可能性低,每次去拿資料的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個資料,採取在寫時先讀出當前版本號,然後加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重複讀-比較-寫的操作。
java 中的樂觀鎖基本都是通過 CAS 操作實現的, CAS 是一種更新的原子操作, 比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。
36、什麼是悲觀鎖
悲觀鎖是就是悲觀思想,即認為寫多,遇到併發寫的可能性高,每次去拿資料的時候都認為別人會修改,所以每次在讀寫資料的時候都會上鎖,這樣別人想讀寫這個資料就會 block 直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嚐試cas樂觀鎖去獲取鎖, 獲取不到,才會轉換為悲觀鎖,如 RetreenLock。
37、什麼是自旋鎖
自旋鎖原理非常簡單, 如果持有鎖的執行緒能在很短時間內釋放鎖資源,那麼那些等待競爭鎖的執行緒就不需要做核心態和使用者態之間的切換進入阻塞掛起狀態,它們只需要等一等(自旋),等持有鎖的執行緒釋放鎖後即可立即獲取鎖,這樣就避免使用者執行緒和核心的切換的消耗。
執行緒自旋是需要消耗 cup 的,說白了就是讓 cup 在做無用功,如果一直獲取不到鎖,那執行緒也不能一直佔用 cup 自旋做無用功,所以需要設定一個自旋等待的最大時間。
如果持有鎖的執行緒執行的時間超過自旋等待的最大時間扔沒有釋放鎖,就會導致其它爭用鎖的執行緒在最大等待時間內還是獲取不到鎖,這時爭用執行緒會停止自旋進入阻塞狀態。
自旋鎖的優缺點
自旋鎖儘可能的減少執行緒的阻塞,這對於鎖的競爭不激烈,且佔用鎖時間非常短的程式碼塊來說效能能大幅度的提升,因為自旋的消耗會小於執行緒阻塞掛起再喚醒的操作的消耗,這些操作會導致執行緒發生兩次上下文切換!
但是如果鎖的競爭激烈,或者持有鎖的執行緒需要長時間佔用鎖執行同步塊,這時候就不適合使用自旋鎖了,因為自旋鎖在獲取鎖前一直都是佔用 cpu 做無用功,佔著 XX 不 XX,同時有大量執行緒在競爭一個鎖,會導致獲取鎖的時間很長,執行緒自旋的消耗大於執行緒阻塞掛起操作的消耗,其它需要 cup 的執行緒又不能獲取到 cpu,造成 cpu 的浪費。所以這種情況下我們要關閉自旋鎖;
自旋鎖時間閾值(1.6 引入了適應性自旋鎖)
自旋鎖的目的是為了佔著 CPU 的資源不釋放,等到獲取到鎖立即進行處理。但是如何去選擇自旋的執行時間呢?如果自旋執行時間太長, 會有大量的執行緒處於自旋狀態佔用 CPU 資源,進而會影響整體系統的效能。因此自旋的週期選的額外重要!
JVM 對於自旋週期的選擇, jdk1.5 這個限度是一定的寫死的, 在 1.6 引入了適應性自旋鎖,適應性自旋鎖意味著自旋的時間不在是固定的了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定,基本認為一個執行緒上下文切換的時間是最佳的一個時間,同時JVM 還針對當前 CPU 的負荷情況做了較多的優化, 如果平均負載小於 CPUs 則一直自旋, 如果有超過(CPUs/2)個執行緒正在自旋,則後來執行緒直接阻塞, 如果正在自旋的執行緒發現 Owner 發生了變化則延遲自旋時間(自旋計數)或進入阻塞, 如果 CPU 處於節電模式則停止自旋, 自旋時間的最壞情況是 CPU的儲存延遲(CPU A 儲存了一個數據,到 CPU B 得知這個資料直接的時間差) , 自旋時會適當放棄執行緒優先順序之間的差異。
自旋鎖的開啟
JDK1.6 中-XX:+UseSpinning 開啟;
-XX:PreBlockSpin=10 為自旋次數;
JDK1.7 後,去掉此引數,由 jvm 控制;
38、Synchronized 同步鎖
synchronized 它可以把任意一個非 NULL 的物件當作鎖。 他屬於獨佔式的悲觀鎖,同時屬於可重入鎖。
Synchronized 作用範圍:
- 作用於方法時,鎖住的是物件的例項(this);
- 當作用於靜態方法時,鎖住的是Class例項,又因為Class的相關資料儲存在永久帶PermGen(jdk1.8 則是 metaspace),永久帶是全域性共享的,因此靜態方法鎖相當於類的一個全域性鎖,會鎖所有呼叫該方法的執行緒;
- synchronized 作用於一個物件例項時,鎖住的是所有以該物件為鎖的程式碼塊。 它有多個佇列,當多個執行緒一起訪問某個物件監視器的時候,物件監視器會將這些執行緒儲存在不同的容器中。
Synchronized 核心元件
4. Wait Set:哪些呼叫 wait 方法被阻塞的執行緒被放置在這裡;
5. Contention List: 競爭佇列,所有請求鎖的執行緒首先被放在這個競爭佇列中;
6. Entry List: Contention List 中那些有資格成為候選資源的執行緒被移動到 Entry List 中;
7. OnDeck:任意時刻, 最多隻有一個執行緒正在競爭鎖資源,該執行緒被成為 OnDeck;
8. Owner:當前已經獲取到所資源的執行緒被稱為 Owner;
9. !Owner:當前釋放鎖的執行緒。
Synchronized 實現
- JVM 每次從佇列的尾部取出一個數據用於鎖競爭候選者(OnDeck),但是併發情況下,
ContentionList 會被大量的併發執行緒進行 CAS 訪問,為了降低對尾部元素的競爭, JVM 會將一部分執行緒移動到 EntryList 中作為候選競爭執行緒。 - Owner 執行緒會在 unlock 時,將 ContentionList 中的部分執行緒遷移到 EntryList 中,並指定
EntryList 中的某個執行緒為 OnDeck 執行緒(一般是最先進去的那個執行緒)。 - Owner 執行緒並不直接把鎖傳遞給 OnDeck 執行緒,而是把鎖競爭的權利交給 OnDeck,
OnDeck 需要重新競爭鎖。這樣雖然犧牲了一些公平性,但是能極大的提升系統的吞吐量,在
JVM 中,也把這種選擇行為稱之為“競爭切換”。 - OnDeck 執行緒獲取到鎖資源後會變為 Owner 執行緒,而沒有得到鎖資源的仍然停留在 EntryList 中。如果 Owner 執行緒被 wait 方法阻塞,則轉移到 WaitSet 佇列中,直到某個時刻通過 notify 或者 notifyAll 喚醒,會重新進去 EntryList 中。
- 處於 ContentionList、 EntryList、 WaitSet 中的執行緒都處於阻塞狀態,該阻塞是由作業系統來完成的(Linux 核心下采用 pthread_mutex_lock 核心函式實現的)。
- Synchronized 是非公平鎖。 Synchronized 線上程進入 ContentionList 時, 等待的執行緒會先嚐試自旋獲取鎖,如果獲取不到就進入 ContentionList,這明顯對於已經進入佇列的執行緒是不公平的,還有一個不公平的事情就是自旋獲取鎖的執行緒還可能直接搶佔 OnDeck 執行緒的鎖資源。
參考: https://blog.csdn.net/zqz_zqz/article/details/70233767 - 每個物件都有個 monitor 物件, 加鎖就是在競爭 monitor 物件,程式碼塊加鎖是在前後分別加上 monitorenter 和 monitorexit 指令來實現的,方法加鎖是通過一個標記位來判斷的
- synchronized 是一個重量級操作,需要呼叫作業系統相關介面,效能是低效的,有可能給執行緒加鎖消耗的時間比有用操作消耗的時間更多。
- Java1.6, synchronized 進行了很多的優化, 有適應自旋、鎖消除、鎖粗化、輕量級鎖及偏向鎖等,效率有了本質上的提高。在之後推出的 Java1.7 與 1.8 中,均對該關鍵字的實現機理做了優化。引入了偏向鎖和輕量級鎖。都是在物件頭中有標記位,不需要經過作業系統加鎖。
- 鎖可以從偏向鎖升級到輕量級鎖,再升級到重量級鎖。這種升級過程叫做鎖膨脹;
- JDK 1.6 中預設是開啟偏向鎖和輕量級鎖,可以通過-XX:-UseBiasedLocking 來禁用偏向鎖。
39、ReentrantLock
ReentantLock 繼承介面 Lock 並實現了介面中定義的方法, 他是一種可重入鎖, 除了能完
成 synchronized 所能完成的所有工作外,還提供了諸如可響應中斷鎖、可輪詢鎖請求、定時鎖等避免多執行緒死鎖的方法。
Lock 介面的主要方法:
- void lock(): 執行此方法時, 如果鎖處於空閒狀態, 當前執行緒將獲取到鎖. 相反, 如果鎖已經被其他執行緒持有, 將禁用當前執行緒, 直到當前執行緒獲取到鎖.
- boolean tryLock(): 如果鎖可用, 則獲取鎖, 並立即返回 true, 否則返回 false. 該方法和
lock()的區別在於, tryLock()只是"試圖"獲取鎖, 如果鎖不可用, 不會導致當前執行緒被禁用, 當前執行緒仍然繼續往下執行程式碼. 而 lock()方法則是一定要獲取到鎖, 如果鎖不可用, 就一直等待, 在未獲得鎖之前,當前執行緒並不繼續向下執行. - void unlock():執行此方法時, 當前執行緒將釋放持有的鎖. 鎖只能由持有者釋放, 如果執行緒並不持有鎖, 卻執行該方法, 可能導致異常的發生.
- Condition newCondition(): 條件物件,獲取等待通知元件。該元件和當前的鎖繫結, 當前執行緒只有獲取了鎖,才能呼叫該元件的 await()方法,而呼叫後,當前執行緒將縮放鎖。
- getHoldCount() : 查詢當前執行緒保持此鎖的次數,也就是執行此執行緒執行 lock 方法的次數。
- getQueueLength() : 返回正等待獲取此鎖的執行緒估計數,比如啟動 10 個執行緒, 1 個執行緒獲得鎖,此時返回的是 9
- getWaitQueueLength: (Condition condition)返回等待與此鎖相關的給定條件的執行緒估計數。比如 10 個執行緒,用同一個 condition 物件,並且此時這 10 個執行緒都執行了
- condition 物件的 await 方法,那麼此時執行此方法返回 10
- hasWaiters(Condition condition): 查詢是否有執行緒等待與此鎖有關的給定條件(condition),對於指定 contidion 物件,有多少執行緒執行了 condition.await 方法
- hasQueuedThread(Thread thread): 查詢給定執行緒是否等待獲取此鎖
- hasQueuedThreads(): 是否有執行緒等待此鎖
- isFair(): 該鎖是否公平鎖
- isHeldByCurrentThread(): 當前執行緒是否保持鎖鎖定,執行緒的執行 lock 方法的前後分別是 false 和 true
- isLock(): 此鎖是否有任意執行緒佔用
- lockInterruptibly() : 如果當前執行緒未被中斷,獲取鎖
- tryLock() : 嘗試獲得鎖,僅在呼叫時鎖未被執行緒佔用,獲得鎖
- tryLock(long timeout TimeUnit unit): 如果鎖在給定等待時間內沒有被另一個執行緒保持, 則獲取該鎖。
非公平鎖
JVM 按隨機、就近原則分配鎖的機制則稱為不公平鎖, ReentrantLock 在建構函式中提供了
是否公平鎖的初始化方式,預設為非公平鎖。 非公平鎖實際執行的效率要遠遠超出公平鎖,除非程式有特殊需要,否則最常用非公平鎖的分配機制。
公平鎖
公平鎖指的是鎖的分配機制是公平的,通常先對鎖提出獲取請求的執行緒會先被分配到鎖,
ReentrantLock 在建構函式中提供了是否公平鎖的初始化方式來定義公平鎖。
40、Condition 類和 Object 類鎖方法區別區別
- Condition 類的 awiat 方法和 Object 類的 wait 方法等效
- Condition 類的 signal 方法和 Object 類的 notify 方法等效
- Condition 類的 signalAll 方法和 Object 類的 notifyAll 方法等效
- ReentrantLock 類可以喚醒指定條件的執行緒,而 object 的喚醒是隨機的
41、tryLock 和 lock 和 lockInterruptibly 的區別
- tryLock 能獲得鎖就返回 true,不能就立即返回 false, tryLock(long timeout,TimeUnit unit),可以增加時間限制,如果超過該時間段還沒獲得鎖,返回 false
- lock 能獲得鎖就返回 true,不能的話一直等待獲得鎖
- lock 和 lockInterruptibly,如果兩個執行緒分別執行這兩個方法,但此時中斷這兩個執行緒,lock 不會丟擲異常,而 lockInterruptibly 會丟擲異常。
42、Semaphore 訊號量
Semaphore 是一種基於計數的訊號量。它可以設定一個閾值,基於此,多個執行緒競爭獲取許可訊號,做完自己的申請後歸還,超過閾值後,執行緒申請許可訊號將會被阻塞。 Semaphore 可以用來構建一些物件池,資源池之類的, 比如資料庫連線池
實現互斥鎖(計數器為 1)
我們也可以建立計數為 1 的 Semaphore,將其作為一種類似互斥鎖的機制,這也叫二元訊號量, 表示兩種互斥狀態。
程式碼實現
// 建立一個計數閾值為 5 的訊號量物件
// 只能 5 個執行緒同時訪問
Semaphore semp = new Semaphore(5); try { // 申請許可
semp.acquire();
try {
// 業務邏輯
} catch (Exception e) {
} finally {
// 釋放許可
semp.release();
}
} catch (InterruptedException e) {
}
43、Semaphore 與 ReentrantLock 區別
Semaphore 基本能完成 ReentrantLock 的所有工作,使用方法也與之類似,通過 acquire()與release()方法來獲得和釋放臨界資源。經實測, Semaphone.acquire()方法預設為可響應中斷鎖, 與 ReentrantLock.lockInterruptibly()作用效果一致,也就是說在等待臨界資源的過程中可以被Thread.interrupt()方法中斷。
此外, Semaphore 也實現了可輪詢的鎖請求與定時鎖的功能,除了方法名 tryAcquire 與 tryLock 不同,其使用方法與 ReentrantLock 幾乎一致。 Semaphore 也提供了公平與非公平鎖的機制,也可在建構函式中進行設定。
Semaphore 的鎖釋放操作也由手動進行,因此與 ReentrantLock 一樣,為避免執行緒因丟擲異常而無法正常釋放鎖的情況發生,釋放鎖的操作也必須在 finally 程式碼塊中完成。
44、可重入鎖(遞迴鎖)
本文裡面講的是廣義上的可重入鎖,而不是單指 JAVA 下的 ReentrantLock。 可重入鎖,也叫做遞迴鎖,指的是同一執行緒 外層函式獲得鎖之後 ,內層遞迴函式仍然有獲取該鎖的程式碼,但不受影響。在 JAVA 環境下 ReentrantLock 和 synchronized 都是 可重入鎖。
45、公平鎖與非公平鎖
公平鎖(Fair)
加鎖前檢查是否有排隊等待的執行緒,優先排隊等待的執行緒,先來先得
非公平鎖(Nonfair)
加鎖時不考慮排隊等待問題,直接嘗試獲取鎖,獲取不到自動到隊尾等待
- 非公平鎖效能比公平鎖高 5~10 倍,因為公平鎖需要在多核的情況下維護一個佇列
- Java 中的 synchronized 是非公平鎖, ReentrantLock 預設的 lock()方法採用的是非公平鎖。
46、ReadWriteLock 讀寫鎖
為了提高效能, Java 提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,如果沒有寫鎖的情況下,讀是無阻塞的,在一定程度上提高了程式的執行效率。 讀寫鎖分為讀鎖和寫鎖,多個讀鎖不互斥,讀鎖與寫鎖互斥,這是由 jvm 自己控制的,你只要上好相應的鎖即可。
讀鎖
如果你的程式碼只讀資料,可以很多人同時讀,但不能同時寫,那就上讀鎖
寫鎖
如果你的程式碼修改資料,只能有一個人在寫,且不能同時讀取,那就上寫鎖。總之,讀的時候上讀鎖,寫的時候上寫鎖!
Java 中 讀 寫 鎖 有 個 接 口 java.util.concurrent.locks.ReadWriteLock , 也 有 具 體 的 實 現
ReentrantReadWriteLock。
48、重量級鎖(Mutex Lock)
Synchronized 是通過物件內部的一個叫做監視器鎖(monitor)來實現的。但是監視器鎖本質又是依賴於底層的作業系統的 Mutex Lock 來實現的。
而作業系統實現執行緒之間的切換這就需要從使用者態轉換到核心態,這個成本非常高,狀態之間的轉換需要相對比較長的時間,這就是為什麼
Synchronized 效率低的原因。
因此, 這種依賴於作業系統 Mutex Lock 所實現的鎖我們稱之為“重量級鎖” 。 JDK 中對 Synchronized 做的種種優化,其核心都是為了減少這種重量級鎖的使用。
JDK1.6 以後,為了減少獲得鎖和釋放鎖所帶來的效能消耗,提高效能,引入了“輕量級鎖”和“偏向鎖”。
49、輕量級鎖
鎖的狀態總共有四種:無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。
鎖升級
隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖(但是鎖的升級是單向的, 也就是說只能從低到高升級,不會出現鎖的降級)。
“輕量級” 是相對於使用作業系統互斥量來實現的傳統鎖而言的。但是,首先需要強調一點的是, 輕量級鎖並不是用來代替重量級鎖的,它的本意是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用產生的效能消耗。
在解釋輕量級鎖的執行過程之前, 先明白一點,輕量級鎖所適應的場景是執行緒交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖
50、偏向鎖
Hotspot 的作者經過以往的研究發現大多數情況下鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得。 偏向鎖的目的是在某個執行緒獲得鎖之後,消除這個執行緒鎖重入(CAS)的開銷,看起來讓這個執行緒得到了偏護。
引入偏向鎖是為了在無多執行緒競爭的情況下儘量減少不必要的輕量級鎖執行路徑,因為輕量級鎖的獲取及釋放依賴多次 CAS 原子指令, 而偏向鎖只需要在置換ThreadID 的時候依賴一次 CAS 原子指令(由於一旦出現多執行緒競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的效能損耗必須小於節省下來的 CAS 原子指令的效能消耗)。
上面說過, 輕量級鎖是為了線上程交替執行同步塊時提高效能, 而偏向鎖則是在只有一個執行緒執行同步塊時進一步提高效能
51、分段鎖
分段鎖也並非一種實際的鎖,而是一種思想 ConcurrentHashMap 是學習分段鎖的最好實踐
52、鎖優化
減少鎖持有時間
只用在有執行緒安全要求的程式上加鎖
減小鎖粒度
將大物件(這個物件可能會被很多執行緒訪問),拆成小物件,大大增加並行度,降低鎖競爭。降低了鎖的競爭,偏向鎖,輕量級鎖成功率才會提高。最最典型的減小鎖粒度的案例就是
ConcurrentHashMap。
鎖分離
最常見的鎖分離就是讀寫鎖 ReadWriteLock,根據功能進行分離成讀鎖和寫鎖,這樣讀讀不互斥,讀寫互斥,寫寫互斥,即保證了執行緒安全,又提高了效能,具體也請檢視[高併發 Java 五]
JDK 併發包 1。讀寫分離思想可以延伸,只要操作互不影響,鎖就可以分離。比如
LinkedBlockingQueue 從頭部取出,從尾部放資料
鎖粗化
通常情況下,為了保證多執行緒間的有效併發,會要求每個執行緒持有鎖的時間儘量短,即在使用完公共資源後,應該立即釋放鎖。但是,凡事都有一個度, 如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於效能的優化 。
鎖消除
鎖消除是在編譯器級別的事情。 在即時編譯器時,如果發現不可能被共享的物件,則可以消除這些物件的鎖操作,多數是因為程式設計師編碼不規範引起。
歡迎關注
CSDN:程式設計師小羊
微信公眾號:程式設計師小羊
部落格園:程式設計師小羊