大家好,我是王有志。關注王有志,一起聊技術,聊遊戲,從北漂生活談到國際風雲。最近搞了個抽獎送書的活動,歡迎點選連結參與。
如果Java面試有什麼是必問的,synchronized
必定佔據一席之地。初出茅廬時synchronized
的用法,成長後synchronized
的原理,可謂是Java工程師的“一生之敵”。
synchronized都問啥?
按照慣例,先來看synchronized
的常見問題:
根據統計資料可以總結出synchronized
的5大考點:
synchronized
的使用方式:synchronized
是什麼?synchronized
怎麼用?- 不同用法都有什麼效果?
synchronized
的實現原理:synchronized
的特性是如何實現的?synchronized
鎖升級的原理。
今天我們先來看synchronized
的基礎部分。
synchronized是什麼?
synchronized
是Java中的關鍵字,提供了原生同步機制,實現互斥語義和可見性保證,通常稱為互斥鎖。
- 互斥指的是,當執行緒獲取到鎖後,其它試圖獲取鎖的執行緒只能阻塞;
- 可見性指的是,
synchronized
修飾的語句內修改共享變數可以立即被其它執行緒獲取。
互斥就意味著,同一時間只有一個執行緒執行synchronized
修飾的程式碼,那麼:
- 無論怎麼重排序,都會遵循as-if-serial語義,因此
synchronized
中不存在有序性問題; - 不主動釋放鎖,其他執行緒無法執行
synchronized
中程式碼,無需考慮原子性問題。
因此synchronized
中互斥就代表了對有序性問題和原子性問題的保證。不過前提是JSR-133中反覆提到的correctly synchronized(正確的同步),舉個例子:
public class IncorrectlySynchronized {
private Integer count = 0;
public void add() {
synchronized (count) {
count++;
}
}
public static void main(String[] args) throws InterruptedException {
IncorrectlySynchronized incorrectlySynchronized = new IncorrectlySynchronized();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
incorrectlySynchronized.add();
}
});
Thread t2 = new Thread(()-> {
for (int i = 0; i < 10000; i++) {
incorrectlySynchronized.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(incorrectlySynchronized.count);
}
}
看似該加synchronized
的地方都加了,但是結果卻會出乎意料,這就典型的錯誤同步的例子。
synchronized鎖什麼?
既然是鎖,那麼synchronized
鎖的是什麼呢?
《The Java Language Specification》中描述(節選)到:
Each object in Java is associated with a monitor, which a thread can lock or unlock.
The synchronized statement computes a reference to an object; it then attempts to perform a lock action on that object's monitor and does not proceed further until the lock action has successfully completed.
Java中每個物件都與一個監視器關聯,執行緒可以鎖定或者解鎖該監視器。synchronized
語句嘗試鎖定與物件關聯的監視器,鎖定成功後才可以繼續執行。
通常,我們將synchronized
鎖定與物件關聯的監視器理解為synchronized
鎖定物件本身。
在我們知道synchronized
鎖什麼後,再去看用法,很多內容就會一目瞭然了。
synchronized怎麼用?
作為關鍵字,synchronized
有兩種用法:
- 修飾程式碼塊
- 修飾方法
- 修飾成員方法
- 修飾靜態方法
之前有個同事特別迷信“背技術”,為了區分不同用法的效果,背了某機構的“執行緒八鎖”,但每過一段時間就會忘記。
其實,知道了synchronized
鎖什麼,不同用法的效果自然就出來了,看一個例子:
public class SynchronizedDemo {
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
Thread t1 = new Thread(synchronizedDemo::lockMemberMethod1);
Thread t2 = new Thread(synchronizedDemo::lockMemberMethod2);
t1.start();
// 確保t1先執行
TimeUnit.SECONDS.sleep(1);
t2.start();
}
private synchronized void lockMemberMethod1() {
System.out.println("方法1");
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private synchronized void lockMemberMethod2() {
System.out.println("方法2");
}
}
透過例項變數呼叫成員方法時,會隱式的傳遞this
。這個例子中,t1和t2想鎖定的監視器是誰的?synchronizedDemo
物件的。t1先獲取到,那麼t2只能等待t1釋放後再獲取了。
那此時的鎖定範圍是什麼?synchronizedDemo
物件。
修改下程式碼:
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
SynchronizedDemo synchronizedDemo2 = new SynchronizedDemo();
Thread t1 = new Thread(synchronizedDemo::lockMemberMethod1);
Thread t2 = new Thread(synchronizedDemo2::lockMemberMethod2);
t1.start();
t2.start();
}
t2不再爭奪synchronizedDemo
而是爭奪synchronizedDemo2
,結果上也能看出t1和t2之間不存在競爭關係。
那麼使用synchronized
修飾靜態方法和程式碼塊是什麼效果呢?
private static synchronized void lockStaticMethod() {
System.out.println("靜態方法!");
}
private void lockCodeBlock(int count) {
synchronized (this) {
System.out.println("成員方法的程式碼塊!");
}
}
使用synchronized
修飾靜態方法,鎖定的物件是SynchronizedDemo.class
。所有SynchronizedDemo
的例項物件共用同一個SynchronizedDemo.class
,同一時間不同變數,只有一個執行緒可以執行lockStaticMethod
方法。
至於synchronized
修飾程式碼塊,就比較靈活了,括號中是誰就鎖定誰。如果是this
就鎖定例項變數,如果是SynchronizedDemo.class
效果就和修飾靜態方法一樣。
至於前面錯誤的同步的例子,它的問題是count
物件在不斷變化(Integer
實現相關)的,因此synchronized
鎖定的並不是同一個物件。
結語
今天的內容非常基礎,難度也不大。
重點可以放在synchronized
鎖什麼的部分,以及是如何推匯出synchronized
不同用法產生的不同效果的。這樣的方式更接近於問題的本質,也能更好的舉一反三,而不是死記硬背“執行緒八鎖”這種東西。
好了,今天就到這裡了,Bye~~