【Java併發】synchronized原理分析

synchronized使用

  • synchronized能夠保證再同一時刻最多隻有一個線程執行該段代碼,以達到保證併發安全的效果。
synchronized (鎖對象) {
	// 受保護資源
}

synchronized與原子性

  • 下面代碼中,number++是個非原子性操作,使用synchronized修飾後,即可保證其原子性。
public class AtomicitySync {

	private static int number = 0;
	// 鎖對象
	private static Object obj = new Object();
	
	public static void main(String[] args) throws InterruptedException {
		
		Runnable increment = () -> {
			for (int i = 0; i < 1000; i++) {
				synchronized (obj) {
					number++;
				}
			}
		};
		
		List<Thread> list = new ArrayList<Thread>();
		
		for (int i= 0; i < 5; i++) {
			Thread t = new Thread(increment);
			t.start();
			list.add(t);
		}
		
		//爲了保證5個線程執行完畢,主線程再打印number
		for (Thread t : list) {
			t.join();
		}
		
		System.out.println("number:" + number);
	}
}
  • 利用javap查看JVM字節碼指令
10: monitorenter
11: getstatic     #12                 // Field number:I
14: iconst_1
15: iadd
16: putstatic     #12                 // Field number:I
19: aload_1
20: monitorexit

synchronized與可見性

  • synchronized會對應 Java內存模型主內存與工作內存交互中的 lock 和 unlock 原子操作,lock操作會讓工作內存中的變量強制刷新爲主內存中的變量值,得到共享變量在主內存的最新值,這樣呢,其他線程修改的共享變量之後,該線程就能立即獲取到最新的值,從而保證可見性。
  • System.out.println()方法內部就加了synchronized,保證打印的變量值是最新更改的值。

synchronized與有序性

  • 爲什麼要重排序:爲了提高程序的執行效率,編譯器和CPU會對程序中的代碼進行重排序
  • as-if-serial語義:不管編譯器和CPU如何重排序,必須保證在單線程情況下,程序運行的結果是正確的。
  • 加synchnorized後依然會進行重排序,但是synchronized可以保證同步代碼塊只會有一個線程執行,單線程執行時運行結果不會出錯。

synchronized的特性

1、可重入特性

  • 一個線程可以多次執行synchronized,重複獲取同一把鎖。
  • 也就是說,一個線程得到鎖對象之後,再次請求該對象鎖,是允許的。
  • 只要線程相同,鎖對象相同,那麼就可重入。
public class Reentrant {

	public static void main(String[] args) {
		new MyThread().start();
		new MyThread().start();
	}
}

class MyThread extends Thread {
	@Override
	public void run() {
		synchronized (MyThread.class) {
			System.out.println(getName() + "進入了代碼塊1");
			
			synchronized (MyThread.class) {
				System.out.println(getName() + "進入了代碼塊2");
			}
		}
	}
}
  • synchronized是可重入鎖,內部鎖對象中有一個計數器(recursions變量)記錄線程獲得幾次鎖,在執行完同步代碼塊時,計數器會減1,直到計數器爲0,就釋放鎖。
  • 可重入特性好處
    • 可以避免死鎖
    • 可以讓我們更好的來封裝代碼

2、不可中斷特性

  • 一個線程獲得鎖後,另一個線程想要獲得鎖,必須處於阻塞或等待狀態,如果第一個線程不釋放鎖對象,第二個線程就一直阻塞或等待,不可被中斷。
public class Uninterruptible {

	private static Object obj = new Object();
	public static void main(String[] args) throws InterruptedException {
		Runnable run = new Runnable() {
			
			@Override
			public void run() {
				synchronized (obj) {
					String name = Thread.currentThread().getName();
					System.out.println(name + "進入代碼塊");
					try {
						Thread.sleep(100000);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			}
		};
		
		Thread t1 = new Thread(run);
		t1.start();
		Thread.sleep(200);
		Thread t2 = new Thread(run);
		t2.start();
		
		System.out.println("中斷前");
		t2.interrupt();
		System.out.println("中斷後");
		
		System.out.println(t1.getName() + ":" + t1.getState());
		System.out.println(t2.getName() + ":" + t2.getState());
	}
}
  • 輸出結果如下:
Thread-0進入代碼塊
中斷前
中斷後
Thread-0:TIMED_WAITING
Thread-1:BLOCKED
  • 可以看到,當調用t2.interrupt();中斷操作後,t2線程狀態是BLOCKED仍然處於阻塞狀態
  • synchronized是屬於不可中斷鎖
  • 補充:Lock類,調用lock()方法是不可中斷的,調用tryLock()是可中斷的。

synchronized原理

同步代碼塊

  • 首先看看上面原子性演示代碼中,Runnable中的lambda表達式,synchronized同步代碼塊,反彙編後JVM字節碼指令
private static void lambda$0();
descriptor: ()V
flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
Code:
  stack=2, locals=2, args_size=0
     0: iconst_0
     1: istore_0
     2: goto          30
     5: getstatic     #17                 // Field obj:Ljava/lang/Object;
     8: dup
     9: astore_1
    10: monitorenter
    11: getstatic     #12                 // Field number:I
    14: iconst_1
    15: iadd
    16: putstatic     #12                 // Field number:I
    19: aload_1
    20: monitorexit
    21: goto          27
    24: aload_1
    25: monitorexit
    26: athrow
    27: iinc          0, 1
    30: iload_0
    31: sipush        1000
    34: if_icmplt     5
    37: return
  Exception table:
     from    to  target type
        11    21    24   any
        24    26    24   any
  • 從第10行指令開始,是加鎖操作
  • monitorenter指令的作用,下面是JVM規範中的描述

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor’s entry count is zero, then tries again to gain ownership.

這段話的大概意思爲:

每個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:

1、如果monitor的進入數爲0,則該線程進入monitor,然後將進入數設置爲1,該線程即爲monitor的所有者。

2、如果線程已經佔有該monitor,只是重新進入,則進入monitor的進入數加1.

3.如果其他線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再重新嘗試獲取monitor的所有權。

  • 總結一下就是說:
    synchronized的鎖對象會關聯一個monitor,這個monitor不是我們主動創建的,是JVM的線程執行到這個同步代碼塊,發現鎖對象沒有monitor,就會創建個monitor對象(是個C++對象),monitor內部有兩個重要的成員變量,owner:擁有這把鎖的線程,recursions:會記錄線程擁有鎖的次數,當一個線程擁有monitor後,其他線程只能等待

  • monitorexit

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

這段話的大概意思爲:

1、能執行monitorexit指令的線程一定擁有當前對象的monitor的所有權的線程
2、執行monitorexit時會將monitor的進入數減1,當進入數減爲0時,當前線程退出monitor,不再擁有monitor的所有權,此時其他被這個monitor阻塞的線程可以嘗試去獲取這個monitor的所有權。

指令執行時,monitor的進入數減1,如果減1後進入數爲0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權。

通過這兩段描述,我們應該能很清楚的看出Synchronized的實現原理,Synchronized的語義底層是通過一個monitor的對象來完成,其實wait/notify等方法也依賴於monitor對象,這就是爲什麼只有在同步的塊或者方法中才能調用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因。

思考synchronized中出現異常會釋放鎖嗎?

  • 看上面JVM字節碼指令最下面有個Exception table,從11行指令開始,到21行指令接入,如果出現任意異常,就跳轉到到24行開始執行,第25行執行monitorexit。所以說,出現異常後會釋放鎖的。

  • monitorexit釋放鎖,插入在方法結束處和異常處,JVM保證每個monitorenter都對應一個monitorexit

同步方法

public synchronized void test() {
	number++;
}

該同步方法,反彙編後JVM字節碼指令

public synchronized void test();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=2, locals=1, args_size=1
       0: getstatic     #10                 // Field number:I
       3: iconst_1
       4: iadd
       5: putstatic     #10                 // Field number:I
       8: return
    LineNumberTable:
      line 14: 0
      line 15: 8
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       9     0  this   Lcom/api/flower/test/Sync;
  • 從反編譯的結果來看,方法的同步並沒有通過指令monitorenter和monitorexit來完成(理論上其實也可以通過這兩條指令來實現),不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。
  • JVM就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,如果設置了,執行線程將先獲取monitor,獲取成功之後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其他任何線程都無法再獲得同一個monitor對象。 其實本質上沒有區別,只是同步方法是一種隱式的方式來實現,無需通過字節碼來完成。

synchronized與Lock的區別

  1. synchronized是關鍵字,而Lock是個接口
  2. synchronized會自動釋放鎖,而Lock必須手動釋放鎖(加鎖成功後,必須調用一下unlock()方法來釋放鎖)
  3. synchronized是不可中斷的,Lock可以中斷也可以不中斷(Lock調用lock()方法時不可中斷,調用tryLock()時可中斷)
  4. 通過Lock可以知道線程有沒有拿到鎖,而synchronized不能(tryLock()方法返回值boolean,標識該線程是否獲取到鎖)
  5. synchronized能鎖住方法和代碼塊,而Lock只能鎖住代碼塊
  6. Lock可以使用讀鎖提高多線程讀效率(Lock的一個實現類ReentrantReadWriteLock,如果讀的時候,允許多個線程讀,如果寫的時候,只允許一個線程寫)
  7. synchronized是非公平鎖,ReentrantLock可以控制是否是公平鎖(公平鎖是指喚醒阻塞等待的線程時按先到先得原則,synchronized喚醒的時候並不是按先來後到的原則喚醒,ReentrantLock有個構造方法,通過傳參決定創建公平鎖還是非公平鎖)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章