Java中synchronized實現對象鎖的兩種方式及原理解析

簡介

本文將介紹Java中兩種對象鎖及其實現方式。分別是同步代碼塊鎖和方法鎖。我們將解決上一篇文章《Java中線程安全和線程不安全解析和示例》中提到的線程不安全問題,看看通過synchronized的對象鎖,怎樣輕鬆解決該問題。

一.同步代碼塊鎖

案例:通過同步代碼塊鎖,實現兩個線程對同一個全局變量count,各自執行1萬次count++,驗證結果是否等於2萬,而不會出現小於2萬的情況。

完整代碼實現:

public class SynchronizeCodeBlockLock implements Runnable {
	private static SynchronizeCodeBlockLock instance = new SynchronizeCodeBlockLock();
	private static int count = 0;

	@Override
	public void run() {
		method();
	}

	private void method() {
		// 關鍵:同步代碼塊的方式,操作同一變量,達到線程安全的效果
		synchronized (this) {
			System.out.println("線程名:" + Thread.currentThread().getName() + ",運行開始");
			for (int i = 0; i < 10000; i++) {
				count++;
			}
			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()) {
			// 若有線程如果還在活動,則不執行下一步(等同於thread.join()方法)
		}
		System.out.println("期待結果:20000,實際結果:" + count);
	}


}

運行結果:

線程名:Thread-0,運行開始
線程:Thread-0,運行結束
線程名:Thread-1,運行開始
線程:Thread-1,運行結束
期待結果:20000,實際結果:20000

分析運行結果:

我們發現使用了synchronized關鍵字後,線程Thread-0先執行,等到其結束後,Thread-1纔開始執行。如果不使用synchronized關鍵字,執行結果可能會是Thread-0和Thread-1幾乎同時執行,幾乎同時運行結束。這就說明使用了synchronized關鍵字後,將多個線程的並行,變爲了串行。

二.方法鎖

本例僅展示方法鎖,在控制線程串行執行的示例。方法鎖保證線程安全的效果,跟同步代碼塊是一致的。

public class MethodLock implements Runnable {

	private static MethodLock instance = new MethodLock();

	@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("測試結束");
	}
}

運行結果:

線程:Thread-0,運行開始
線程:Thread-0,運行結束
線程:Thread-1,運行開始
線程:Thread-1,運行結束
測試結束

發現運行結果中,多個線程也是串行執行的,效果跟同步代碼塊鎖是一致的。

三.synchronized關鍵字,是怎麼保證線程安全的呢?

在Java中所有的對象,都會有一把鎖,叫做內置鎖,也稱作監視器鎖。這是種排他鎖。

排他鎖:一個線程獲取後,其他線程只能等待其釋放後,纔有機會獲得該鎖。

Java中每個對象,都可以把內置鎖,當做一個同步鎖來使用。當一個線程在進入到synchronized代碼塊前時,會自動獲取到監視器鎖,此時其他線程在訪問synchronized代碼塊時,就會被堵塞掛起(被拒之門外的意思)。拿到鎖的線程會在執行完成、或者拋出異常、或者調用wait系列方法時釋放該鎖。其他線程只能等待鎖被釋放後才能獲取該鎖。

通俗的講,synchronized關鍵字將代碼塊中代碼由並行轉變成了串行,這樣就保證了代碼被順序執行。

四.synchronized在內存層面,是如何實現加鎖和釋放鎖的?

  • 進入synchronized代碼塊時,會將代碼塊內用到的變量從該線程的工作內存中清除,轉而從主內存中獲取。
  • 退出synchronized代碼塊時,會將代碼塊內用到的變量的修改,刷新到主內存中。

這其實就是synchronized解決共享變量內存可見性的原理。關於synchronized的性質(可見性、可重入性),我會在後續其他文章中詳細解釋。

五.synchronized將線程的並行處理轉爲串行處理,有什麼缺點?

synchronized將並行改爲串行,當然會影響程序的執行效率,執行速度會受到影響。其次,synchronized操作線程的堵塞,也就是由操作系統控制CPU的內核進行上下文的切換,這個切換本身也是耗時的。所以使用synchronized關鍵字會降低程序的運行效率。

六. 使用Synchronized關鍵字需要注意什麼?

1.Synchronized使用時需要注意的地方鎖對象不能爲空。

鎖對象的信息是保留在對象頭中的,如果對象爲空,則鎖的信息也就不存在了。

2.作用域不宜過大

synchronized代碼塊的代碼量不宜過多,如果把過多的代碼放在其中,程序的運行會變爲串行,速度會下降。各個線程並行可以提高效率,我們應該僅把那些影響線程安全的代碼,放入synchronized代碼塊中,串行執行;不需要考慮線程安全的代碼,並行執行,達到效率最高。

3.避免死鎖

避免讓線程對鎖持有並等待的情況出現(後續文章將講解死鎖的相關知識)。

七.總結

本文講解了Java中的對象鎖的兩種實現方式,分別是以下兩種實現形式:

方式一:同步代碼塊鎖:

synchronized (共享變量) {
    //需要同步的代碼
}

方式二:方法鎖:

	private synchronized void method() {
	    //需要同步的代碼
	}

無論你使用哪一種形式,都應該在保證同步的情況下,儘量減少同步代碼的內容,這樣可以提高程序的運行效率,還能保證線程的安全。喜歡本文請點贊和收藏,也歡迎繼續閱讀本專欄多線程的其他文章。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章