JAVA 併發知識庫
文章目錄
- 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、鎖優化
- 擴展連接:[更多請點擊這裏](https://blog.csdn.net/weixin_44395707/category_9792353.html)
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 從頭部取出,從尾部放數據
鎖粗化
通常情況下,爲了保證多線程間的有效併發,會要求每個線程持有鎖的時間儘量短,即在使用完公共資源後,應該立即釋放鎖。但是,凡事都有一個度, 如果對同一個鎖不停的進行請求、同步和釋放,其本身也會消耗系統寶貴的資源,反而不利於性能的優化 。
鎖消除
鎖消除是在編譯器級別的事情。 在即時編譯器時,如果發現不可能被共享的對象,則可以消除這些對象的鎖操作,多數是因爲程序員編碼不規範引起。
參考: https://www.jianshu.com/p/39628e1180a9
未完待續。。。
擴展連接:更多請點擊這裏
博主公衆號程序員小羊 只發面試相關推文