原始碼解析Synchronized同步方法的八種使用場景,收藏
簡介
本文將介紹8種同步方法的訪問場景,我們來看看這八種情況下,多執行緒訪問同步方法是否還是執行緒安全的。這些場景是多執行緒程式設計中經常遇到的,而且也是面試時高頻被問到的問題,所以不管是理論還是實踐,這些都是多執行緒場景必須要掌握的場景。
好了,話不多說,看正文吧,關注公眾號:Java架構師聯盟
場景一:兩個執行緒同時訪問同一個物件的同步方法
分析:這種情況是經典的物件鎖中的方法鎖,兩個執行緒爭奪同一個物件鎖,所以會相互等待,是執行緒安全的。
程式碼實現
package com.syn; /** * @author :biws * @date :Created in 2020/12/21 22:27 * @description:兩個執行緒同時訪問同一個物件的同步方法 */ public class Condition1 implements Runnable { private static Condition1 instance = new Condition1(); @Override public void run() { method(); } //關鍵:synchronized可以保證此方法被順序執行,執行緒1執行完4秒鐘後,執行緒2再執行4秒。不加synchronized,執行緒1和執行緒2將同時執行 private synchronized void method() { System.out.println("執行緒:" + Thread.currentThread().getName() + ",執行開始"); try { //模擬執行一段操作,耗時4秒鐘 Thread.sleep(4000); System.out.println("執行緒:" + Thread.currentThread().getName() + ",執行結束"); } catch (InterruptedException e) { e.printStackTrace(); } } public static void main(String[] args) { // 模擬:同一個物件下,兩個執行緒,同步執行一個方法(序列執行則為執行緒安全,並行執行,則為執行緒不安全,) Thread thread1 = new Thread(instance); Thread thread2 = new Thread(instance); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()) { } System.out.println("測試結束"); } }
執行結果
結果分析
發現執行結果中,多個執行緒也是序列執行的,效果跟同步程式碼塊鎖是一致的。
場景二:兩個執行緒同時訪問兩個物件的同步方法
這種場景就是物件鎖失效的場景,原因出在訪問的是兩個物件的同步方法,那麼這兩個執行緒分別持有的兩個執行緒的鎖,所以是互相不會受限的。加鎖的目的是為了讓多個執行緒競爭同一把鎖,而這種情況多個執行緒之間不再競爭同一把鎖,而是分別持有一把鎖
程式碼驗證:
package com.syn; /** * @author :biws * @date :Created in 2020/12/21 22:10 * @description:兩個執行緒同時訪問兩個物件的同步方法 */ public class Condition2 implements Runnable { // 建立兩個不同的物件 static Condition2 instance1 = new Condition2(); static Condition2 instance2 = new Condition2(); @Override public void run() { method(); } private synchronized void method() { System.out.println("執行緒名:" + Thread.currentThread().getName() + ",執行開始"); try { Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("執行緒:" + Thread.currentThread().getName() + ",執行結束"); } public static void main(String[] args) { Thread thread1 = new Thread(instance1); Thread thread2 = new Thread(instance2); thread1.start(); thread2.start(); while (thread1.isAlive() || thread2.isAlive()) { } System.out.println("測試結束"); } }
執行結果:
兩個執行緒是並行執行的,所以執行緒不安全。
程式碼分析:
「問題在此:」兩個執行緒(thread1、thread2),訪問兩個物件(instance1、instance2)的同步方法(method()),兩個執行緒都有各自的鎖,不能形成兩個執行緒競爭一把鎖的局勢,所以這時,synchronized修飾的方法method()和不用synchronized修飾的效果一樣(不信去把synchronized關鍵字去掉,執行結果一樣),所以此時的method()只是個普通方法。
「如何解決這個問題:」若要使鎖生效,只需將method()方法用static修飾,這樣就形成了類鎖,多個例項(instance1、instance2)共同競爭一把類鎖,就可以使兩個執行緒序列執行了。
場景三:兩個執行緒同時訪問(一個或兩個)物件的靜態同步方法
這個場景解決的是場景二中出現的執行緒不安全問題,即用類鎖實現
程式碼實現
package com.syn;
/**
* @author :biws
* @date :Created in 2020/12/21 22:32
* @description:靜態方法鎖的方式實現類鎖
*/
public class Condition3 implements Runnable {
// 這兩個例項,宣告為static,是因為要在main方法中測試,與方法鎖無關
static Condition3 instance1 = new Condition3();
static Condition3 instance2 = new Condition3();
// 關鍵: static synchronized兩個關鍵字同時使用
private static synchronized void method() {
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",執行開始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒:" + Thread.currentThread().getName() + ",執行結束");
}
@Override
public void run() {
method();
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance1);
Thread thread2 = new Thread(instance2);
thread1.start();
thread2.start();
try {
//兩個執行緒必須都執行完畢後
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("兩個執行緒都已經執行完畢");
}
}
結果展示
場景四:兩個執行緒分別同時訪問(一個或兩個)物件的同步方法和非同步方法
這個場景是兩個執行緒其中一個訪問同步方法,另一個訪問非同步方法,此時程式會不會序列執行呢,也就是說是不是執行緒安全的呢?我們可以確定是執行緒不安全的,如果方法不加synchronized都是安全的,那就不需要同步方法了。
package com.syn;
/**
* @author :biws
* @date :Created in 2020/12/21 22:12
* @description:兩個執行緒同時訪問(一個或兩個)物件的靜態同步方法
*/
public class Condition4 implements Runnable {
static Condition4 instance = new Condition4();
@Override
public void run() {
//兩個執行緒訪問同步方法和非同步方法
if (Thread.currentThread().getName().equals("Thread-0")) {
//執行緒0,執行同步方法method0()
method0();
}
if (Thread.currentThread().getName().equals("Thread-1")) {
//執行緒1,執行非同步方法method1()
method1();
}
}
// 同步方法
private synchronized void method0() {
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",同步方法,執行開始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒:" + Thread.currentThread().getName() + ",同步方法,執行結束");
}
// 普通方法
private void method1() {
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",普通方法,執行開始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒:" + Thread.currentThread().getName() + ",普通方法,執行結束");
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("測試結束");
}
}
執行結果:
兩個執行緒是並行執行的,所以是執行緒不安全的。
結果分析
「問題在於此:」 method1沒有被synchronized修飾,所以不會受到鎖的影響。即便是在同一個物件中,當然在多個例項中,更不會被鎖影響了。
場景五:兩個執行緒訪問同一個物件中的同步方法,同步方法又呼叫一個非同步方法
我們來實驗下這個場景,用兩個執行緒呼叫同步方法,在同步方法中呼叫普通方法;再用一個執行緒直接呼叫普通方法,看看是否是執行緒安全的?
package com.syn;
/**
* @author :biws
* @date :Created in 2020/12/21 22:14
* @description:兩個執行緒訪問同一個物件中的同步方法,同步方法又呼叫一個非同步方法
*/
public class Condition5 implements Runnable {
static Condition5 instance = new Condition5();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
//直接呼叫普通方法
method2();
} else {
// 先呼叫同步方法,在同步方法內呼叫普通方法
method1();
}
}
// 同步方法
private static synchronized void method1() {
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",同步方法,執行開始");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒:" + Thread.currentThread().getName() + ",同步方法,執行結束,開始呼叫普通方法");
method2();
}
// 普通方法
private static void method2() {
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",普通方法,執行開始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒:" + Thread.currentThread().getName() + ",普通方法,執行結束");
}
public static void main(String[] args) {
// 此執行緒直接呼叫普通方法
Thread thread0 = new Thread(instance);
// 這兩個執行緒直接呼叫同步方法
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread0.start();
thread1.start();
thread2.start();
while (thread0.isAlive() || thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("測試結束");
}
}
執行結果:
結果分析:
我們可以看出,普通方法被兩個執行緒並行執行,不是執行緒安全的。這是為什麼呢?
因為如果非同步方法,有任何其他執行緒直接呼叫,而不是僅在呼叫同步方法時,才呼叫非同步方法,此時會出現多個執行緒並行執行非同步方法的情況,執行緒就不安全了。
對於同步方法中呼叫非同步方法時,要想保證執行緒安全,就必須保證非同步方法的入口,僅出現在同步方法中。但這種控制方式不夠優雅,若被不明情況的人直接呼叫非同步方法,就會導致原有的執行緒同步不再安全。所以不推薦大家在專案中這樣使用,但我們要理解這種情況,並且我們要用語義明確的、讓人一看就知道這是同步方法的方式,來處理執行緒安全的問題。
所以,最簡單的方式,是在非同步方法上,也加上synchronized關鍵字,使其變成一個同步方法,這樣就變成了《場景五:兩個執行緒同時訪問同一個物件的不同的同步方法》,這種場景下,大家就很清楚的看到,同一個物件中的兩個同步方法,不管哪個執行緒呼叫,都是執行緒安全的了。
場景六:兩個執行緒同時訪問同一個物件的不同的同步方法
這個場景也是在探討物件鎖的作用範圍,物件鎖的作用範圍是物件中的所有同步方法。所以,當訪問同一個物件中的多個同步方法時,結論是:
package com.syn;
/**
* @author :biws
* @date :Created in 2020/12/21 22:16
* @description:兩個執行緒同時訪問同一個物件的不同的同步方法
*/
public class Condition6 implements Runnable {
static Condition6 instance = new Condition6();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
//執行緒0,執行同步方法method0()
method0();
}
if (Thread.currentThread().getName().equals("Thread-1")) {
//執行緒1,執行同步方法method1()
method1();
}
}
private synchronized void method0() {
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",同步方法0,執行開始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒:" + Thread.currentThread().getName() + ",同步方法0,執行結束");
}
private synchronized void method1() {
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",同步方法1,執行開始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒:" + Thread.currentThread().getName() + ",同步方法1,執行結束");
}
//執行結果:序列
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("測試結束");
}
}
執行結果:
結果分析:
兩個方法(method0()和method1())的synchronized修飾符,雖沒有指定鎖物件,但預設鎖物件為this物件為鎖物件, 所以對於同一個例項(instance),兩個執行緒拿到的鎖是同一把鎖,此時同步方法會序列執行。這也是synchronized關鍵字的可重入性的一種體現。
場景七:兩個執行緒分別同時訪問靜態synchronized和非靜態synchronized方法
這種場景的本質也是在探討兩個執行緒獲取的是不是同一把鎖的問題。靜態synchronized方法屬於類鎖,鎖物件是(*.class)物件,非靜態synchronized方法屬於物件鎖中的方法鎖,鎖物件是this物件。兩個執行緒拿到的是不同的鎖,自然不會相互影響。
程式碼實現:
package com.syn;
/**
* @author :biws
* @date :Created in 2020/12/21 22:20
* @description:兩個執行緒分別同時訪問靜態synchronized和非靜態synchronized方法
*/
public class Condition7 implements Runnable {
static Condition7 instance = new Condition7();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
//執行緒0,執行靜態同步方法method0()
method0();
}
if (Thread.currentThread().getName().equals("Thread-1")) {
//執行緒1,執行非靜態同步方法method1()
method1();
}
}
// 重點:用static synchronized 修飾的方法,屬於類鎖,鎖物件為(*.class)物件。
private static synchronized void method0() {
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",靜態同步方法0,執行開始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒:" + Thread.currentThread().getName() + ",靜態同步方法0,執行結束");
}
// 重點:synchronized 修飾的方法,屬於方法鎖,鎖物件為(this)物件。
private synchronized void method1() {
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",非靜態同步方法1,執行開始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒:" + Thread.currentThread().getName() + ",非靜態同步方法1,執行結束");
}
//執行結果:並行
public static void main(String[] args) {
//問題原因: 執行緒1的鎖是類鎖(*.class)物件,執行緒2的鎖是方法鎖(this)物件,兩個執行緒的鎖不一樣,自然不會互相影響,所以會並行執行。
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("測試結束");
}
}
執行結果:
場景八:同步方法丟擲異常後,JVM會自動釋放鎖的情況
本場景探討的是synchronized釋放鎖的場景:
所以,在一個執行緒的同步方法中出現異常的時候,會釋放鎖,另一個執行緒得到鎖,繼續執行。而不會出現一個執行緒丟擲異常後,另一個執行緒一直等待獲取鎖的情況。這是因為JVM在同步方法丟擲異常的時候,會自動釋放鎖物件。
程式碼實現:
package com.syn;
/**
* @author :biws
* @date :Created in 2020/12/21 22:22
* @description:同步方法丟擲異常後,JVM會自動釋放鎖的情況
*/
public class Condition8 implements Runnable {
private static Condition8 instance = new Condition8();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")) {
//執行緒0,執行拋異常方法method0()
method0();
}
if (Thread.currentThread().getName().equals("Thread-1")) {
//執行緒1,執行正常方法method1()
method1();
}
}
private synchronized void method0() {
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",執行開始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//同步方法中,當丟擲異常時,JVM會自動釋放鎖,不需要手動釋放,其他執行緒即可獲取到該鎖
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",丟擲異常,釋放鎖");
throw new RuntimeException();
}
private synchronized void method1() {
System.out.println("執行緒名:" + Thread.currentThread().getName() + ",執行開始");
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("執行緒:" + Thread.currentThread().getName() + ",執行結束");
}
public static void main(String[] args) {
Thread thread1 = new Thread(instance);
Thread thread2 = new Thread(instance);
thread1.start();
thread2.start();
while (thread1.isAlive() || thread2.isAlive()) {
}
System.out.println("測試結束");
}
}
執行結果:
結果分析:
可以看出執行緒還是序列執行的,說明是執行緒安全的。而且出現異常後,不會造成死鎖現象,JVM會自動釋放出現異常執行緒的鎖物件,其他執行緒獲取鎖繼續執行。