日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

原子性、可见性、有序性解决方案

發布時間:2024/3/12 编程问答 45 豆豆
生活随笔 收集整理的這篇文章主要介紹了 原子性、可见性、有序性解决方案 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

原子性、可見性、有序性解決方案

(一)原子性

原子性是指:一個或多個操作,要么全部執行且在執行過程中不被任何因素打斷,要么全部不執行。在Java中當我們討論一個操作具有原子性問題是一般就是指這個操作會被線程的隨機調度打斷。

JMM對原子性的保證大概分以下幾種類型:java自帶原子性、synchronized、Lock鎖、原子操作類(CAS)。下面我們來一個一個細說。

1. java自帶原子性

在Java中,對基本數據類型的變量的讀取和賦值操作是原子性操作,但是long和double類型是64位,在32位JVM中會將64位數據的讀寫操作分成兩次32位來處理,所以long和double在32位JVM中是非原子操作,也就是說在并發訪問時是非線程安全的。

尤其要注意,這里我們講的僅僅是讀取和賦值兩種操作具有原子性。這里的賦值僅僅指具體數值的賦值,而不包括變量給變量賦值。另外,組合操作(例如++和–操作)也同樣不具有原子性。我們可以看幾個經典例子:

a = true; // 原子性 a = 5; // 原子性 a = b; // 非原子性,分兩步完成,第一步加載b的值,第二步將b賦值給a a = b + 2; // 非原子性,分三步完成 a++; // 非原子性,分三步完成

了解匯編語言的朋友很容易理解下面三個例子為什么不能一步完成。不了解匯編語言的朋友記住即可,這個地方非常關鍵。

2. synchronized

synchronized可以保證操作結果的原子性(注意這里的描述)。synchronized保證原子性的原理也很簡單,因為synchronized可以防止多個線程并發執行同一段代碼。

方法加了synchronized后,當一個線程沒執行完這個方法前,其他線程是不能執行這段代碼的。其實我們發現synchronized并不能將代碼變成原子性操作,代碼在執行過程中還是有可能被中斷的,但是,即使被中斷了其他線程也不能乘機突然進入臨界區執行這段代碼,當之前被中斷的線程繼續執行直到結束時得到的結果還是正確的。

因此,synchronized對原子性問題的保證是從最終結果上來保證的,也就是說它只保證最終的結果正確,中間操作的是否被打斷沒法保證。

3. Lock鎖

Lock鎖的原理與synchronized基本一致,因此不再贅述。

4. 原子操作類(CAS)

JDK提供了很多原子操作類來保證操作的原子性。原子操作類的底層是使用CAS機制的,這個機制對原子性的保證和synchronized有本質的區別。CAS機制保證了整個賦值操作是原子的不能被打斷的,而synchronized只能保證代碼最后執行結果的正確性,也就是說synchronized能消除原子性問題對代碼最后執行結果的影響,但原子操作類(CAS)是真正保證了操作的原子性。

(二)可見性

Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程在工作內存中保存的值是主內存中值的副本,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存,等到線程對變量操作完畢之后會將變量的最新值刷新回到主內存。

但是何時刷新這個最新值又是隨機的。所以就有可能一個線程已經將一個共享變量更新了,但是還沒刷新回主內存,那么這時其他對這個變量進行讀寫的線程就看不到這個最新值。還有一種可能就是雖然修改線程已經將最新值刷新到主內存中去了,但是讀線程的工作內存中副本的緩存值還沒過期,那么讀線程還是會使用這個副本值,而不是主內存中的最新值。這個就是多CPU多線程編程環境下的可見性問題。

JMM針對可見性問題提出了下面幾種解決方案:volatile、synchronized、Lock鎖、原子操作類(CAS),下面一個個細說。

1. volatile

我們可以看一下volatile究竟做了什么。使用volatile修飾一個共享變量可以達到如下的效果(內存語義):

  • 一旦線程對這個共享變量的副本做了修改,會立馬刷新最新值到主內存中去
  • 一旦線程對這個共享變量的副本做了修改,其他線程中對這個共享變量拷貝的副本值會失效,其他線程如果需要對這個共享變量進行讀寫,必須重新從主內存中加載
  • volatile底層使用的是內存屏障來保證可見性的。我們先來了解一下內存屏障。

    內存屏障(英語:Memory barrier),也稱內存柵欄,內存柵障,屏障指令等,是一類同步屏障指令,是CPU或編譯器在對內存隨機訪問的操作中的一個同步點,使得此點之前的所有讀寫操作都執行后才可以開始執行此點之后的操作。大多數現代計算機為了提高性能而采取亂序執行,這使得內存屏障成為必須。語義上,內存屏障之前的所有寫操作都要寫入內存;內存屏障之后的讀操作都可以獲得同步屏障之前的寫操作的結果。因此,對于敏感的程序塊,在寫操作之后、讀操作之前可以插入內存屏障。

    我們可以從上面一大段定義中抽出兩條要點來對應volatile的內存語義:內存屏障之前的寫操作都必須立馬刷新回主內存、內存屏障之后的讀操作都必須從主內存中讀取最新值。這也就是volatile保證可見性的基本原理。

    2. synchronized

    當線程釋放鎖時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存中。當線程獲取鎖時,JMM會把該線程對應的本地內存置為無效,從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。這樣就保證了可見性。

    從這里我們發現,實際上鎖具有和volatile一致的內存語義,所以使用synchronized也可以實現共享變量的可見性。

    3. Lock鎖

    Lock鎖的原理與synchronized基本一致,因此不再贅述。

    4. 原子操作類(CAS)

    使用原子操作類也可以保證共享變量操作的可見性。原子操作類底層使用的是CAS機制。Java中CAS機制每次都會從主內存中獲取最新值進行compare,比較一致之后才會將新值set到主內存中去。而且這個整個操作是一個原子操作。所以CAS操作每次拿到的都是主內存中的最新值,每次set的值也會立即寫到主內存中。

    (三)有序性

    為什么會出現有序性問題,其根源就是指令重排。指令重排是指編譯器和處理器在不影響代碼單線程執行結果的前提下,對源代碼的指令進行重新排序執行。這種重排序執行是一種優化手段,目的是為了處理器內部的運算單元能盡量被充分利用,提升程序的整體運行效率。

    重排序分為以下幾種:

  • 編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。
  • 指令級并行的重排序。現代處理器采用了指令級并行技術來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序。
  • 內存系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
  • 處理器為了提升程序的性能,可以對程序進行重排序。但是必須滿足重排序之后的代碼在單線程環境下執行的結果不能改變(很關鍵),這個原則也就是我們常說的as-if-serial語義。為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關系的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關系,這些操作就可能被編譯器和處理器重排序。

    但是,as-if-serial只能保證單線程情況下結果保持不變,在多線程情況下就無法保證。因此,多線程下的有序性問題可能會導致最終的結果發生無法預測的變化,這是一個非常嚴重的問題。

    因此,JMM使用了四種方式來確保有序性:happens-before原則、synchronized、Lock鎖、volatile,下面我們一一細說:

    1. happens-before原則

    我們先來看一下《java并發編程的藝術》中的定義:

    在JMM中,如果一個操作執行的結果需要對另一個操作可見,那么這兩個操作之間必須要存在happens- before關系。這里提到的兩個操作既可以是在一個線程之內,也可以是在不同線程之間。兩個操作之間具有happens-before關系,并不意味著前一個操作必須要在后一個操作之前執行!happens-before僅僅要求前一個操作(執行的結果)對后一個操作可見,且前一個操作按順序排在第二個操作之前(the f irst is visible toand ordered before the second)

    我們尤其要注意:如果a happen-before b,則a所做的任何操作對b是可見的。這一點大家務必要記住,因為happen-before這個詞很容易被誤解為是時間的前后。

    下面是Java內存模型一些自帶的先行發生關系(摘自《java并發編程的藝術》)這些先行發生關系無須任何同步器協助就已經存在,可以在編碼中直接使用。 如果兩個操作之間的關系不在此列,并且無法從下列規則間接推導出來的話,它們就沒有有序性保障,虛擬機可以對它們隨意地進行重排序:

  • 程序次序規則(Program Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生于書寫在后面的操作。 準確地說,應該是控制流順序而不是程序代碼順序,因為要考慮分支、 循環等結構。
  • 監視器鎖規則(Monitor Lock Rule):一個unlock操作先行發生于后面對同一個鎖的lock操作。 這里必須強調的是同一個鎖,而“后面”是指時間上的先后順序。
  • volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生于后面對這個變量的讀操作,這里的“后面”同樣是指時間上的先后順序。
  • 線程啟動規則(Thread Start Rule):Thread對象的start()方法先行發生于此線程的每一個動作。
  • 線程終止規則(Thread Termination Rule):線程中的所有操作都先行發生于對此線程的終止檢測,我們可以通過Thread.join()方法結束、 Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
  • 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生,可以通過Thread.interrupted()方法檢測到是否有中斷發生。
  • 對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生于它的finalize()方法的開始。
  • 傳遞性(Transitivity):如果操作A先行發生于操作B,操作B先行發生于操作C,那就可以得出操作A先行發生于操作C的結論。
  • 如果不能滿足happens-before原則,就需要使用synchronized機制或volatile機制機制來保證有序性。

    2. synchronized

    synchronized保證有序性的方法非常簡單粗暴,但是這也就帶來了更大的資源浪費。synchronized語義表示鎖在同一時刻只能由一個線程進行獲取,當鎖被占用后,其他線程只能等待。因此,synchronized語義就要求線程在訪問讀寫共享變量時只能“串行”執行,因此synchronized具有有序性。

    3. Lock鎖

    Lock鎖的原理與synchronized基本一致,因此不再贅述。

    4. volatile

    volatile的底層是使用內存屏障來保證有序性的。若用volatile修飾共享變量,在JVM底層volatile是采用“內存屏障”來實現禁止特定類型的處理器重排序。加入volatile關鍵字時,會多出一個lock前綴指令,lock前綴指令實際上相當于一個內存屏障(也成內存柵欄)。內存屏障可以保證:

  • 當第二個操作是volatile寫時,不管第一個操作是什么,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之后。
  • 當第一個操作是volatile讀時,不管第二個操作是什么,都不能重排序。這個規則確保volatile讀之后的操作不會被編譯器重排序到volatile讀之前。
  • 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序。
  • (四)總結


    我們把上述要點總結一下,實際上主要就是三個關鍵點:volatile無法保證原子性、synchronized只能保證原子性問題不影響最終結果但無法真正保證原子性、原子類無法保證有序性。

    2020年7月11日

    總結

    以上是生活随笔為你收集整理的原子性、可见性、有序性解决方案的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。