07.synchronized都問啥?

2023-01-05 21:01:32

大家好,我是王有志。關注王有志,一起聊技術,聊遊戲,從北漂生活談到國際風雲。最近搞了個抽獎送書的活動,歡迎點選連結參與。

如果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~~