Java併發編程系列---volatile和synchronized關鍵字詳解

一、簡介

在多線程併發編程中synchronized和volatile都扮演着重要的角色,volatile是輕量級的 synchronized,它在多處理器開發中保證了共享變量的“可見性”。可見性的意思是當一個 線程修改一個共享變量時,另外一個線程能讀到這個修改的值。如果volatile變量修飾符使 用恰當的話,它比synchronized的使用和執行成本更低,因爲它不會引起線程上下文的切 換和調度。

二、volatile的定義與實現原理

Java編程語言允許線程訪問共享變量, 爲了確保共享變量能被準確和一致地更新,線程應該確保通過排他鎖單獨獲得這個變量。 Java語言提供了volatile,在某些情況下比鎖要更加方便。如果一個字段被聲明成volatile, Java線程內存模型確保所有線程看到這個變量的值是一致的。也就是說volatile關鍵字只保證了線程的可見性,沒有保證其原子性。而且volatile關鍵字禁止JVM和處理器對其修飾的指令進行重排序。

有volatile變量修飾的共享變量進行寫操作的時候會多出一彙編代碼(具有Lock前綴),Lock前綴的指令在多核處理器下會引發了兩件事情。

  1. 將當前處理器緩存行的數據寫回到系統內存。
  2. 這個寫回內存的操作會使在其他CPU裏緩存了該內存地址的數據無效。

爲了提高處理速度,處理器不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存後再進行操作,但操作完不知道何時會寫到內存。如果對聲明瞭volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量 所在緩存行的數據寫回到系統內存。但是,就算寫回到內存,如果其他處理器緩存的值還 是舊的,再執行計算操作就會有問題。所以,在多處理器下,爲了保證各個處理器的緩存 是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自 己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前 處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系 統內存中把數據讀到處理器緩存裏。

2.1 爲什麼volatile保證不了原子性

因爲volatile進行讀寫操作的時候,需要三步。

  1. 先去主內存讀取值,然後寫回(緩存)工作內存
  2. 然後在工作內存進行加減操作或者一些別的事務操作。
  3. 然後再寫入到主內存

這三步,都是原子操作,但是,合到一起就不是原子操作了,因爲每一步都有可能被其他線程打斷。
具體使用:
在這裏插入圖片描述

三、synchronized的實現原理與應用

在多線程併發編程中synchronized一直是元老級角色,很多人都會稱呼它爲重量級鎖。但是,隨着Java SE 1.6對synchronized進行了各種優化之後,有些情況下它就並不那麼重了。

利用synchronized實現同步的基礎:Java中的每一個對象都可以作爲鎖。具體表現爲以下3種形式。

  1. 對於普通同步方法,鎖是當前實例對象。
  2. 對於靜態同步方法,鎖是當前類的Class對象。
  3. 對於同步方法塊,鎖是Synchonized括號裏配置的對象。

當一個線程試圖訪問同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放 鎖。那麼鎖到底存在哪裏呢?鎖裏面會存儲什麼信息呢?
從JVM規範中可以看到Synchonized在JVM裏的實現原理,JVM基於進入和退出 Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使 用monitorenter和monitorexit指令實現的,而方法同步是使用另外一種方式實現的,細節在 JVM規範裏並沒有詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現。
monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit是插入到方 法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何 對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態。線程執 行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象 的鎖。

在這裏插入圖片描述
上圖可以看出任意線程對Object(Object由synchronized保護)的訪問,首先要 獲得Object的監視器。如果獲取失敗,線程進入同步隊列,線程狀態變爲BLOCKED。當 訪問Object的前驅(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的 線程,使其重新嘗試對監視器的獲取。
隨便寫個demo,加上synchronized關鍵字。查看下字節碼文件就可以發現了。
在這裏插入圖片描述

四、volatile和synchronized的區別

  1. 使用上的區別
    volatile關鍵字只能用於修飾實例變量或者類變量,不能用於修飾方法以及方法參數和局部變量、常量等。
    synchronized關鍵字不能用於對變量的修飾,只能用於修飾方法或者語句塊。
    volatile修飾的變量可以爲null, synchronized 關鍵字同步語句塊的monitor對象不能爲null。

  2. 對原子性的保證
    volatile無法保證原子性。
    由於synchronized是一種排他的機制,因此被synchronized關鍵字修飾的同步代碼是無法被中途打斷的,因此其能夠保證代碼的原子性。

  3. 對可見性的保證
    兩者均可以保證共享資源在多線程間的可見性,但是實現機制完全不同。
    synchronized藉助於JVM指令monitorenter和monitorexit對通過排他的方式使得同步代碼串行化,在monitorexit時所有共享資源都將會被刷新到主內存中。
    相比較於synchronized關鍵字volatile使用機器指令(偏硬件)“lock ;”的方式迫使其他線程工作內存中的數據失效,不得到主內存中進行再次加載。

  4. 對有序性的保證
    volatile關鍵字禁止JVM編譯器以及處理器對其進行重排序,所以它能夠保證有序性。.
    雖然synchronized關鍵字所修飾的同步方法也可以保證順序性,但是這種順序性是以程序的串行化執行換來的,在synchronized關鍵字所修飾的代碼塊中代碼指令也會發生指令重排序的情況,比如:

    synchronized(this){
    int x=10;
    int y=20;
    x++ ;
    y = y+1;
    

    x和y誰最先定義以及誰最先進行運算,對程序來說沒有任何的影響,另外x和y之間也沒有依賴關係,但是由於synchronized關鍵字同步的作用,在synchronized的作用域結束時x必定是11, y必定是21,也就是說達到了最終的輸出結果和代碼編寫順序的一致性。

  5. 其他
    volatile不會使線程陷入阻塞。
    synchronized關鍵字會使線程進人阻塞狀態。

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