1. 程式人生 > >詳解Java多執行緒鎖之synchronized

詳解Java多執行緒鎖之synchronized

synchronized是Java中解決併發問題的一種最常用的方法,也是最簡單的一種方法。

synchronized的四種使用方式

  1. 修飾程式碼塊:被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的程式碼,作用於呼叫物件
  2. 修飾方法:被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用於呼叫物件

    注意:synchronized修飾方法時必須是顯式呼叫,如果沒有顯式呼叫,例如子類重寫該方法時沒有顯式加上synchronized,則不會有加鎖效果。

  3. 修飾靜態方法:其作用的範圍是整個靜態方法,作用於所有物件
  4. 修飾類:其作用的範圍是synchronized後面括號括起來的部分(例如:test.class),作用於所有物件

物件鎖和類鎖是否會互相影響麼?

  • 物件鎖:Java的所有物件都含有1個互斥鎖,這個鎖由JVM自動獲取和釋放。執行緒進入synchronized方法的時候獲取該物件的鎖,當然如果已經有執行緒獲取了這個物件的鎖,那麼當前執行緒會等待;synchronized方法正常返回或者拋異常而終止,JVM會自動釋放物件鎖。這裡也體現了用synchronized來加鎖的1個好處,方法拋異常的時候,鎖仍然可以由JVM來自動釋放。

  • 類鎖:物件鎖是用來控制例項方法之間的同步,類鎖是用來控制靜態方法(或靜態變數互斥體)之間的同步。其實類鎖只是一個概念上的東西,並不是真實存在的,它只是用來幫助我們理解鎖定例項方法和靜態方法的區別的。java類可能會有很多個物件,但是隻有1個Class物件,也就是說類的不同例項之間共享該類的Class物件。Class物件其實也僅僅是1個java物件,只不過有點特殊而已。由於每個java物件都有1個互斥鎖,而類的靜態方法是需要Class物件。所以所謂的類鎖,不過是Class物件的鎖而已。

類鎖和物件鎖不是同1個東西,一個是類的Class物件的鎖,一個是類的例項的鎖。也就是說:1個執行緒訪問靜態synchronized的時候,允許另一個執行緒訪問物件的例項synchronized方法。反過來也是成立的,因為他們需要的鎖是不同的。

對應的實驗程式碼如下:

@Slf4j
public class SynchronizedExample {

    // 修飾一個程式碼塊
    public void test1(int j) {
        synchronized (this) {
            for (int i = 0; i < 10; i++) {
                log.info("test1 {} - {}", j, i);
            }
        }
    }

    // 修飾一個方法
    public synchronized void test2(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test2 {} - {}", j, i);
        }
    }

    // 修飾一個類
    public static void test3(int j) {
        synchronized (SynchronizedExample.class) {
            for (int i = 0; i < 10; i++) {
                log.info("test3 {} - {}", j, i);
            }
        }
    }

    // 修飾一個靜態方法
    public static synchronized void test4(int j) {
        for (int i = 0; i < 10; i++) {
            log.info("test4 {} - {}", j, i);
        }
    }

    public static void main(String[] args) {
        SynchronizedExample example1 = new SynchronizedExample();
        SynchronizedExample example2 = new SynchronizedExample();
        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.execute(() -> {
            example1.test2(1);
        });
        executorService.execute(() -> {
            example2.test2(2);
        });
    }
}

在JDK1.6之前,synchronized一直被稱呼為重量級鎖(重量級鎖就是採用互斥量來控制對資源的訪問)。通過反編譯成位元組碼指令可以看到,synchronized會在同步塊的前後分別形成monitorenter和monitorexit這兩個位元組碼指令。根據虛擬機器規範的要求,在執行monitorenter指令時,首先要嘗試獲取物件的鎖。如果這個物件沒被鎖定,或者當前執行緒已經擁有了那個物件的鎖,把鎖的計數器加1,相應的,在執行monitorexit指令時會將鎖計算器減1,當計數器為0時,鎖就被釋放,然後notify通知所有等待的執行緒。
Java的執行緒是對映到作業系統的原生執行緒上的,如果要阻塞或喚醒一個執行緒,都需要作業系統來幫忙完成,這就需要使用者態和核心態切換,大量的狀態轉換需要耗費很多處理器的時間。

synchronized的優化

在JDK1.6中對鎖的實現引入了大量的優化:

  1. 鎖粗化(Lock Coarsening):將多個連續的鎖擴充套件成一個範圍更大的鎖,用以減少頻繁互斥同步導致的效能損耗。
  2. 鎖消除(Lock Elimination):JVM即時編譯器在執行時,通過逃逸分析,如果判斷一段程式碼中,堆上的所有資料不會逃逸出去從來被其他執行緒訪問到,就可以去除這個鎖。
  3. 偏向鎖(Biased Locking):目的是消除資料無競爭情況下的同步原語。使用CAS記錄獲取它的執行緒。下一次同一個執行緒進入則偏向該執行緒,無需任何同步操作。
  4. 適應性自旋(Adaptive Spinning):為了避免執行緒頻繁掛起、恢復的狀態切換消耗。執行緒會進入自旋狀態。JDK1.6引入了自適應自旋。自旋時間根據之前鎖自旋時間和執行緒狀態,動態變化,可以能減少自旋的時間。
  5. 輕量級鎖(Lightweight Locking):在沒有多執行緒競爭的情況下避免重量級互斥鎖,只需要依靠一條CAS原子指令就可以完成鎖的獲取及釋放。

在JDK1.6之後,synchronized不再是重量級鎖,鎖的狀態變成以下四種狀態:
無鎖->偏向鎖->輕量級鎖->重量級鎖

鎖的狀態

自適應自旋鎖

大部分時候,共享資料的鎖定狀態只會持續很短的一段時間,為了這段時間去掛起和恢復執行緒並不值得。如果能讓兩個或以上的執行緒同時並行執行,我們就可以讓後面請求鎖的那個執行緒“稍等一下”,但不放棄處理器的執行時間,看看持有鎖的執行緒是否很快就會釋放鎖。這項技術就是所謂的自旋鎖。
自旋等待的時間必須要有一定的限度,如果自旋超過了限定的次數仍然沒有獲取到鎖,則該執行緒應該被掛起。在JDK1.6中引入了自適應的自旋鎖,自適應意味著自旋的時間不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。

所謂自旋,不是獲取不到就阻塞,而是在原地等待一會兒,再次嘗試(當然次數或者時長有限),他是以犧牲CPU為代價來換取核心狀態切換帶來的開銷。藉助於適應性自旋,可以在CPU時間片的損耗和核心狀態的切換開銷之間相對的找到一個平衡,進而能夠提高效能

偏向鎖

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

輕量級鎖

如果說偏向鎖是隻允許一個執行緒獲得鎖,那麼輕量級鎖就是允許多個執行緒獲得鎖,但是隻允許他們順序拿鎖,不允許出現競爭,也就是拿鎖失敗的情況,輕量級鎖的步驟如下:

  1. 執行緒1在執行同步程式碼塊之前,JVM會先在當前執行緒的棧幀中建立一個空間用來儲存鎖記錄,然後再把物件頭中的MarkWord複製到該鎖記錄中,官方稱之為DisplacedMarkWord。然後執行緒嘗試使用CAS將物件頭中的MarkWord替換為指向鎖記錄的指標。如果成功,則獲得鎖,進入步驟3)。如果失敗執行步驟2)
  2. 執行緒自旋,自旋成功則獲得鎖,進入步驟3)。自旋失敗,則膨脹成為重量級鎖,並把鎖標誌位變為10,執行緒阻塞進入步驟3)
  3. 鎖的持有執行緒執行同步程式碼,執行完CAS替換MarkWord成功釋放鎖,如果CAS成功則流程結束,CAS失敗執行步驟4)
  4. CAS執行失敗說明期間有執行緒嘗試獲得鎖並自旋失敗,輕量級鎖升級為了重量級鎖,此時釋放鎖之後,還要喚醒等待的執行緒

重量級鎖

自旋的執行緒在自旋過程中,成功獲得資源(即之前獲的資源的執行緒執行完成並釋放了共享資源),則整個狀態依然處於輕量級鎖的狀態,如果自旋失敗則進入重量級鎖的狀態,這個時候,自旋的執行緒進行阻塞,等待之前執行緒執行完成並喚醒自己,需要從使用者態切換到核心態實現。(當競爭競爭激烈時,執行緒直接進入阻塞狀態。不過在高版本的JVM中不會立刻進入阻塞狀態而是會自旋一小會兒看是否能獲取鎖如果不能則進入阻塞狀態。)

總結

可以簡單總結是如下場景:

  1. 只有一個執行緒進入加鎖區,鎖狀態是偏向鎖
  2. 多個執行緒交替進入加鎖區,鎖狀態可能是輕量級鎖
  3. 多執行緒同時進入加鎖區,鎖狀態可能是重量級鎖

最後,限於筆者經驗水平有限,歡迎讀者就文中的觀點提出寶貴的建議和意見。如果想獲得更多的學習資源或者想和更多的技術愛好者一起交流,可以關注我的公眾號『全菜工程師小輝』後臺回覆關鍵詞領取學習資料、進入後端技術交流群和程式設計師副業群。同時也可以加入程式設計師副業群Q群:735764906 一起交流。

相關推薦

Java執行synchronized

synchronized是Java中解決併發問題的一種最常用的方法,也是最簡單的一種方法。 synchronized的四種使用方式 修飾程式碼塊:被修飾的程式碼塊稱為同步語句塊,其作用的範圍是大括號{}括起來的程式碼,作用於呼叫物件 修飾方法:被修飾的方法稱為同步方法,其作用的範圍是整個方法,作用於呼叫物件

Java執行記憶體可見性

可見性:一個執行緒對共享變數值的修改,能夠及時的被其他執行緒看到。  共享變數:如果一個電量在多個執行緒的工作記憶體中都存在副本,那麼這個變數就是這幾個執行緒的共享變數。 JAVA記憶體模型(Java Memory Model)描述了Java程式中各種變數(執行緒共享變

Java執行-併發synchronized 關鍵字

synchronized 關鍵字 答: 底層實現: 進入時,執行 monitorenter,將計數器 +1,釋放鎖 monitorexit 時,計數器 -1 當一個執行緒判斷到計數器為 0 時,則當前鎖空閒,可以佔用;反之,當前執行緒進入等待狀態 含義

java執行synchronized

關鍵字synchronized取得的鎖都是物件鎖,而不是把一段程式碼或方法(函式)當作鎖,這裡如果是把一段程式碼或方法(函式)當作鎖,其實獲取的也是物件鎖,只是監視器(物件)不同而已,哪個執行緒先執行帶synchronized關鍵字的方法,哪個執行緒就持有該方法所屬物件的鎖,其他執行緒都只能呈等待狀

Java執行學習wait、notify/notifyAll

轉載 https://www.cnblogs.com/moongeek/p/7631447.html 1、wait()、notify/notifyAll() 方法是Object的本地final方法,無法被重寫。 2、wait()使當前執行緒阻塞,前提是 必須先獲

理解 JAVA執行技術

1.    虛假的多執行緒     例1:     public class TestThread     {     int i=0, j=0;     public void go(int flag){     while(true){     try{    

JAVA執行問題

  一、死鎖是什麼? 舉個例子:兩個人一起吃飯,每個人都拿了一隻筷子,雙方都在等待對方將筷子讓給自己,結果兩個人都吃不了飯。這種情況和計算機中的死鎖情況很相似。 假設有兩個執行緒,互相等待對方釋放佔有的鎖,但是釋放鎖的條件又不可能形成,這時候死鎖就形成了。 還是買票的問題,有的時候時會發生死

Java執行-併發執行產生死的4個必要條件?如何避免死

多執行緒產生死鎖的4個必要條件? 答: 互斥條件:一個資源每次只能被一個執行緒使用 請求與保持條件:一個執行緒因請求資源而阻塞時,對已獲得的資源保持不放 不剝奪條件:程序已經獲得的資源,在未使用完之前,不能強行剝奪 迴圈等待條件:若干執行緒之間形成一種頭

Java資深架構師大廠執行面試題,細談併發程式設計深造歷程

  多執行緒、執行緒池 多執行緒是實現併發機制的一種有效手段。程序和執行緒一樣,都是實現併發的一個基本單位。執行緒是比程序更小的執行單位,執行緒是程序的基礎之上進行進一步的劃分。所謂多執行緒是指一個程序在執行過程中可以產生多個更小的程式單元,這些更小的單元稱為執行緒,這

Java 高併發程式設計執行與架構設計

內容簡介 本書主要包含四個部分: 部分主要闡述 Thread 的基礎知識,詳細介紹執行緒的 API 使用、執行緒安全、執行緒間資料通訊,以及如何保護共享資源等內容,它是深入學習多執行緒內容的基礎。 第二部分引入了 ClassLoader,這是因為 ClassLoader 與執行緒不無關係

Java執行優化偏向原理分析

本文來自Ken Wu's Blog,原文標題:《Java偏向鎖實現原理(Biased Locking)》 閱讀本文的讀者,需要對Java輕量級鎖有一定的瞭解,知道lock record, mark word之類的名詞。 Java偏向鎖(Biased Locking)是

java執行程式設計讀寫設計高效能快取器

解決多執行緒執行緒安全問題的主要方法是通過加鎖的方式來實現,當多個執行緒對某個變數進行讀取或寫入的時候通過加鎖來限定只有當前獲取鎖許可權的執行緒才可以對資料進行讀寫,當該執行緒訪問完畢釋放鎖之後其他阻

lock Mutex Monitor 之間的區別與, .net 執行 同步非同步操作,

Framework為我們提供了三個加鎖的機制,分別是Monitor類、Lock關 鍵字和Mutex類。   總體而言,lock和monitor可以鎖定物件,也可以鎖定函式;而mutex一般用於鎖定函式,並保證不同執行緒間同步呼叫函式,而不會受執行緒優先順序影響。使用lo

Java執行基礎物件的同步與非同步

同步:synchronized 同步的概念就是共享,如果不是共享的資源,就沒有必要進行同步。 非同步:asynchronized 非同步的概念就是獨立,相互之間不受到任何制約。 同步的目的就是為了執行緒安全,對於

java執行程式設計使用Synchronized塊同步變數

通過synchronized塊來同步特定的靜態或非靜態方法。 要想實現這種需求必須為這些特性的方法定義一個類變數,然後將這些方法的程式碼用synchronized塊括起來,並將這個類變數作為引數傳入synchronized塊   下面的程式碼演示瞭如何同步特定的類方法:

Java執行——概念與優化

為了效能與使用的場景,Java實現鎖的方式有非常多。而關於鎖主要的實現包含synchronized關鍵字、AQS框架下的鎖,其中的實現都離不開以下的策略。 悲觀鎖與樂觀鎖 樂觀鎖。樂觀的想法,認為併發讀多寫少。每次操作的時候都不上鎖,直到更新的時候才通過CAS判斷更新。對於AQS框架下的鎖,初始就是

Java執行-併發執行

執行緒池有了解嗎? 答: java.util.concurrent.ThreadPoolExecutor 類就是一個執行緒池。客戶端呼叫ThreadPoolExecutor.submit(Runnable task) 提交任務,執行緒池內部維護的工作者執行緒的數量就是該執行緒池的執行

Java執行-併發sleep() 和 wait(n) 、 wait() 的區別

sleep() 和 wait(n) 、 wait() 的區別 答: sleep 方法:是 Thread 類的靜態方法,當前執行緒將睡眠 n 毫秒,執行緒進入阻塞狀態。當睡眠時間到了,會接觸阻塞,進入可執行狀態,等待 CPU 的到來。睡眠不釋放鎖(如果有的話) wai

Java執行-併發執行和程序的區別

執行緒和程序的區別 答: 程序是一個“執行中的程式”,是系統進行資源分配和排程的一個獨立單位 執行緒是程序的一個實體,一個程序中擁有多個執行緒,執行緒之間共享地址空間和其他資源(所以通訊和同步等操作執行緒比程序更加容易) 執行緒上下文的切換比程序上下文切換要快

Java執行-併發如何制定執行執行順序?

文章目錄 如何讓10個執行緒按照順序列印0123456789? 程式碼如下: 1.建立一個鎖物件類 2.建立一個執行緒類 3.測試類 如何讓10個執行緒按照順序列印012