1. 程式人生 > >併發程式設計原理剖析——併發程式設計的實現原理

併發程式設計原理剖析——併發程式設計的實現原理

在多執行緒併發程式設計中synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖。但是,隨著Java SE 1.6對 synchronized進行了各種優化之後,有些情況下它就並不那麼重了,Java SE 1.6中為了減少獲得鎖和釋放鎖帶來的 效能消耗而引入的偏向鎖和輕量級鎖,以及鎖的儲存結構和升級過程。我們仍然沿用前面使用的案例,然後通過 synchronized關鍵字來修飾在inc的方法上。再看看執行結果。

public class Demo {
    private static int count=0;
    public static void inc(){
        synchronized (Demo.class){
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            count++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i=0;i<1000;i++){
            new Thread(()->{
                Demo.inc();
            }).start();
        }
        Thread.sleep(3000);
        System.out.println("執行結果:"+count);
    }
}

執行結果:1000

synchronized的三種應用方式

synchronized有三種方式來加鎖,分別是

1、修飾例項方法,作用於當前例項加鎖,進入同步程式碼塊前要獲得當前例項的鎖

2、靜態方法,作用於當前類物件加鎖,進入同步程式碼前要獲得當前物件的鎖

3、修飾程式碼塊,指定加鎖物件,對給定物件加鎖,進入同步程式碼塊前要獲得給定物件的鎖。

synchronzied括號後面的物件

synchronized擴後後面的物件是一把鎖,在java中任意一個物件都可以成為鎖,簡單來說,我們把object比喻是一 個key,擁有這個key的執行緒才能執行這個方法,拿到這個key以後在執行方法過程中,這個key是隨身攜帶的,並且 只有一把。如果後續的執行緒想訪問當前方法,因為沒有key所以不能訪問只能在門口等著,等之前的執行緒把key放回 去。所以,synchronized鎖定的物件必須是同一個,如果是不同物件,就意味著是不同的房間的鑰匙,對於訪問者 來說是沒有任何影響的 。

synchronized的位元組碼指令

通過javap -v 來檢視對應程式碼的位元組碼指令,對於同步塊的實現使用了monitorenter和monitorexit指令,前面我 們在講JMM的時候,提到過這兩個指令,他們隱式的執行了Lock和UnLock操作,用於提供原子性保證。 monitorenter指令插入到同步程式碼塊開始的位置、monitorexit指令插入到同步程式碼塊結束位置,jvm需要保證每 個monitorenter都有一個monitorexit對應。 這兩個指令,本質上都是對一個物件的監視器(monitor)進行獲取,這個過程是排他的,也就是說同一時刻只能有 一個執行緒獲取到由synchronized所保護物件的監視器
執行緒執行到monitorenter指令時,會嘗試獲取物件所對應的monitor所有權,也就是嘗試獲取物件的鎖;而執行 monitorexit,就是釋放monitor的所有權 

synchronized的鎖的原理

jdk1.6以後對synchronized鎖進行了優化,包含偏向鎖、輕量級鎖、重量級鎖; 在瞭解synchronized鎖之前,我們 需要了解兩個重要的概念,一個是物件頭、另一個是monitor 

Java物件頭

在Hotspot虛擬機器中,物件在記憶體中的佈局分為三塊區域:物件頭、例項資料和對齊填充;Java物件頭是實現 synchronized的鎖物件的基礎,一般而言,synchronized使用的鎖物件是儲存在Java物件頭裡。它是輕量級鎖和偏 向鎖的關鍵 

Mark Word

Mark Word用於儲存物件自身的執行時資料,如雜湊碼(HashCode)、GC分代年齡、鎖狀態標誌、執行緒持有的 鎖、偏向執行緒 ID、偏向時間戳等等。Java物件頭一般佔有兩個機器碼(在32位虛擬機器中,1個機器碼等於4位元組, 也就是32bit)

在原始碼中的體現

如果想更深入瞭解物件頭在JVM原始碼中的定義,需要關心幾個檔案,oop.hpp/markOop.hpp oop.hpp,每個 Java Object 在 JVM 內部都有一個 native 的 C++ 物件 oop/oopDesc 與之對應。先在oop.hpp中看 oopDesc的定義

_mark 被宣告在 oopDesc 類的頂部,所以這個 _mark 可以認為是一個 頭部, 前面我們講過頭部儲存了一些重要的 狀態和標識資訊,在markOop.hpp檔案中有一些註釋說明markOop的記憶體佈局

Monitor

什麼是Monitor?我們可以把它理解為一個同步工具,也可以描述為一種同步機制。所有的Java物件是天生的 Monitor,每個object的物件裡 markOop->monitor() 裡可以儲存ObjectMonitor的物件。從原始碼層面分析一下 monitor物件
Ø oop.hpp下的oopDesc類是JVM物件的頂級基類,所以每個object物件都包含markOop img Ø markOop.hpp**中** markOopDesc繼承自oopDesc,並擴充套件了自己的monitor方法,這個方法返回一個 ObjectMonitor指標物件
img Ø objectMonitor.hpp,在hotspot虛擬機器中,採用ObjectMonitor類來實現monitor,

synchronized的鎖升級和獲取過程

瞭解了物件頭以及monitor以後,接下來去分析synchronized的鎖的實現,就會非常簡單了。前面講過synchronized的鎖是經過優化的,引入了偏向鎖、輕量級鎖;鎖的等級從低到高逐步升級,無鎖->偏向鎖->輕量級鎖->重量級鎖

自旋鎖(CAS)

自旋鎖就是讓不滿足條件的執行緒等待一段時間,而不是立即掛起。看持有鎖的執行緒是否能夠很快的釋放鎖。怎麼自旋呢?

其實就是一段沒有任何意義的迴圈。

雖然它通過佔用處理器的時間來避免執行緒切換帶來的開銷,但是如果持有鎖的執行緒不能很快釋放鎖,那麼自旋的執行緒就會浪費處理器的資源,因為它不會做任何有意義的工作。所以,自旋等待的時間或者次數是一個有限度的,如果自旋超過了定義的時間任然沒有獲取到鎖,則執行緒應該被掛起。

偏向鎖

大多數情況下,鎖不僅不存在多執行緒競爭,而且總是由同一執行緒多次獲得,為了讓執行緒獲得鎖的代價更低而引入了 偏向鎖。當一個執行緒訪問同步塊並獲取鎖時,會在物件頭和棧幀中的鎖記錄裡儲存鎖偏向的執行緒ID,以後該執行緒在 進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下物件頭的Mark Word裡是否儲存著指 向當前執行緒的偏向鎖。如果測試成功,表示執行緒已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏 向鎖的標識是否設定成1(表示當前是偏向鎖):如果沒有設定,則使用CAS競爭鎖;如果設定了,則嘗試使用CAS 將物件頭的偏向鎖指向當前執行緒
 

輕量級鎖 

引入輕量級鎖的主要目的是在沒有多執行緒競爭的前提下,減少傳統的重量級鎖使用作業系統互斥量產生的效能消 耗。當關閉偏向鎖功能或者多個執行緒競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖 

重量級鎖 

重量級鎖通過物件內部的監視器(monitor)實現,其中monitor的本質是依賴於底層作業系統的Mutex Lock實 現,作業系統實現執行緒之間的切換需要從使用者態到核心態的切換,切換成本非常高。
前面我們在講Java物件頭的時候,講到了monitor這個物件,在hotspot虛擬機器中,通過ObjectMonitor類來實現 monitor。他的鎖的獲取過程的體現會簡單很多

wait和notify 
wait和notify是用來讓執行緒進入等待狀態以及使得執行緒喚醒的兩個操作

public class ThreadWait extends Thread {
    private Object lock;

    public  ThreadWait(Object lock){
        this.lock=lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println("開始執行 thread wait....");
            try {
                lock.wait();
            }catch (InterruptedException e){
                e.printStackTrace();
            }
            System.out.println("執行結束 thread wait....");
        }
    }
}
public class ThreadWait extends Thread {
    private Object lock;

    public  ThreadWait(Object lock){
        this.lock=lock;
    }

    @Override
    public void run() {
        synchronized (lock){
            System.out.println("開始執行 thread wait....");
            lock.notify();
            System.out.println("執行結束 thread wait....");
        }
    }
}

wait和notify的原理 

呼叫wait方法,首先會獲取監視器鎖,獲得成功以後,會讓當前執行緒進入等待狀態進入等待佇列並且釋放鎖;然後 當其他執行緒呼叫notify或者notifyall以後,會選擇從等待佇列中喚醒任意一個執行緒,而執行完notify方法以後,並不 會立馬喚醒執行緒,原因是當前的執行緒仍然持有這把鎖,處於等待狀態的執行緒無法獲得鎖。必須要等到當前的執行緒執 行完按monitorexit指令以後,也就是鎖被釋放以後,處於等待佇列中的執行緒就可以開始競爭鎖了

wait和notify為什麼需要在synchronized裡面 

wait方法的語義有兩個,一個是釋放當前的物件鎖、另一個是使得當前執行緒進入阻塞佇列, 而這些操作都和監視器是 相關的,所以wait必須要獲得一個監視器鎖
而對於notify來說也是一樣,它是喚醒一個執行緒,既然要去喚醒,首先得知道它在哪裡?所以就必須要找到這個對 象獲取到這個物件的鎖,然後到這個物件的等待佇列中去喚醒一個執行緒。

連結:https://blog.csdn.net/scdn_cp/article/details/864917