欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

java同步之volatile解析

 更新時間:2019年05月24日 09:17:18   作者:彤哥讀源碼  
volatile可以說是Java虛擬機提供的最輕量級的同步機制了,了解volatile的語義對理解多線程的特性具有很重要的意義,下面小編帶大家一起學(xué)習(xí)一下

問題

(1)volatile是如何保證可見性的?

(2)volatile是如何禁止重排序的?

(3)volatile的實現(xiàn)原理?

(4)volatile的缺陷?

簡介

volatile可以說是Java虛擬機提供的最輕量級的同步機制了,但是它并不容易被正確地理解,以至于很多人不習(xí)慣使用它,遇到多線程問題一律使用synchronized或其它鎖來解決。

了解volatile的語義對理解多線程的特性具有很重要的意義,所以彤哥專門寫了一篇文章來解釋volatile的語義到底是什么。

語義一:可見性

前面介紹Java內(nèi)存模型的時候,我們說過可見性是指當(dāng)一個線程修改了共享變量的值,其它線程能立即感知到這種變化。

關(guān)于Java內(nèi)存模型的講解請參考【細談java同步之JMM(Java Memory Model)】。

而普通變量無法做到立即感知這一點,變量的值在線程之間的傳遞均需要通過主內(nèi)存來完成,比如,線程A修改了一個普通變量的值,然后向主內(nèi)存回寫,另外一條線程B只有在線程A的回寫完成之后再從主內(nèi)存中讀取變量的值,才能夠讀取到新變量的值,也就是新變量才能對線程B可見。

在這期間可能會出現(xiàn)不一致的情況,比如:

(1)線程A并不是修改完成后立即回寫;

(線路A修改了變量x的值為5,但是還沒有回寫,線程B從主內(nèi)存讀取到的還舊值0)

(2)線程B還在用著自己工作內(nèi)存中的值,而并不是立即從主內(nèi)存讀取值;

 

(線程A回寫了變量x的值為5到主內(nèi)存中,但是線程B還沒有讀取主內(nèi)存的值,依舊在使用舊值0在進行運算)

基于以上兩種情況,所以,普通變量都無法做到立即感知這一點。

但是,volatile變量可以做到立即感知這一點,也就是volatile可以保證可見性。

java內(nèi)存模型規(guī)定,volatile變量的每次修改都必須立即回寫到主內(nèi)存中,volatile變量的每次使用都必須從主內(nèi)存刷新最新的值。

 

volatile的可見性可以通過下面的示例體現(xiàn):

public class VolatileTest {
 // public static int finished = 0;
 public static volatile int finished = 0;

 private static void checkFinished() {
 while (finished == 0) {
  // do nothing
 }
 System.out.println("finished");
 }

 private static void finish() {
 finished = 1;
 }

 public static void main(String[] args) throws InterruptedException {
 // 起一個線程檢測是否結(jié)束
 new Thread(() -> checkFinished()).start();

 Thread.sleep(100);

 // 主線程將finished標(biāo)志置為1
 finish();

 System.out.println("main finished");

 }
}

在上面的代碼中,針對finished變量,使用volatile修飾時這個程序可以正常結(jié)束,不使用volatile修飾時這個程序永遠不會結(jié)束。

因為不使用volatile修飾時,checkFinished()所在的線程每次都是讀取的它自己工作內(nèi)存中的變量的值,這個值一直為0,所以一直都不會跳出while循環(huán)。

使用volatile修飾時,checkFinished()所在的線程每次都是從主內(nèi)存中加載最新的值,當(dāng)finished被主線程修改為1的時候,它會立即感知到,進而會跳出while循環(huán)。

語義二:禁止重排序

前面介紹Java內(nèi)存模型的時候,我們說過Java中的有序性可以概括為一句話:如果在本線程中觀察,所有的操作都是有序的;如果在另一個線程中觀察,所有的操作都是無序的。

前半句是指線程內(nèi)表現(xiàn)為串行的語義,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存和主內(nèi)存同步延遲”現(xiàn)象。

普通變量僅僅會保證在該方法的執(zhí)行過程中所有依賴賦值結(jié)果的地方都能獲得正確的結(jié)果,而不能保證變量賦值操作的順序與程序代碼中的執(zhí)行順序一致,因為一個線程的方法執(zhí)行過程中無法感知到這點,這就是“線程內(nèi)表現(xiàn)為串行的語義”。

比如,下面的代碼:

// 兩個操作在一個線程
int i = 0;
int j = 1;

上面兩句話沒有依賴關(guān)系,JVM在執(zhí)行的時候為了充分利用CPU的處理能力,可能會先執(zhí)行int j = 1;這句,也就是重排序了,但是在線程內(nèi)是無法感知的。

看似沒有什么影響,但是如果是在多線程環(huán)境下呢?

我們再看一個例子:

public class VolatileTest3 {
 private static Config config = null;
 private static volatile boolean initialized = false;

 public static void main(String[] args) {
 // 線程1負責(zé)初始化配置信息
 new Thread(() -> {
  config = new Config();
  config.name = "config";
  initialized = true;
 }).start();

 // 線程2檢測到配置初始化完成后使用配置信息
 new Thread(() -> {
  while (!initialized) {
  LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
  }

  // do sth with config
  String name = config.name;
 }).start();
 }
}

class Config {
 String name;
}

這個例子很簡單,線程1負責(zé)初始化配置,線程2檢測到配置初始化完畢,使用配置來干一些事。

在這個例子中,如果initialized不使用volatile來修飾,可能就會出現(xiàn)重排序,比如在初始化配置之前把initialized的值設(shè)置為了true,這樣線程2讀取到這個值為true了,就去使用配置了,這時候可能就會出現(xiàn)錯誤。

(此處這個例子只是用于說明重排序,實際運行時很難出現(xiàn)。)

通過這個例子,彤哥相信大家對“如果在本線程內(nèi)觀察,所有操作都是有序的;在另一個線程觀察,所有操作都是無序的”有了更深刻的理解。

所以,重排序是站在另一個線程的視角的,因為在本線程中,是無法感知到重排序的影響的。

而volatile變量是禁止重排序的,它能保證程序?qū)嶋H運行是按代碼順序執(zhí)行的。

實現(xiàn):內(nèi)存屏障

上面講了volatile可以保證可見性和禁止重排序,那么它是怎么實現(xiàn)的呢?

答案就是,內(nèi)存屏障。

內(nèi)存屏障有兩個作用:

(1)阻止屏障兩側(cè)的指令重排序;

(2)強制把寫緩沖區(qū)/高速緩存中的數(shù)據(jù)回寫到主內(nèi)存,讓緩存中相應(yīng)的數(shù)據(jù)失效;

關(guān)于“內(nèi)存屏障”的知識點,各路大神的觀點也不完全一致,所以這里也就不展開講述了,感興趣的可以看看下面的文章:

(1)Doug Lea的《The JSR-133 Cookbook for Compiler Writers》

http://g.oswego.edu/dl/jmm/cookbook.html

Doug Lea 就是java并發(fā)包的作者,大牛!

(2)Martin Thompson的《Memory Barriers/Fences》

https://mechanical-sympathy.blogspot.com/2011/07/memory-barriersfences.html

Martin Thompson 專注于把性能提升到極致,專注于從硬件層面思考問題,比如如何避免偽共享等,大牛!

它的博客地址就是上面這個地址,里面有很多底層的知識,有興趣的可以去看看。

(3)Dennis Byrne的《Memory Barriers and JVM Concurrency》

https://www.infoq.com/articles/memory_barriers_jvm_concurrency

這是InfoQ英文站上面的一篇文章,我覺得寫的挺好的,基本上綜合了上面的兩種觀點,并從匯編層面分析了內(nèi)存屏障的實現(xiàn)。

目前國內(nèi)市面上的關(guān)于內(nèi)存屏障的講解基本不會超過這三篇文章,包括相關(guān)書籍中的介紹。

我們還是來看一個例子來理解內(nèi)存屏障的影響:

public class VolatileTest4 {
 // a不使用volatile修飾
 public static long a = 0;
 // 消除緩存行的影響
 public static long p1, p2, p3, p4, p5, p6, p7;
 // b使用volatile修飾
 public static volatile long b = 0;
 // 消除緩存行的影響
 public static long q1, q2, q3, q4, q5, q6, q7;
 // c不使用volatile修飾
 public static long c = 0;

 public static void main(String[] args) throws InterruptedException {
 new Thread(()->{
  while (a == 0) {
  long x = b;
  }
  System.out.println("a=" + a);
 }).start();

 new Thread(()->{
  while (c == 0) {
  long x = b;
  }
  System.out.println("c=" + c);
 }).start();

 Thread.sleep(100);

 a = 1;
 b = 1;
 c = 1;
 }
}

這段代碼中,a和c不使用volatile修飾,b使用volatile修飾,而且我們在a/b、b/c之間各加入7個long字段消除偽共享的影響。

關(guān)于偽共享的相關(guān)知識,可以查看彤哥之前寫的文章【雜談 什么是偽共享(false sharing)?】。

在a和c的兩個線程的while循環(huán)中我們獲取一下b,你猜怎樣?如果把long x = b;這行去掉呢?運行試試吧。

彤哥這里直接說結(jié)論了:volatile變量的影響范圍不僅僅只包含它自己,它會對其上下的變量值的讀寫都有影響。

缺陷

上面我們介紹了volatile關(guān)鍵字的兩大語義,那么,volatile關(guān)鍵字是不是就是萬能的了呢?

當(dāng)然不是,忘了我們內(nèi)存模型那章說的一致性包括的三大特性了么?

一致性主要包含三大特性:原子性、可見性、有序性。

volatile關(guān)鍵字可以保證可見性和有序性,那么volatile能保證原子性么?

請看下面的例子:

public class VolatileTest5 {
 public static volatile int counter = 0;

 public static void increment() {
 counter++;
 }

 public static void main(String[] args) throws InterruptedException {
 CountDownLatch countDownLatch = new CountDownLatch(100);
 IntStream.range(0, 100).forEach(i->
  new Thread(()-> {
   IntStream.range(0, 1000).forEach(j->increment());
   countDownLatch.countDown();
  }).start());

 countDownLatch.await();

 System.out.println(counter);
 }
}

這段代碼中,我們起了100個線程分別對counter自增1000次,一共應(yīng)該是增加了100000,但是實際運行結(jié)果卻永遠不會達到100000。

讓我們來看看increment()方法的字節(jié)碼(IDEA下載相關(guān)插件可以查看):

0 getstatic #2 <com/coolcoding/code/synchronize/VolatileTest5.counter>
3 iconst_1
4 iadd
5 putstatic #2 <com/coolcoding/code/synchronize/VolatileTest5.counter>
8 return

可以看到counter++被分解成了四條指令:

(1)getstatic,獲取counter當(dāng)前的值并入棧

(2)iconst_1,入棧int類型的值1

(3)iadd,將棧頂?shù)膬蓚€值相加

(4)putstatic,將相加的結(jié)果寫回到counter中

由于counter是volatile修飾的,所以getstatic會從主內(nèi)存刷新最新的值,putstatic也會把修改的值立即同步到主內(nèi)存。

但是中間的兩步iconst_1和iadd在執(zhí)行的過程中,可能counter的值已經(jīng)被修改了,這時并沒有重新讀取主內(nèi)存中的最新值,所以volatile在counter++這個場景中并不能保證其原子性。

volatile關(guān)鍵字只能保證可見性和有序性,不能保證原子性,要解決原子性的問題,還是只能通過加鎖或使用原子類的方式解決。

進而,我們得出volatile關(guān)鍵字使用的場景:

(1)運算的結(jié)果并不依賴于變量的當(dāng)前值,或者能夠確保只有單一的線程修改變量的值;

(2)變量不需要與其他狀態(tài)變量共同參與不變約束。

說白了,就是volatile本身不保證原子性,那就要增加其它的約束條件來使其所在的場景本身就是原子的。

比如:

private volatile int a = 0;

// 線程A
a = 1;

// 線程B
if (a == 1) {
 // do sth
}

a = 1;這個賦值操作本身就是原子的,所以可以使用volatile來修飾。

總結(jié)

(1)volatile關(guān)鍵字可以保證可見性;

(2)volatile關(guān)鍵字可以保證有序性;

(3)volatile關(guān)鍵字不可以保證原子性;

(4)volatile關(guān)鍵字的底層主要是通過內(nèi)存屏障來實現(xiàn)的;

(5)volatile關(guān)鍵字的使用場景必須是場景本身就是原子的;

以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。

相關(guān)文章

  • Java?DelayQueue實現(xiàn)任務(wù)延時示例講解

    Java?DelayQueue實現(xiàn)任務(wù)延時示例講解

    DelayQueue是一個無界的BlockingQueue的實現(xiàn)類,用于放置實現(xiàn)了Delayed接口的對象,其中的對象只能在其到期時才能從隊列中取走。本文就來利用DelayQueue實現(xiàn)延時任務(wù),感興趣的可以了解一下
    2022-09-09
  • Spring Boot自定義錯誤視圖的方法詳解

    Spring Boot自定義錯誤視圖的方法詳解

    這篇文章主要介紹了Spring Boot自定義錯誤視圖的方法詳解,需要的朋友可以參考下
    2020-08-08
  • Spring中注解方式的異步請求

    Spring中注解方式的異步請求

    今天給大家整理了Spring中注解方式的異步請求的知識點,對正在學(xué)習(xí)java的小伙伴們很有幫助,需要的朋友可以參考下
    2021-06-06
  • 詳解SpringMVC學(xué)習(xí)系列(6) 之 數(shù)據(jù)驗證

    詳解SpringMVC學(xué)習(xí)系列(6) 之 數(shù)據(jù)驗證

    這篇文章主要介紹了詳解SpringMVC學(xué)習(xí)系列(6) 之 數(shù)據(jù)驗證 ,具有一定的參考價值,感興趣的小伙伴們可以參考一下。
    2016-12-12
  • Java數(shù)據(jù)結(jié)構(gòu)及算法實例:三角數(shù)字

    Java數(shù)據(jù)結(jié)構(gòu)及算法實例:三角數(shù)字

    這篇文章主要介紹了Java數(shù)據(jù)結(jié)構(gòu)及算法實例:三角數(shù)字,本文直接給出實現(xiàn)代碼,代碼中包含詳細注釋,需要的朋友可以參考下
    2015-06-06
  • 為什么Java項目中別用!=null做判空

    為什么Java項目中別用!=null做判空

    本文主要介紹了為什么Java項目中別用!=null做判空,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2023-04-04
  • Java?9中的"菱形"語法詳解

    Java?9中的"菱形"語法詳解

    Java?9?再次增強了“菱形”語法,它甚至允許在創(chuàng)建匿名內(nèi)部類時使用菱形語法,Java?可根據(jù)上下文來推斷匿名內(nèi)部類中泛型的類型,下面程序示范了在匿名內(nèi)部類中使用菱形語法,感興趣的朋友跟隨小編一起看看吧
    2023-06-06
  • 詳解MyBatis開發(fā)Dao層的兩種方式(Mapper動態(tài)代理方式)

    詳解MyBatis開發(fā)Dao層的兩種方式(Mapper動態(tài)代理方式)

    這篇文章主要介紹了詳解MyBatis開發(fā)Dao層的兩種方式(Mapper動態(tài)代理方式),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-12-12
  • spring整合struts2過程詳解

    spring整合struts2過程詳解

    這篇文章主要介紹了spring整合struts2過程詳解,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下
    2020-01-01
  • 細數(shù)java中Long與Integer比較容易犯的錯誤總結(jié)

    細數(shù)java中Long與Integer比較容易犯的錯誤總結(jié)

    下面小編就為大家?guī)硪黄殧?shù)java中Long與Integer比較容易犯的錯誤總結(jié)。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-01-01

最新評論