Java中 volatile的使用和理解

簡介

Volatile 是 Java 虛擬機提供輕量級的同步機制。它有三個特性:緩存

  • 保證可見性
  • 不保證原子性
  • 禁止指令重排

基本原理

當對非volatile變量進行讀寫的時候,每一個線程先從主內存拷貝變量到CPU緩存中,若是計算機有多個CPU,每一個線程可能在不一樣的CPU上被處理,這意味着每一個線程能夠拷貝到不一樣的CPU cache中。
  volatile共享變量則會在修改以後當即刷新到內存中,並使其餘CPU中的變量失效,使得其餘CPU使用時必須從內存中從新加載。(這就保證了可見性優化

image-20210131171850157

在volatile修飾的共享變量在進行寫操做的時候會多出來一行彙編代碼,這一行彙編代碼在多核處理器下會引起兩件事情:spa

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

禁止指令重排

  指令重排序是JVM爲了優化指令、提升程序運行效率,在不影響單線程程序執行結果的前提下,儘量地提升並行度。指令重排序包括編譯器重排序和運行時重排序。
  在JDK1.5以後,可使用volatile變量禁止指令重排序。針對volatile修飾的變量,在讀寫操做指令先後會插入內存屏障,指令重排序時不能把後面的指令重排序到內存屏.net

image-20210131173544509

爲何volatile不能保證原子性

首先須要瞭解的是,Java中只有對基本類型變量的賦值和讀取是原子操做,如i = 1的賦值操做,可是像j = i或者i++這樣的操做都不是原子操做,由於他們都進行了屢次原子操做,好比先讀取i的值,再將i的值賦值給j,兩個原子操做加起來就不是原子操做了。線程

因此,若是一個變量被volatile修飾了,那麼確定能夠保證每次讀取這個變量值的時候獲得的值是最新的,可是一旦須要對變量進行自增這樣的非原子操做,就不會保證這個變量的原子性了。3d

舉個栗子:blog

一個變量i被volatile修飾,兩個線程想對這個變量修改,都對其進行自增操做也就是i++,i++的過程能夠分爲三步,首先獲取i的值,其次對i的值進行加1,最後將獲得的新值寫會到緩存中。
線程A首先獲得了i的初始值100,可是還沒來得及修改,就阻塞了,這時線程B開始了,它也獲得了i的值,因爲i的值未被修改,即便是被volatile修飾,主存的變量還沒變化,那麼線程B獲得的值也是100,以後對其進行加1操做,獲得101後,將新值寫入到緩存中,再刷入主存中。根據可見性的原則,這個主存的值能夠被其餘線程可見。
問題來了,線程A已經讀取到了i的值爲100,也就是說讀取的這個原子操做已經結束了,因此這個可見性來的有點晚,線程A阻塞結束後,繼續將100這個值加1,獲得101,再將值寫到緩存,最後刷入主存,因此即使是volatile具備可見性,也不能保證對它修飾的變量具備原子性。

JMM Java內存模型

JMM : Java內存模型,不存在的東西,概念!約定!排序

image-20210131180940997

關於JMM的一些同步的約定:內存

  • 一、線程解鎖前,必須把共享變量馬上刷回主存。
  • 二、線程加鎖前,必須讀取主存中的最新值到工做內存中!
  • 三、加鎖和解鎖是同一把鎖

內存交互操做有8種,虛擬機實現必須保證每個操做都是原子的,不可在分的(對於double和long類型的變量來講,load、store、read和write操做在某些平臺上容許例外)rem

  • lock (鎖定):做用於主內存的變量,把一個變量標識爲線程獨佔狀態
  • unlock (解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定
  • read (讀取):做用於主內存變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用
  • load (載入):做用於工做內存的變量,它把read操做從主存中變量放入工做內存中
  • use (使用):做用於工做內存中的變量,它把工做內存中的變量傳輸給執行引擎,每當虛擬機遇到一個須要使用到變量的值,就會使用到這個指令
  • assign (賦值):做用於工做內存中的變量,它把一個從執行引擎中接受到的值放入工做內存的變量副本中
  • store (存儲):做用於主內存中的變量,它把一個從工做內存中一個變量的值傳送到主內存中,以便後續的write使用
  • write (寫入):做用於主內存中的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中

JMM對這八種指令的使用,制定了以下規則:

  • 不容許read和load、store和write操做之一單獨出現。即便用了read必須load,使用了store必須write
  • 不容許線程丟棄他最近的assign操做,即工做變量的數據改變了以後,必須告知主存
  • 不容許一個線程將沒有assign的數據從工做內存同步回主內存
  • 一個新的變量必須在主內存中誕生,不容許工做內存直接使用一個未被初始化的變量。就是懟變量實施use、store操做以前,必須通過assign和load操做
  • 一個變量同一時間只有一個線程能對其進行lock。屢次lock後,必須執行相同次數的unlock才能解鎖
  • 若是對一個變量進行lock操做,會清空全部工做內存中此變量的值,在執行引擎使用這個變量前,必須從新load或assign操做初始化變量的值
  • 若是一個變量沒有被lock,就不能對其進行unlock操做。也不能unlock一個被其餘線程鎖住的變量
  • 對一個變量進行unlock操做以前,必須把此變量同步回主內存

參考

volatile的原理

相關文章
相關標籤/搜索