在 Java 語言中,保證線程安全性的主要手段是加鎖,而 Java 中的鎖主要有兩種:synchronized 和 Lock,我們今天重點來看一下 synchronized 的幾種用法。
用法簡介
使用 synchronized 無需手動執行加鎖和釋放鎖的操作,我們只需要聲明 synchronized 關鍵字就可以了,JVM 層面會幫我們自動的進行加鎖和釋放鎖的操作。
synchronized 可用於修飾普通方法、靜態方法和代碼塊,接下來我們分別來看。
1、修飾普通方法
synchronized 修飾普通方法的用法如下:
/**
* synchronized 修飾普通方法
*/
public synchronized void method() {
// ....
}
當 synchronized 修飾普通方法時,被修飾的方法被稱為同步方法,其作用范圍是整個方法,作用的對象是調用這個方法的對象。
2、修飾靜態方法
synchronized 修飾靜態方法和修飾普通方法類似,它的用法如下:
/**
* synchronized 修飾靜態方法
*/
public static synchronized void staticMethod() {
// .......
}
當 synchronized 修飾靜態方法時,其作用范圍是整個程序,這個鎖對於所有調用這個鎖的對象都是互斥的。
所謂的互斥,指的是同一時間只能有一個線程能使用,其他線程只能排隊等待。
修飾普通方法 VS 修飾靜態方法
synchronized 修飾普通方法和靜態方法看似相同,但二者完全不同,對於靜態方法來說 synchronized 加鎖是全局的,也就是整個程序運行期間,所有調用這個靜態方法的對象都是互斥的,而普通方法是針對對象級別的,不同的對象對應着不同的鎖,比如以下代碼,同樣是調用兩次方法,但鎖的獲取完全不同,實現代碼如下:
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class SynchronizedUsage {
public static void main(String[] args) throws InterruptedException {
// 創建線程池同時執行任務
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 執行兩次靜態方法
threadPool.execute(() -> {
staticMethod();
});
threadPool.execute(() -> {
staticMethod();
});
// 執行兩次普通方法
threadPool.execute(() -> {
SynchronizedUsage usage = new SynchronizedUsage();
usage.method();
});
threadPool.execute(() -> {
SynchronizedUsage usage2 = new SynchronizedUsage();
usage2.method();
});
}
/**
* synchronized 修飾普通方法
* 本方法的執行需要 3s(因為有 3s 的休眠時間)
*/
public synchronized void method() {
System.out.println("普通方法執行時間:" + LocalDateTime.now());
try {
// 休眠 3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* synchronized 修飾靜態方法
* 本方法的執行需要 3s(因為有 3s 的休眠時間)
*/
public static synchronized void staticMethod() {
System.out.println("靜態方法執行時間:" + LocalDateTime.now());
try {
// 休眠 3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
以上程序的執行結果如下:
從上述結果可以看出,靜態方法加鎖是全局的,針對的是所有調用者;而普通方法加鎖是對象級別的,不同的對象擁有的鎖也不同。
3、修飾代碼塊
我們在日常開發中,最常用的是給代碼塊加鎖,而不是給方法加鎖,因為給方法加鎖,相當於給整個方法全部加鎖,這樣的話鎖的粒度就太大了,程序的執行性能就會受到影響,所以通常情況下,我們會使用 synchronized 給代碼塊加鎖,它的實現語法如下:
public void classMethod() throws InterruptedException {
// 前置代碼...
// 加鎖代碼
synchronized (SynchronizedUsage.class) {
// ......
}
// 后置代碼...
}
從上述代碼我們可以看出,相比於修飾方法,修飾代碼塊需要自己手動指定加鎖對象,加鎖的對象通常使用 this 或 xxx.class 這樣的形式來表示,比如以下代碼:
// 加鎖某個類
synchronized (SynchronizedUsage.class) {
// ......
}
// 加鎖當前類對象
synchronized (this) {
// ......
}
this VS class
使用 synchronized 加鎖 this 和 xxx.class 是完全不同的,當加鎖 this 時,表示用當前的對象進行加鎖,每個對象都對應了一把鎖;而當使用 xxx.class 加鎖時,表示使用某個類(而非類實例)來加鎖,它是應用程序級別的,是全局生效的,如以下代碼所示:
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class SynchronizedUsageBlock {
public static void main(String[] args) throws InterruptedException {
// 創建線程池同時執行任務
ExecutorService threadPool = Executors.newFixedThreadPool(10);
// 執行兩次 synchronized(this)
threadPool.execute(() -> {
SynchronizedUsageBlock usage = new SynchronizedUsageBlock();
usage.thisMethod();
});
threadPool.execute(() -> {
SynchronizedUsageBlock usage2 = new SynchronizedUsageBlock();
usage2.thisMethod();
});
// 執行兩次 synchronized(xxx.class)
threadPool.execute(() -> {
SynchronizedUsageBlock usage3 = new SynchronizedUsageBlock();
usage3.classMethod();
});
threadPool.execute(() -> {
SynchronizedUsageBlock usage4 = new SynchronizedUsageBlock();
usage4.classMethod();
});
}
/**
* synchronized(this) 加鎖
* 本方法的執行需要 3s(因為有 3s 的休眠時間)
*/
public void thisMethod() {
synchronized (this) {
System.out.println("synchronized(this) 加鎖:" + LocalDateTime.now());
try {
// 休眠 3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* synchronized(xxx.class) 加鎖
* 本方法的執行需要 3s(因為有 3s 的休眠時間)
*/
public void classMethod() {
synchronized (SynchronizedUsageBlock.class) {
System.out.println("synchronized(xxx.class) 加鎖:" + LocalDateTime.now());
try {
// 休眠 3s
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
以上程序的執行結果如下:
總結
synchronized 用 3 種用法,用它可以來修飾普通方法、靜態方法和代碼塊,其中最常用的是修飾代碼塊,而修飾代碼塊時需要指定一個加鎖對象,這個加鎖對象通常使用 this 或 xxx.class 來表示,當使用 this 時,表示使用當前對象來加鎖,而使用 class 時,表示表示使用某個類(非類對象實例)來加鎖,它是全局生效的。
是非審之於己,毀譽聽之於人,得失安之於數。
公眾號:Java面試真題解析