volatile和synchronized到底啥區別?多圖文講解告訴你

2021-01-07 碼農小胖哥

你有一個思想,我有一個思想,我們交換後,一個人就有兩個思想If you can NOT explain it simply, you do NOT understand it well enough

現陸續將Demo代碼和技術文章整理在一起 Github實踐精選 ,方便大家閱讀查看,本文同樣收錄在此,覺得不錯,還請Star

之前寫了幾篇 Java並發編程的系列 文章,有個朋友微群裡問我,還是不能理解 volatile 和 synchronized 二者的區別, 他的問題主要可以歸納為這幾個:

volatile 與 synchronized 在處理哪些問題是相對等價的?為什麼說 volatile 是 synchronized 弱同步的方式?volatile 除了可見性問題,還能解決什麼問題?二者我要如何選擇使用?如果你不能回答上面的幾個問題,說明你對二者的區別還有一些含混。本文就通過圖文的方式好好說說他們微妙的關係

都聽過【天上一天,地下一年】,假設 CPU 執行一條普通指令需要一天,那麼 CPU 讀寫內存就得等待一年的時間。

受【木桶原理】的限制,在CPU眼裡,程序的整體性能都被內存的辦事效率拉低了,為了解決這個短板,硬體同學也使用了我們做軟體常用的提速策略——使用緩存Cache(實則是硬體同學給軟體同學挖的坑)

Java 內存模型(JMM)

CPU 增加了緩存均衡了與內存的速度差異,這一增加還是好幾層。

此時內存的短板不再那麼明顯,CPU甚喜。但隨之卻帶來很多問題

看上圖,每個核都有自己的一級緩存(L1 Cache),有的架構裡面還有所有核共用的二級緩存(L2 Cache)。使用緩存之後,當線程要訪問共享變量時,如果 L1 中存在該共享變量,就不會再逐級訪問直至主內存了。所以,通過這種方式,就補上了訪問內存慢的短板

具體來說,線程讀/寫共享變量的步驟是這樣:

從主內存複製共享變量到自己的工作內存在工作內存中對變量進行處理處理完後,將變量值更新回主內存假設現在主內存中有共享變量 X, 其初始值為 0

線程1先訪問變量 X, 套用上面的步驟就是這樣:

L1 和 L2 中都沒有發現變量 X,直到在主內存中找到拷貝變量 X 到 L1 和 L2 中在 L1 中將 X 的值修改為1,並逐層寫回到主內存中此時,在線程 1 眼中,X 的值是這樣的:

接下來,線程 2 同樣按照上面的步驟訪問變量 X

L1 中沒有發現變量 XL2 中發現了變量X從L2中拷貝變量到L1中在L1中將X 的值修改為2,並逐層寫回到主內存中此時,線程 2 眼中,X 的值是這樣的:

結合剛剛的兩次操作,當線程1再訪問變量x,我們看看有什麼問題:

此刻,如果線程 1 再次將 x=1回寫,就會覆蓋線程2 x=2 的結果,同樣的共享變量,線程拿到的結果卻不一樣(線程1眼中x=1;線程2眼中x=2),這就是共享變量內存不可見的問題。

怎麼補坑呢?今天的兩位主角閃亮登場,不過在說明 volatile關鍵字之前,我們先來說說你最熟悉的 synchronized 關鍵字

synchronized

遇到線程不安全的問題,習慣性的會想到用 synchronized 關鍵字來解決問題,暫且先不論該辦法是否合理,我們來看 synchronized 關鍵字是怎麼解決上面提到的共享變量內存可見性問題的

【進入】synchronized 塊的內存語義是把在 synchronized 塊內使用的變量從線程的工作內存中清除,從主內存中讀取【退出】synchronized 塊的內存語義事把在 synchronized 塊內對共享變量的修改刷新到主內存中二話不說,無情向下看 volatile

volatile

當一個變量被聲明為 volatile 時:

線程在【讀取】共享變量時,會先清空本地內存變量值,再從主內存獲取最新值線程在【寫入】共享變量時,不會把值緩存在寄存器或其他地方(就是剛剛說的所謂的「工作內存」),而是會把值刷新回主內存有種換湯不換藥的感覺,你看的一點都沒錯

所以,當使用 synchronized 或 volatile 後,多線程操作共享變量的步驟就變成了這樣:

簡單點來說就是不再參考 L1 和 L2 中共享變量的值,而是直接訪問主內存

來點踏實的,上例子

publicclassThreadNotSafeInteger{/** * 共享變量 value */privateint value;publicintgetValue(){return value; }publicvoidsetValue(int value){this.value = value; }}經過前序分析鋪墊,很明顯,上面代碼中,共享變量 value 存在大大的隱患,嘗試對其作出一些改變

先使用 volatile 關鍵字改造:

publicclassThreadSafeInteger{/** * 共享變量 value */privatevolatileint value;publicintgetValue(){return value; }publicvoidsetValue(int value){this.value = value; }}再使用 synchronized 關鍵字改造

publicclassThreadSafeInteger{/** * 共享變量 value */privateint value;publicsynchronizedintgetValue(){return value; }publicsynchronizedvoidsetValue(int value){this.value = value; }}這兩個結果是完全相同,在解決【當前】共享變量數據可見性的問題上,二者算是等同的

如果說 synchronized 和 volatile 是完全等同的,那就沒必要設計兩個關鍵字了,繼續看個例子

@Slf4jpublicclassVisibilityIssue{privatestaticfinalint TOTAL = 10000;// 即便像下面這樣加了 volatile 關鍵字修飾不會解決問題,因為並沒有解決原子性問題privatevolatileint count;publicstaticvoidmain(String[] args){ VisibilityIssue visibilityIssue = new VisibilityIssue(); Thread thread1 = new Thread(() -> visibilityIssue.add10KCount()); Thread thread2 = new Thread(() -> visibilityIssue.add10KCount()); thread1.start(); thread2.start();try { thread1.join(); thread2.join(); } catch (InterruptedException e) { log.error(e.getMessage()); } log.info("count 值為:{}", visibilityIssue.count); }privatevoidadd10KCount(){int start = 0;while (start ++ < TOTAL){this.count ++; } }}其實就是將上面setValue 簡單賦值操作 (this.value = value;)變成了 (this.count ++;)形式,如果你運行代碼,你會發現,count的值始終是處於1w和2w之間的

將上面方法再以 synchronized 的形式做改動

@Slf4jpublicclassVisibilityIssue{privatestaticfinalint TOTAL = 10000;privateint count;//... 同上privatesynchronizedvoidadd10KCount(){int start = 0;while (start ++ < TOTAL){this.count ++; } }}再次運行代碼,count 結果就是 2w

兩組代碼,都通過 volatile 和 synchronized 關鍵字以同樣形式修飾,怎麼有的可以帶來相同結果,有的卻不能呢?

這就要說說二者的不同了

count++ 程序代碼是一行,但是翻譯成 CPU 指令確是三行( 不信你用 javap -c 命令試試)

synchronized 是獨佔鎖/排他鎖(就是有你沒我的意思),同時只能有一個線程調用 add10KCount 方法,其他調用線程會被阻塞。所以三行 CPU 指令都是同一個線程執行完之後別的線程才能繼續執行,這就是通常說說的 原子性(線程執行多條指令不被中斷)

但 volatile 是非阻塞算法(也就是不排他),當遇到三行 CPU 指令自然就不能保證別的線程不插足了,這就是通常所說的,volatile 能保證內存可見性,但是不能保證原子性

一句話,那什麼時候才能用volatile關鍵字呢?(千萬記住了,重要事情說三遍,感覺這句話過時了)

如果寫入變量值不依賴變量當前值,那麼就可以用 volatile如果寫入變量值不依賴變量當前值,那麼就可以用 volatile如果寫入變量值不依賴變量當前值,那麼就可以用 volatile

比如上面 count++ ,是獲取-計算-寫入三步操作,也就是依賴當前值的,所以不能靠volatile 解決問題

到這裡,文章開頭第一個問題【volatile 與 synchronized 在處理哪些問題是相對等價的?】答案已經揭曉了

先自己腦補一下,如果讓你同一段時間內【寫幾行代碼】就要去【數錢】,數幾下錢就要去【唱歌】,唱完歌又要去【寫代碼】,反覆頻繁這樣操作,還要接上上一次的操作(代碼接著寫,錢累加著數,歌接著唱)還需要保證不出錯,你累不累?

synchronized 是排他的,線程排隊就要有切換,這個切換就好比上面的例子,要完成切換,還得記準線程上一次的操作,很累CPU大腦,這就是通常說的上下文切換會帶來很大開銷

volatile 就不一樣了,它是非阻塞的方式,所以在解決共享變量可見性問題的時候,volatile 就是 synchronized 的弱同步體現了

到這,文章的第二個問題【為什麼說 volatile 是 synchronized 弱同步的方式?】你也應該明白了吧

volatile 除了還能解決可見性問題,還能解決編譯優化重排序問題,之前的文章已經介紹過,請大家點擊連結自行查看就好(面試常問的雙重檢查鎖單例模式為什麼不是線程安全的也可以在裡面找到答案哦):

有序性可見性,Happens-before來搞定面試volatile關鍵字時,我們應該具備哪些談資?看完這兩篇文章,相信第三個問題也就迎刃而解了

了解了這些,相信你也就懂得如何使用了

相關焦點

  • 打工人,從 JMM 透析 volatile 與 synchronized 原理
    synchronized 如何保證並發安全?volatile 語義的內存可見性指的是什麼?這其中又跟 JMM 有什麼關係,在並發編程中 JMM 的作用是什麼,為什麼需要 JMM?與 JVM 內存結構有什麼區別?「碼哥字節」 總結出裡面的核心知識點以及面試重點,圖文並茂無畏面試與並發編程,全面提升並發編程內功!
  • 深入synchronized和volatile底層原理
    1.概述 工作中許多地方需要涉及到多線程的設計與開發,java多線程開發當中我們為了線程安全所做的任何操作其實都是為了滿足多線程的原子性、可見性、有序性三大特性為目的展開的。內存可見性:要求在多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看到,保證不會出現髒讀的。
  • 詳解鎖原理,synchronized、volatile+cas底層實現
    隨著多進程多線程的出現,對共享資源(設備,數據等)的競爭往往會導致資源的使用表現為隨機無序例如:一個線程想在控制臺輸出"I am fine",剛寫到"I am",就被另一線程搶佔控制臺輸出"naughty",導致結果是"I am naughty";對於資源的被搶佔使用,我們能怎麼辦呢?
  • 並發編程(一)| Volatile 與 Synchronized 深度解析
    請聯繫他:hack7458@outlook.com一、Volatile 關鍵字的實現及定義1.1 定義Java 程式語言允許線程訪問共享變量,為了確保共享變量能被準確和一致性的更新,線程應該確保通過排他鎖單獨獲得這個變量。Java 語言提供了 volatile,在某些情況下比鎖要更加的方便。
  • volatile你以為你真的懂?
    本文轉載自【微信公眾號:小碼逆襲,ID:gh_7c5a039380a0】經微信公眾號授權轉載,如需轉載與原文作者聯繫01—volatile是個啥這裡我們看一下百度詞條的解釋volatile的變量是說這變量可能會被意想不到地改變,這樣,編譯器就不會去假設這個變量的值了。現在像大型網際網路企業的面試,基本上volatile是必問的,當然了有時候不問是因為他覺得你應該會,現在volatile中小企業也開始會問這方面的問題了。
  • 我說:volatile 是輕量級的 synchronized,面試官讓我回去等通知
    volatile 是並發編程的重要組成部分,也是面試常被問到的問題之一。不要向小強那樣,因為一句:volatile 是輕量級的 synchronized,而與期望已久的大廠失之交臂。volatile 有兩大特性:保證內存的可見性和禁止指令重排序。那什麼是可見性和指令重排呢?接下來我們一起來看。
  • Java的synchronized 能防止指令重排序嗎?
    「面試官」:好的,我看你簡歷上寫著熟練掌握並發編程你能跟我說說並發編程裡面你都知道哪些關鍵字。「二胖:」 這不就是要考我 synchronized 和volatile 這個我擅長啊,我特意背過的,synchronized 是java提供的一個關鍵字它主要能保證原子性、有序性它的底層主要是通過Monitor來實現的。
  • Java 多線程 —— 深入理解 volatile 的原理以及應用
    推薦閱讀:《java 多線程—線程怎麼來的》這一篇主要講解一下volatile的原理以及應用,想必看完這一篇之後,你會對volatile的應用原理以及使用邊界會有更深刻的認知。本篇主要內容:volatile 讀寫同步原理volatile重排序原則volatile應用關鍵字volatile是jvm提供的輕量級的同步機制,但它並不容易理解,而且在多數情況下用不到,被多數開發者拋棄並採用synchronized代替,synchronized屬於重度鎖,如果你對性能有高的要求,那麼同等情況下,變量聲明volatile會減小更少的同步開銷
  • 面試官最愛的 volatile 關鍵字,這些問題你都搞懂了沒?
    volatile關鍵字基本介紹volatile可以看成是synchronized的一種輕量級的實現,但volatile並不能完全代替synchronized,volatile有synchronized可見性的特性,但沒有synchronized原子性的特性。
  • 1分鐘讀懂java中的volatile關鍵字
    在Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程並發執行的正確性。(本文不對指令重排作介紹,但不代表它不重要,它是理解JAVA並發原理時非常重要的一個概念)。
  • 可惜了,面試敗在了volatile關鍵字上,直擊痛點搞懂volatile
    i = 0;public synchronized int getI(){return i;}public synchronized void setI(int j){this.i = j;}public void
  • JAVA中synchronized與static synchronized 的區別
    1.synchronized與static synchronized 的區別        synchronized
  • Java並發編程徹底搞懂volatile關鍵字
    背景Java線程控制中常用的兩個關鍵字:synchronized、volatile因上篇文章《程式設計師眼中的Synchronized同步鎖》對synchronized說明:在多線程環境中,use和assign是多次出現的,但這一操作並不是原子性,也就是read和load之後,如果主內存count變量發生修改之後,線程工作內存中的值由於已經加載,不會產生對應的變化,也就是私有內存和公共內存中的變量不同步,所以計算時結果和預期不一致,也就出現線程安全問題。
  • 如何理解volatile關鍵字
    面試中常常會問道,說說你對volatile的理解?在講解之前,我們先來了解一個Java並發編程中可見性的問題。可以看出線程1和線程2都是在操作變量data,但是線程1修改了data的值之後,線程2是看不到的,它只能看到自己工作內存中的那個副本值,這個就是Java並發過程中的可見性問題。
  • Java程式設計師面試必備:Volatile全方位解析
    「指令重排序」了解一下,指令重排是指在程序執行過程中,「為了提高性能」, 「編譯器和CPU可能會對指令進行重新排序」。CPU重排序包括指令並行重排序和內存系統重排序,重排序類型和重排序執行過程如下:實際上,可以給flag加上volatile關鍵字,來保證有序性。當然,也可以通過synchronized和Lock來保證有序性。
  • 知名公司面試題:談談你對volatile關鍵字的理解
    作為一名java程式設計師,求職面試時,關於volatile關鍵字時常會遇到。張工最近到某知名網際網路公司面試,面試官提出這樣的一個問題:談談你對volatile關鍵字的理解張工一時間沒有回答上來,面試官:你都工作三年了,怎麼對volatile關鍵字都沒掌握啊。
  • Java面試總是問StringBuffer,StringBuilder區別是啥?檔次為什麼這麼低?
    StringBuffer,StringBuilder區別是啥?有了synchronized,還要volatile幹什麼?synchronized的鎖優化是怎麼回事?(鎖粗化?鎖消除?自旋鎖?偏向鎖?輕量級鎖?)那什麼是fail-fast?什麼是fail-safe?資料庫鎖有了解麼?行級鎖?表級鎖?共享鎖?排他鎖?gap鎖?next-key lock?什麼是聚簇索引?非聚簇索引?最左前綴是什麼?
  • 從零開始了解多線程知識之開始篇目——jvm&volatile
    本文章主要介紹到到了一些CPU緩存一致性協議的基礎知識,由此引出的多線程知識,同時談到了多線程中數據操作 原子性 可見性 有序性 的問題從線程的基本概念到多線程下工作的數據安全問題,主要談到了java知識中volatile關鍵字,使用實例的模式講解了 volatile可見性,有序性,指令重排的問題 接下來大家一起來學習學習吧
  • 國內Java面試總是問StringBuffer,StringBuilder區別是啥?檔次為什麼這麼低?
    StringBuffer,StringBuilder區別是啥?有了synchronized,還要volatile幹什麼?synchronized的鎖優化是怎麼回事?(鎖粗化?鎖消除?自旋鎖?偏向鎖?輕量級鎖?)那什麼是fail-fast?什麼是fail-safe?資料庫鎖有了解麼?行級鎖?表級鎖?共享鎖?排他鎖?gap鎖?next-key lock?什麼是聚簇索引?非聚簇索引?最左前綴是什麼?
  • Java中volatile關鍵字概覽
    一、第一章 volatile關鍵字概覽多線程下變量的不可見性概述在多線程並發執行下,多個線程修改共享的成員變量,會出現一個線程修改了共享變量的值後,另一個線程不能直接 看到該線程修改後的變量的最新值。變量不可見性內存語義概述在介紹多線程並發修改變量不可見現象的原因之前,我們需要了解回顧一下Java內存模型(和Java並發編程有關的模 型):JMM。