用程式碼說話:synchronized關鍵字和多執行緒訪問同步方法的7種情況
synchronized關鍵字在多執行緒併發程式設計中一直是元老級角色的存在,是學習併發程式設計中必須面對的坎,也是走向Java高階開發的必經之路。
一、synchronized性質
synchronized是Java提供的內建鎖機制,有如下兩種特性:
互斥性:即在同一時間最多隻有一個執行緒能持有這種鎖。當執行緒1嘗試去獲取一個由執行緒2持有的鎖時,執行緒1必須等待或者阻塞,知道執行緒2釋放這個鎖。如果執行緒2永遠不釋放鎖,那麼執行緒1將永遠等待下去。
可重入性:即某個執行緒可以獲取一個已經由自己持有的鎖。
二、synchronized用法
Java中的每個物件都可以作為鎖。根據鎖物件的不同,synchronized的用法可以分為以下兩種:
物件鎖:包括方法鎖(預設鎖物件為this當前例項物件)和同步程式碼塊鎖(自己制定鎖物件)
類鎖:指的是synchronized修飾靜態的方法或指定鎖為Class物件。
三、多執行緒訪問同步方法的7種情況
本部分針對面試中常考的7中情況進行程式碼實戰和原理解釋。
1. 兩個執行緒同時訪問一個物件的同步方法
/** * 兩個執行緒同時訪問一個物件的同步方法 */ public class Demo1 implements Runnable { static Demo1 instance = new Demo1(); @Override public void run() { fun(); } public synchronized void fun() { System.out.println(Thread.currentThread().getName() + "開始執行"); try { Thread.sleep(2000); } 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("finished"); } }
結果:兩個執行緒順序執行。
解釋:thread1和thread2共用一把鎖instance;同一時刻只能有一個執行緒獲取鎖;thread1先啟動,先獲得到鎖,先執行,此時thread2只能等待。當thread1釋放鎖之後,thread2獲取到鎖,進行執行。
2. 兩個執行緒訪問的是兩個物件的同步方法
public class Demo2 implements Runnable{ static Demo2 instance1 = new Demo2(); static Demo2 instance2 = new Demo2(); @Override public void run() { fun(); } public synchronized void fun() { System.out.println(Thread.currentThread().getName() + "開始執行"); try { Thread.sleep(2000); } 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("finished"); } }
結果: 兩個執行緒並行執行。
解釋:thread1使用的鎖物件是instance1,thread2使用的鎖物件是instance2,兩個物件使用的鎖物件不是同一個,所以執行緒之間互不影響,是並行執行的。
3. 兩個執行緒訪問的是synchronized的靜態方法
public class Demo3 implements Runnable{
static Demo3 instance1 = new Demo3();
static Demo3 instance2 = new Demo3();
@Override
public void run() {
fun();
}
public static synchronized void fun() {
System.out.println(Thread.currentThread().getName() + "開始執行");
try {
Thread.sleep(2000);
} 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("finished");
}
}
結果:兩個執行緒順序執行。
解釋:雖然兩個執行緒使用了兩個不同的instance例項,但是隻要方法是靜態的,對應的鎖物件是同一把鎖,需要先後獲取到鎖進行執行。
4. 同時訪問同步方法與非同步方法
public class Demo4 implements Runnable {
static Demo4 instance = new Demo4();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")){
fun1();
}else{
fun2();
}
}
public synchronized void fun1() {
System.out.println(Thread.currentThread().getName() + "開始執行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "fun1執行結束");
}
public void fun2() {
System.out.println(Thread.currentThread().getName() + "fun2開始執行");
try {
Thread.sleep(2000);
} 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("finished");
}
}
結果:兩個執行緒並行執行。
解釋:synchronize的關鍵字只對fun1起作用,不會對其他方法造成影響。也就是說同步方法不會對非同步方法造成影響,兩個方法並行執行。
### 5. 訪問同一個物件的不同的普通同步方法
public class Demo5 implements Runnable {
static Demo5 instance = new Demo5();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")){
fun1();
}else{
fun2();
}
}
public synchronized void fun1() {
System.out.println(Thread.currentThread().getName() + "開始執行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "fun1執行結束");
}
public synchronized void fun2() {
System.out.println(Thread.currentThread().getName() + "fun2開始執行");
try {
Thread.sleep(2000);
} 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("finished");
}
}
結果:順序執行。
解釋:兩個方法共用了instance物件鎖,兩個方法無法同時執行,只能先後執行。
6. 同時訪問靜態synchronized和非靜態的synchronized方法
public class Demo6 implements Runnable{
static Demo6 instance = new Demo6();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")){
fun1();
}else{
fun2();
}
}
public static synchronized void fun1() {
System.out.println(Thread.currentThread().getName() + "開始執行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "fun1執行結束");
}
public synchronized void fun2() {
System.out.println(Thread.currentThread().getName() + "fun2開始執行");
try {
Thread.sleep(2000);
} 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("finished");
}
}
結果:兩個執行緒並行執行
解釋:有static關鍵字,鎖的是類本身;沒有static關鍵字,鎖的是物件例項;鎖不是同一把鎖,兩個鎖之間是沒有衝突的;所以兩個執行緒可以並行執行。
7. 方法拋異常後,會釋放鎖
public class Demo7 implements Runnable{
static Demo7 instance = new Demo7();
@Override
public void run() {
if (Thread.currentThread().getName().equals("Thread-0")){
fun1();
}else{
fun2();
}
}
public synchronized void fun1() {
System.out.println(Thread.currentThread().getName() + "開始執行");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
throw new RuntimeException();
//System.out.println(Thread.currentThread().getName() + "fun1執行結束");
}
public synchronized void fun2() {
System.out.println(Thread.currentThread().getName() + "fun2開始執行");
try {
Thread.sleep(2000);
} 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("finished");
}
}
結果:thread1執行時遇到異常,並未執行結束,thread2開始執行,並執行至結束。
解釋:方法丟擲異常後,JVM自動釋放鎖。
8. 上述7種情況總結
3點核心思想:
一把鎖只能同時被一個執行緒獲取,沒有拿到鎖的執行緒必須等待。
每個例項都對應有自己的一把鎖,不同例項之間互不影響;例外:鎖物件是.class以及synchronized修飾的是static方法的時候,所有物件共用同一把鎖。
無論是方法正常執行完畢或者方法丟擲異常,都會釋放鎖。
四、synchronized和ReentrantLock比較
雖然ReentrantLock是更加高階的鎖機制,但是synchronized依然存在著如下的優點:
synchronized作為內建鎖為更多的開發人員所熟悉,程式碼簡潔;
synchronized較ReentrantLock更加安全,ReentrantLock如果忘記在finally中釋放鎖,雖然程式碼表面上執行正常,但實際上已經留下了隱患
synchronized線上程轉儲中能給出在哪些呼叫幀中獲得了哪些瑣,並能夠檢測和識別發生死鎖的執行緒。
五、總結
synchronized關鍵字是Java提供的一種互斥的、可重入的內建鎖機制。
其有兩種用法:物件鎖和類鎖。
雖然synchronized與高階鎖相比有著不夠靈活、效率低等不足,但也有自身的優勢:安全,依然是併發程式設計領域不得不學習的重要知識點。