1.Java的鎖
1.1 鎖的記憶體語義
- 鎖可以讓臨界區互斥執行,還可以讓釋放鎖的執行緒向同一個鎖的執行緒傳送訊息
- 鎖的釋放要遵循Happens-before原則(
鎖規則:解鎖必然發生在隨後的加鎖之前
) - 鎖在Java中的具體表現是
Synchronized
和Lock
1.2 鎖的釋放
執行緒A釋放鎖後,會將共享變更操作重新整理到主記憶體中
1.3 鎖的獲取
執行緒B獲取鎖時,JMM會將該執行緒的本地記憶體置為無效,被監視器保護的臨界區程式碼必須從主記憶體中讀取共享變數
1.4 鎖的釋放與獲取
- 鎖獲取與volatile讀有相同的記憶體語義,讀者可參見筆者的 併發番@Java記憶體模型&Volatile一文通(1.7版)
- 執行緒A釋放一個鎖,實質是執行緒A告知下一個獲取到該鎖的某個執行緒其已變更該共享變數
- 執行緒B獲取一個鎖,實質是執行緒B得到了執行緒A告知其(在釋放鎖之前)變更共享變數的訊息
- 執行緒A釋放鎖,隨後執行緒B競爭到該鎖,實質是執行緒A通過主記憶體向執行緒B發訊息告知其變更了共享變數
2.Synchronized的綜述
- 同步機制: synchronized是Java同步機制的一種實現,即互斥鎖機制,它所獲得的鎖叫做互斥鎖
- 互斥鎖: 指的是每個物件的鎖一次只能分配給一個執行緒,同一時間只能由一個執行緒佔用
- 作用: synchronized用於保證同一時刻只能由一個執行緒進入到臨界區,同時保證共享變數的可見性、原子性和有序性
- 使用: 當一個執行緒試圖訪問同步程式碼方法(塊)時,它首先必須得到鎖,退出或丟擲異常時必須釋放鎖
3.Synchronized的使用
3.1 Synchronized的三種應用方式
補充: 使用同步程式碼塊的好處在於其他執行緒仍可以訪問非synchronized(this)的同步程式碼塊
3.2 Synchronized的使用規則
/**
* 先定義一個測試模板類
* 這裡補充一個知識點:Thread.sleep(long)不會釋放鎖
* 讀者可參見筆者的`併發番@Thread一文通`
*/
public class SynchronizedDemo {
public static synchronized void staticMethod(){
System.out.println(Thread.currentThread().getName() + "訪問了靜態同步方法staticMethod");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "結束訪問靜態同步方法staticMethod");
}
public static void staticMethod2(){
System.out.println(Thread.currentThread().getName() + "訪問了靜態同步方法staticMethod2");
synchronized (SynchronizedDemo.class){
System.out.println(Thread.currentThread().getName() + "在staticMethod2方法中獲取了SynchronizedDemo.class");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void synMethod(){
System.out.println(Thread.currentThread().getName() + "訪問了同步方法synMethod");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "結束訪問同步方法synMethod");
}
public synchronized void synMethod2(){
System.out.println(Thread.currentThread().getName() + "訪問了同步方法synMethod2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "結束訪問同步方法synMethod2");
}
public void method(){
System.out.println(Thread.currentThread().getName() + "訪問了普通方法method");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "結束訪問普通方法method");
}
private Object lock = new Object();
public void chunkMethod(){
System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod方法");
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "在chunkMethod方法中獲取了lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void chunkMethod2(){
System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod2方法");
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "在chunkMethod2方法中獲取了lock");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void chunkMethod3(){
System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod3方法");
//同步程式碼塊
synchronized (this){
System.out.println(Thread.currentThread().getName() + "在chunkMethod3方法中獲取了this");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public void stringMethod(String lock){
synchronized (lock){
while (true){
System.out.println(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
複製程式碼
3.2.1 普通方法與同步方法呼叫互不關聯
當一個執行緒進入同步方法時,其他執行緒可以正常訪問其他非同步方法
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
//呼叫普通方法
synDemo.method();
});
Thread thread2 = new Thread(() -> {
//呼叫同步方法
synDemo.synMethod();
});
thread1.start();
thread2.start();
}
---------------------
//輸出:
Thread-1訪問了同步方法synMethod
Thread-0訪問了普通方法method
Thread-0結束訪問普通方法method
Thread-1結束訪問同步方法synMethod
//分析:通過結果可知,普通方法和同步方法是非阻塞執行的
複製程式碼
3.2.2 所有同步方法只能被一個執行緒訪問
當一個執行緒執行同步方法時,其他執行緒不能訪問任何同步方法
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
synDemo.synMethod();
synDemo.synMethod2();
});
Thread thread2 = new Thread(() -> {
synDemo.synMethod2();
synDemo.synMethod();
});
thread1.start();
thread2.start();
}
---------------------
//輸出:
Thread-0訪問了同步方法synMethod
Thread-0結束訪問同步方法synMethod
Thread-0訪問了同步方法synMethod2
Thread-0結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod2
Thread-1結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod
Thread-1結束訪問同步方法synMethod
//分析:通過結果可知,任務的執行是阻塞的,顯然Thread-1必須等待Thread-0執行完畢之後才能繼續執行
複製程式碼
3.2.3 同一個鎖的同步程式碼塊同一時刻只能被一個執行緒訪問
當同步程式碼塊都是同一個鎖時,方法可以被所有執行緒訪問,但同一個鎖的同步程式碼塊同一時刻只能被一個執行緒訪問
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
//呼叫同步塊方法
synDemo.chunkMethod();
synDemo.chunkMethod2();
});
Thread thread2 = new Thread(() -> {
//呼叫同步塊方法
synDemo.chunkMethod();
synDemo.synMethod2();
});
thread1.start();
thread2.start();
}
---------------------
//輸出:
Thread-0訪問了chunkMethod方法
Thread-1訪問了chunkMethod方法
Thread-0在chunkMethod方法中獲取了lock
...停頓等待...
Thread-1在chunkMethod方法中獲取了lock
...停頓等待...
Thread-0訪問了chunkMethod2方法
Thread-0在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1訪問了chunkMethod2方法
Thread-1在chunkMethod2方法中獲取了lock
//分析可知:
//1.對比18行和19行可知,即使普通方法有同步程式碼塊,但方法的訪問是非阻塞的,任何執行緒都可以自由進入
//2.對比20行、22行以及25行和27行可知,對於同一個鎖的同步程式碼塊的訪問一定是阻塞的
複製程式碼
3.2.4 執行緒間同時訪問同一個鎖的多個同步程式碼的執行順序不定
- 執行緒間同時訪問同一個鎖多個同步程式碼的執行順序不定,即使是使用同一個物件鎖,這點跟同步方法有很大差異
- ??讀者可以先思考為什麼會出現這樣的問題??
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
//呼叫同步塊方法
synDemo.chunkMethod();
synDemo.chunkMethod2();
});
Thread thread2 = new Thread(() -> {
//呼叫同步塊方法
synDemo.chunkMethod2();
synDemo.chunkMethod();
});
thread1.start();
thread2.start();
}
---------------------
//輸出:
Thread-0訪問了chunkMethod方法
Thread-1訪問了chunkMethod2方法
Thread-0在chunkMethod方法中獲取了lock
...停頓等待...
Thread-0訪問了chunkMethod2方法
Thread-1在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1訪問了chunkMethod方法
Thread-0在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1在chunkMethod方法中獲取了lock
//分析可知:
//現象:對比20行、22行和24行、25行可知,雖然是同一個lock物件,但其不同程式碼塊的訪問是非阻塞的
//原因:根源在於鎖的釋放和重新競爭,當Thread-0訪問完chunkMethod方法後會先釋放鎖,這時Thread-1就有機會能獲取到鎖從而優先執行,依次類推到24行、25行時,Thread-0又重新獲取到鎖優先執行了
//注意:但有一點是必須的,對於同一個鎖的同步程式碼塊的訪問一定是阻塞的
//補充:同步方法之所有會被全部阻塞,是因為synDemo物件一直被執行緒在內部把持住就沒釋放過,論把持住的重要性!
複製程式碼
3.2.5 不同鎖之間訪問非阻塞
- 由於三種使用方式的鎖物件都不一樣,因此相互之間不會有任何影響
- 但有兩種情況除外:
- 1.當同步程式碼塊使用的Class物件和類物件一致時屬於同一個鎖,遵循上面的
3.2.3
原則 - 2.當同步程式碼塊使用的是this,即與同步方法使用鎖屬於同一個鎖,遵循上面的
3.2.2
和3.2.3
原則
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> synDemo.chunkMethod() );
Thread thread2 = new Thread(() -> synDemo.chunkMethod3());
Thread thread3 = new Thread(() -> staticMethod());
Thread thread4 = new Thread(() -> staticMethod2());
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
---------------------
//輸出:
Thread-1訪問了chunkMethod3方法
Thread-1在chunkMethod3方法中獲取了this
Thread-2訪問了靜態同步方法staticMethod
Thread-0訪問了chunkMethod方法
Thread-0在chunkMethod方法中獲取了lock
Thread-3訪問了靜態同步方法staticMethod2
...停頓等待...
Thread-2結束訪問靜態同步方法staticMethod
Thread-3在staticMethod2方法中獲取了SynchronizedDemo.class
//分析可知:
//現象:對比16行、18行和24行、25行可知,雖然是同一個lock物件,但其不同程式碼塊的訪問是非阻塞的
//原因:根源在於鎖的釋放和重新競爭,當Thread-0訪問完chunkMethod方法後會先釋放鎖,這時Thread-1就有機會能獲取到鎖從而優先執行,依次類推到24行、25行時,Thread-0又重新獲取到鎖優先執行了
複製程式碼
3.3 Synchronized的可重入性
- 重入鎖:當一個執行緒再次請求自己持有物件鎖的臨界資源時,這種情況屬於重入鎖,請求將會成功
- 實現:一個執行緒得到一個物件鎖後再次請求該物件鎖,是允許的,每重入一次,monitor進入次數+1
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> {
synDemo.synMethod();
synDemo.synMethod2();
});
Thread thread2 = new Thread(() -> {
synDemo.synMethod2();
synDemo.synMethod();
});
thread1.start();
thread2.start();
}
---------------------
//輸出:
Thread-0訪問了同步方法synMethod
Thread-0結束訪問同步方法synMethod
Thread-0訪問了同步方法synMethod2
Thread-0結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod2
Thread-1結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod
Thread-1結束訪問同步方法synMethod
//分析:對比16行和18行可知,在程式碼塊中繼續呼叫了當前例項物件的另外一個同步方法,再次請求當前例項鎖時,將被允許,進而執行方法體程式碼,這就是重入鎖最直接的體現
複製程式碼
3.4 Synchronized與String鎖
- 隱患:由於在JVM中具有String常量池快取的功能,因此相同字面量是同一個鎖!!!
- 注意:嚴重不推薦將String作為鎖物件,而應該改用其他非快取物件
- 提示:對字面量有疑問的話請先回顧一下String的基礎,這裡不加以解釋
public static void main(String[] args) {
SynchronizedDemo synDemo = new SynchronizedDemo();
Thread thread1 = new Thread(() -> synDemo.stringMethod("sally"));
Thread thread2 = new Thread(() -> synDemo.stringMethod("sally"));
thread1.start();
thread2.start();
}
---------------------
//輸出:
Thread-0
Thread-0
Thread-0
Thread-0
...死迴圈...
//分析:輸出結果永遠都是Thread-0的死迴圈,也就是說另一個執行緒,即Thread-1執行緒根本不會執行
//原因:同步塊中的鎖是同一個字面量
複製程式碼
3.5 Synchronized與不可變鎖
- 隱患:當使用不可變類物件(final Class)作為物件鎖時,使用synchronized同樣會有併發問題
- 原因:由於不可變特性,當作為鎖但同步塊內部仍然有計算操作,會生成一個新的鎖物件
- 注意:嚴重不推薦將final Class作為鎖物件時仍對其有計算操作
- 補充:雖然String也是final Class,但它的原因卻是字面量常量池
public class SynchronizedDemo {
static Integer i = 0; //Integer是final Class
public static void main(String[] args) throws InterruptedException {
Runnable runnable = new Runnable() {
@Override
public void run() {
for (int j = 0;j<10000;j++){
synchronized (i){
i++;
}
}
}
};
Thread thread1 = new Thread(runnable);
Thread thread2 = new Thread(runnable);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(i);
}
}
---------------------
//輸出:
14134
//分析:跟預想中的20000不一致,當使用Integer作為物件鎖時但還有計算操作就會出現併發問題
複製程式碼
我們通過反編譯發現執行i++操作相當於執行了i = Integer.valueOf(i.intValue()+1)
通過檢視Integer的valueOf方法實現可知,其每次都new了一個新的Integer物件,鎖變了有木有!!!
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i); //每次都new一個新的鎖有木有!!!
}
複製程式碼
3.6 Synchronized與死鎖
- 死鎖:當執行緒間需要相互等待對方已持有的鎖時,就形成死鎖,進而產生死迴圈
- 注意: 程式碼中嚴禁出現死鎖!!!
public static void main(String[] args) {
Object lock = new Object();
Object lock2 = new Object();
Thread thread1 = new Thread(() -> {
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "獲取到lock鎖");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "獲取到lock2鎖");
}
}
});
Thread thread2 = new Thread(() -> {
synchronized (lock2){
System.out.println(Thread.currentThread().getName() + "獲取到lock2鎖");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock){
System.out.println(Thread.currentThread().getName() + "獲取到lock鎖");
}
}
});
thread1.start();
thread2.start();
}
---------------------
//輸出:
Thread-1獲取到lock2鎖
Thread-0獲取到lock鎖
.....
//分析:執行緒0獲得lock鎖,執行緒1獲得lock2鎖,但之後由於兩個執行緒還要獲取對方已持有的鎖,但已持有的鎖都不會被雙方釋放,執行緒"假死",無法往下執行,從而形成死迴圈,即死鎖,之後一直在做無用的死迴圈,嚴重浪費系統資源
複製程式碼
我們用 jstack 檢視一下這個任務的各個執行緒執行情況,可以發現兩個執行緒都被阻塞 BLOCKED
我們很明顯的發現,Java-level=deadlock,即死鎖,兩個執行緒相互等待對方的鎖
Synchronized一文通(1.8版) 由 黃志鵬kira 創作,採用 知識共享 署名-非商業性使用 4.0 國際 許可協議進行許可。