Java多執行緒概述、synchronized
概述
Java 程式天生就是多執行緒程式,我們可以通過 JMX 來看一下一個普通的 Java 程式有哪些執行緒,程式碼如下。
public class MultiThread { public static void main(String[] args) { // 獲取Java執行緒管理MXBean ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); // 不需要獲取同步的 monitor 和 synchronizer 資訊,僅獲取執行緒和執行緒堆疊資訊 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); for (ThreadInfo threadInfo:threadInfos) { System.out.println("[" + threadInfo.getThreadId() + "]" + threadInfo.getThreadName()); } } }
輸出:
[6]Monitor Ctrl-Break
[5] Attach Listener //新增事件
[4] Signal Dispatcher // 分發處理給 JVM 訊號的執行緒
[3] Finalizer //呼叫物件 finalize 方法的執行緒
[2] Reference Handler //清除 reference 執行緒
[1] main //main 執行緒,程式入口
一個 Java 程式的執行是 main 執行緒和多個其他執行緒同時執行。
一個程序中可以有多個執行緒,多個執行緒共享程序的堆和方法區 (JDK1.8 之後的元空間)資源,但是每個執行緒有自己的程式計數器、虛擬機器棧 和 本地方法棧
。總結: 執行緒是程序劃分成的更小的執行單位。執行緒和程序最大的不同在於基本上各程序是獨立的,而各執行緒則不一定,因為同一程序中的執行緒極有可能會相互影響。執行緒執行開銷小,但不利於資源的管理和保護;而程序正相反。
一個 Native Method 就是一個 Java 呼叫非 Java 程式碼的介面。一個 Native Method 是這樣一個 Java 的方法:該方法的實現由非 Java 語言實現,比如 C。
虛擬機器棧為虛擬機器執行 Java 方法 (也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的 Native 方法服務。 在 HotSpot 虛擬機器中和 Java 虛擬機器棧合二為一。
java.lang.Thread.State
列舉類中定義了六種執行緒的狀態,可以呼叫執行緒Thread中的getState()
方法獲取當前執行緒的狀態。
原圖中 wait 到 runnable 狀態的轉換中,
join
實際上是Thread
類的方法,但這裡寫成了Object
。
執行緒在執行過程中會有自己的執行條件和狀態(也稱上下文),比如程式計數器,棧資訊等。
死鎖例子:
public class MultiThread {
private static Object resource1 = new Object();
private static Object resource2 = new Object();
public static void main(String[] args) {
new Thread(()-> {
synchronized (resource1) {
System.out.println(Thread.currentThread()+"get resource1");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread()+"get resource2");
}
}
},"執行緒1").start();
new Thread(()-> {
synchronized (resource2) {
System.out.println(Thread.currentThread()+"get resource2");
try {
Thread.sleep(1_000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread()+"waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread()+"get resource1");
}
}
},"執行緒2").start();
}
}
/*
輸出:
Thread[執行緒1,5,main]get resource1
Thread[執行緒2,5,main]get resource2
Thread[執行緒2,5,main]waiting get resource1
Thread[執行緒1,5,main]waiting get resource2
*/
死鎖的必備條件:
資源鎖、獲取資源阻塞時不釋放、不被搶、迴圈等待資源。
sleep()
方法和 wait()
方法區別和共同點:
- 兩者最主要的區別在於:
sleep()
方法沒有釋放鎖,而wait()
方法釋放了鎖。 - 兩者都可以暫停執行緒的執行。
wait()
通常被用於執行緒間互動/通訊,sleep()
通常被用於暫停執行。wait()
方法被呼叫後,執行緒不會自動甦醒,需要別的執行緒呼叫同一個物件上的notify()
或者notifyAll()
方法。sleep()
方法執行完成後,執行緒會自動甦醒。或者可以使用wait(long timeout)
超時後執行緒會自動甦醒。
synchronized
synchronized
關鍵字解決的是多個執行緒之間訪問資源的同步性,synchronized
關鍵字可以保證被它修飾的方法或者程式碼塊在任意時刻只能有一個執行緒執行。
另外,在 Java 早期版本中,synchronized
屬於 重量級鎖,效率低下。
因為監視器鎖(monitor)是依賴於底層的作業系統的 Mutex Lock
來實現的,Java 的執行緒是對映到作業系統的原生執行緒之上的。如果要掛起或者喚醒一個執行緒,都需要作業系統幫忙完成,而作業系統實現執行緒之間的切換時需要從使用者態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高。
慶幸的是在 Java 6 之後 Java 官方對從 JVM 層面對 synchronized
較大優化,所以現在的 synchronized
鎖效率也優化得很不錯了。JDK1.6 對鎖的實現引入了大量的優化,如自旋鎖、適應性自旋鎖、鎖消除、鎖粗化、偏向鎖、輕量級鎖等技術來減少鎖操作的開銷。
所以,你會發現目前的話,不論是各種開源框架還是 JDK 原始碼都大量使用了 synchronized
關鍵字。
三種使用方式
// 1 修飾例項方法,獲取當前物件例項的鎖
synchronized void method() {
// code
}
// 2 修飾靜態方法, 獲得當前class的鎖,所以1和2兩個方法不會衝突,不是一個鎖
synchronized static void method() {
// code
}
// 3 修飾程式碼塊,獲取指定物件的鎖
synchronized(this) {
// code
}
// 儘量不要使用 synchronized(String a) 因為 JVM 中,字串常量池具有快取功能!
volatile
可見性,是指執行緒之間的可見性,一個執行緒修改的狀態對另一個執行緒是可見的。也就是一個執行緒修改的結果。另一個執行緒馬上就能看到。比如:用volatile修飾的變數,就會具有可見性。volatile修飾的變數不允許執行緒內部快取和重排序,即直接修改記憶體。所以對其他執行緒是可見的。
雙重校驗鎖實現單例模式
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {}
public static Singleton getUniqueInstance() {
// 基本判斷不加鎖,提升效率
// 不會出現就算是有例項了也要加鎖情況
if (uniqueInstance == null) {
// 加鎖是因為此時可能有執行緒初始化好了例項
synchronized (Singleton.class) {
if(uniqueInstance == null) {
// volatile 防止指令重排序
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
構造方法本身就屬於執行緒安全的,不存在同步的構造方法一說。
Synchronized
原理
public class SynchronizedDemo {
public void method() {
synchronized (this) {
System.out.println("synchronized 程式碼塊");
}
}
}
通過 JDK 自帶的 javap
命令檢視 SynchronizedDemo
類的相關位元組碼資訊:
$ javap -c -s -v -l SynchronizedDemo.class
Classfile .../SynchronizedDemo.class
Last modified 2021年9月11日; size 546 bytes
MD5 checksum a4e4607df26d89d05a469490ff762e94
Compiled from "SynchronizedDemo.java"
...
public void method();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter # 指向同步程式碼塊的開始位置
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String synchronized code block
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit # 指向同步程式碼塊的結束位置
14: goto 22
17: astore_2
18: aload_1
19: monitorexit
20: aload_2
21: athrow
22: return
Exception table:
from to target type
4 14 17 any
17 20 17 any
LineNumberTable:
line 5: 0
line 6: 4
line 7: 12
line 8: 22
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 17
locals = [ class com/lhq/SynchronizedDemo, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
}
SourceFile: "SynchronizedDemo.java"
當執行 monitorenter
指令時,執行緒試圖獲取鎖也就是獲取 物件監視器 monitor
的持有權。wait/notify
等方法也依賴於monitor
物件,這就是為什麼只有在同步的塊或者方法中才能呼叫wait/notify
等方法,否則會丟擲java.lang.IllegalMonitorStateException
的異常的原因。
synchronized
修飾的方法並沒有 monitorenter
指令和 monitorexit
指令,取得代之的確實是 ACC_SYNCHRONIZED
標識,該標識指明瞭該方法是一個同步方法。JVM 通過該 ACC_SYNCHRONIZED
訪問標誌來辨別一個方法是否宣告為同步方法,從而執行相應的同步呼叫。
兩者的本質都是對物件監視器 monitor 的獲取。
ReentrantLock
可重入鎖
重入鎖實現可重入性原理或機制是:每一個鎖關聯一個執行緒持有者和計數器,當計數器為 0 時表示該鎖沒有被任何執行緒持有,那麼任何執行緒都可能獲得該鎖而呼叫相應的方法;當某一執行緒請求成功後,JVM 會記下鎖的持有執行緒,並且將計數器置為 1;此時其它執行緒請求該鎖,則必須等待;而該持有鎖的執行緒如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增;當執行緒退出同步程式碼塊時,計數器會遞減,如果計數器為 0,則釋放該鎖。
ReentrantLock
與synchronized
的比較
相似點
它們都是加鎖方式同步,而且都是阻塞式的同步,也就是說當如果一個執行緒獲得了物件鎖,進入了同步塊,其他訪問該同步塊的執行緒都必須阻塞在同步塊外面等待,等到釋放掉鎖或者喚醒後才能繼續獲得鎖。
都是可重入鎖。
區別
對於Synchronized來說,它是java語言的關鍵字,是原生語法層面的互斥,需要jvm實現。而ReentrantLock它是JDK 1.5之後提供的API層面的互斥鎖,需要lock()和unlock()方法配合try/finally語句塊來完成
便利性:Synchronized的使用比較方便簡潔,並且由編譯器去保證鎖的加鎖和釋放,而ReenTrantLock需要手工宣告來加鎖和釋放鎖,為了避免忘記手工釋放鎖造成死鎖,所以最好在finally中宣告釋放鎖。
虛擬機器團隊在 JDK1.6 為 synchronized
關鍵字進行了很多優化,但是這些優化都是在虛擬機器層面實現的,並沒有直接暴露給我們。
相比synchronized
,ReentrantLock
增加了一些高階功能。主要來說主要有三點:
- 等待可中斷 :
ReentrantLock
提供了一種能夠中斷等待鎖的執行緒的機制,通過lock.lockInterruptibly()
來實現這個機制。也就是說正在等待的執行緒可以選擇放棄等待,改為處理其他事情。
lock
優先考慮獲取鎖,待獲取鎖成功後,才響應中斷。
lockInterruptibly
優先考慮響應中斷,而不是響應鎖的普通獲取或重入獲取。
ReentrantLock.lockInterruptibly
允許在等待時,由其它執行緒呼叫等待執行緒的Thread.interrupt
方法來中斷等待執行緒的等待而直接返回,這時不用獲取鎖,而會丟擲一個InterruptedException
。ReentrantLock.lock
方法不允許Thread.interrupt
中斷,即使檢測到Thread.isInterrupted
,一樣會繼續嘗試獲取鎖,失敗則繼續休眠。只是在最後獲取鎖成功後再把當前執行緒置為interrupted
狀態,然後再中斷執行緒。
public class SynchronizedDemo {
public static void main(String[] args) throws InterruptedException {
final Lock lock = new ReentrantLock();
lock.lock();
Thread.sleep(1000);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
//lock.lock(); // 即使執行了interrupt()方法也沒有反應
try {
lock.lockInterruptibly();
// 輸出 Thread-0 interrupted.丟擲異常
// 見程式碼最後
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" interrupted.");
}
});
t1.start();
Thread.sleep(1000);
t1.interrupt();
Thread.sleep(1000);
}
}
/*
java.lang.InterruptedException
at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchronizer.java:1222)
at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335)
at com.lhq.SynchronizedDemo$1.run(SynchronizedDemo.java:18)
at java.lang.Thread.run(Thread.java:748)
*/
- 可實現公平鎖 :
ReentrantLock
可以指定是公平鎖還是非公平鎖。而synchronized
只能是非公平鎖。所謂的公平鎖就是先等待的執行緒先獲得鎖。ReentrantLock
預設情況是非公平的,可以通過ReentrantLock
類的ReentrantLock(boolean fair)
構造方法來制定是否是公平的。 - 可實現選擇性通知(鎖可以繫結多個條件):
synchronized
關鍵字與wait()
和notify()
/notifyAll()
方法相結合可以實現等待/通知機制。ReentrantLock
類當然也可以實現,但是需要藉助於Condition
介面與newCondition()
方法。
公平鎖在公平性得以保障,但因為公平的獲取鎖沒有考慮到作業系統對執行緒的排程因素以及其他因素,會影響效能。非公平鎖:飢餓問題。