線程:synchronized原理及使用

1. synchronized作用

能夠保證原子性的Atomic變量和保證可見性、有序性的 volatile關鍵字是輕量級的實現併發同步的方式,但是都存在着一定的侷限,具體參考博文

synchronized是重量級的同步實現方式,synchronized 作用域中的代碼是同步執行的。併發的情況下,執行到對同一個對象加鎖的 synchronized 代碼塊時,多線程會轉爲串行執行的。這裏注意,不是同一個同步代碼塊,而是對同一個對象上鎖的同步代碼塊。這意味着範圍更廣。

此外 JMM關於synchronized的兩條規定:

  1. 線程解鎖前,必須把共享變量的最新值刷新到主內存中
  2. 線程加鎖時,將清空工作內存中共享變量的值,從而使用共享變量時需要從主內存中重新獲取最新的值

由此可見synchronized 可以確保可見性,在一個線程執行完 synchronized 代碼後,所有代碼中對變量值的變化都能立即被其它線程所看到。

2. synchronized使用

同步代碼塊

以下面的代碼爲例,假設存在一個隊列對象 tasks,裏面存放着需要執行的任務(即Runnable的實現類),

// 消費者run方法中的同步代碼塊
synchronized (tasks) {
    if (tasks.size() > 0) {
        task = tasks.removeFirst();
        sleep(100);
        tasks.notifyAll();
   } else {
        tasks.wait();
    }
}

// 生產者run方法中的同步代碼塊
synchronized (tasks) {
    if (tasks.size() < MAX) {
         Task task = new Task(new Random().nextInt(3) + 1, getPunishedWord());
        tasks.addLast(task);
        System.out.println(threadName + "留了作業,抄寫" + task.getWordToCopy() + " " + task.getLeftCopyCount() + "次");
        tasks.notifyAll();
    } else {
        System.out.println(threadName+"開始等待");
        tasks.wait();
        System.out.println("teacher線程 " + threadName + "線程-" + name + "等待結束");
    }
}

synchronized (tasks) ,這行代碼小括號裏的 tasks 對象和 synchronized 實現的方式相關。小括號裏的對象是可以是任意的對象。這個對象相當於是同步代碼塊的看門人,每個對其 synchronized 的線程,它都會記錄下來,然後等到同步代碼塊沒有線程執行的時候,它就會通知其它線程來執行同步代碼塊。所以並不是括號內的對象加鎖,只是讓它來維護秩序

例子中,併發的線程並不是同樣類型的 Thread(因爲存在生產者和消費者兩種線程),一個是 Student,還一個是 Teacher。對於不同線程對象的同步控制,一定要選用兩種線程都持有的對象纔行。否則各自使用不同的對象,相當於聘用了兩個看門人,各看各的門,毫無瓜葛。那麼原本想要串行執行的代碼仍舊會並行執行。

同步方法

對於非靜態方法,

public synchronized void eat(){
	.......
  .......
}

同步方法的鎖對象是this,等效於下面的代碼塊,

public void eat(){
	synchronized(this){
		.......
  	.......
	}
}

對於靜態方法,

public static synchronized void eat(){
	.......
  .......
}

此時同步方法爲類的 Class 對象。如果上述靜態方法所在的類爲 Test。那麼鎖對象就是 Test.class。

synchronized使用總結

  1. 選用一個鎖對象,可以是任意對象
  2. 鎖對象鎖住的是同步代碼塊,而不是自己
  3. 不同類型的線程如果執行相同的同步代碼,鎖對象要使用所有線程共同持有的一個對象
  4. sychronized代碼塊可以同時滿足併發的三個特性

3. synchronized原理

synchronized 的祕密其實都在同步對象上,這個對象就是一個看門人,每次只允許一個線程進來,進門後此線程可以做任何自己想做的事情,然後再出來。線程出來後,看門人會提示其它線程,總有個敏捷值最高的線程先衝入門內,那麼其它線程只好繼續等待。

每個對象Java都關聯了一個 monitor lock。當一個線程獲取了 monitor lock 後,其它線程如果運行到獲取同一個 monitor 的時候就會被 block 住。當這個線程執行完同步代碼,則會釋放 monitor lock。在後一個線程獲取鎖後,happens-before 原則生效,前一個線程所做的任何修改都會被這個線程看到。

再深入底層一點,每個 Java 對象在 JVM 的對等對象的頭中保存鎖狀態,指向 ObjectMonitor。該模式是JVM內部基於C++實現的,模式圖如下,
image

  1. ObjectMonitor 保存了當前持有鎖的線程引用
  2. EntryList 中保存目前等待獲取鎖的線程,WaitSet 保存 wait 的線程
  3. 此外還有一個計數器,每當線程獲得 monitor 鎖,計數器 +1,當線程重入此鎖時,計數器還會 +1。當計數器不爲0時,其它嘗試獲取 monitor 鎖的線程將會被保存到EntryList中,並被阻塞
  4. 當持有鎖的線程釋放了monitor 鎖後,計數器 -1。當計數器歸位爲 0 時,所有 EntryList 中的線程會嘗試去獲取鎖,但只會有一個線程會成功,沒有成功的線程仍舊保存在 EntryList 中。

當一個線程需要獲取 Object 的鎖時,會被放入 EntrySet 中進行等待,如果該線程獲取到了鎖,成爲當前鎖的 owner。如果根據程序邏輯,一個已經獲得了鎖的線程缺少某些外部條件,而無法繼續進行下去(例如生產者發現隊列已滿或者消費者發現隊列爲空),那麼該線程可以通過調用 wait 方法將鎖釋放,進入 wait set 中阻塞進行等待,其它線程在這個時候有機會獲得鎖,去幹其它的事情,從而使得之前不成立的外部條件成立,這樣先前被阻塞的線程就可以重新進入 EntrySet 去競爭鎖。這個外部條件在 monitor 機制中稱爲條件變量。引自博文

4. synchronized注意事項

  1. synchronized 使用的爲非公平鎖,如果需要公平鎖,可以使用 ReentrantLock,設置爲公平鎖
  2. 鎖對象不能爲 null。如果鎖對象爲 null,就不存在與其關聯的 monitor 鎖
  3. 只把需要同步的代碼放入 synchronized 代碼塊。如果不思考,爲了線程安全把方法中全部代碼都放入同步代碼塊,那麼將會喪失多線程的優勢。再多的線程也只能串行執行,這完全違背了併發的初衷
  4. 只有使用同一個對象作爲鎖對象,才能同步。是同一個對象,而不是同一個類。有一種常犯的錯誤是,不同線程持有的是同一個類的不同實例。那麼該對象實例用作鎖對象的話,多個線程並不會同步。還一種錯誤是使用不同類的實例作爲鎖對象,但是期望不同位置的同步代碼塊能夠同步執行
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章