1. 程式人生 > >java原子操作實現原理

java原子操作實現原理

在瞭解java原子操作之前我們需要先了解併發程式設計,java記憶體模型,volatile以及CAS演算法。

JMM定義了Java 虛擬機器(JVM)在計算機記憶體(RAM)中的工作方式。JVM是整個計算機虛擬模型,所以JMM是隸屬於JVM的。Java記憶體模型定義了多執行緒之間共享變數的可見性以及如何在需要的時候對共享變數進行同步。

併發程式設計

    在共享記憶體的併發模型裡,執行緒之間共享程式的公共狀態,執行緒之間通過寫-讀記憶體中的公共狀態來隱式進行通訊,典型的共享記憶體通訊方式就是通過共享物件進行通訊。

    在訊息傳遞的併發模型裡,執行緒之間沒有公共狀態,執行緒之間必須通過明確的傳送訊息來顯式進行通訊,在java中典型的訊息傳遞方式就是wait()和notify()。

關於Java執行緒之間的通訊,可以參考執行緒之間的通訊(thread signal)。

執行緒之間的同步

同步是指程式用於控制不同執行緒之間操作發生相對順序的機制。

在共享記憶體併發模型裡,同步是顯式進行的。程式設計師必須顯式指定某個方法或某段程式碼需要線上程之間互斥執行。

在訊息傳遞的併發模型裡,由於訊息的傳送必須在訊息的接收之前,因此同步是隱式進行的。Java的併發採用的是共享記憶體模型。

    Java執行緒之間的通訊總是隱式進行,整個通訊過程對程式設計師完全透明。如果編寫多執行緒程式的Java程式設計師不理解隱式進行的執行緒之間通訊的工作機制,很可能會遇到各種奇怪的記憶體可見性問題。

java記憶體模型(JMM)

    上面講到了Java執行緒之間的通訊採用的是過共享記憶體模型,這裡提到的共享記憶體模型指的就是Java記憶體模型(簡稱JMM),JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體(main memory)中,每個執行緒都有一個私有的本地記憶體(local memory),本地記憶體中儲存了該執行緒以讀/寫共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了快取,寫緩衝區,暫存器以及其他的硬體和編譯器優化。

這裡寫圖片描述

從上圖來看,執行緒A與執行緒B之間如要通訊的話,必須要經歷下面2個步驟:

  1. 首先,執行緒A把本地記憶體A中更新過的共享變數重新整理到主記憶體中去。
  2. 然後,執行緒B到主記憶體中去讀取執行緒A之前已更新過的共享變數。

下面通過示意圖來說明這兩個步驟:
這裡寫圖片描述

    如上圖所示,本地記憶體A和B有主記憶體中共享變數x的副本。假設初始時,這三個記憶體中的x值都為0。執行緒A在執行時,把更新後的x值(假設值為1)臨時存放在自己的本地記憶體A中。當執行緒A和執行緒B需要通訊時,執行緒A首先會把自己本地記憶體中修改後的x值重新整理到主記憶體中,此時主記憶體中的x值變為了1。隨後,執行緒B到主記憶體中去讀取執行緒A更新後的x值,此時執行緒B的本地記憶體的x值也變為了1。

    從整體來看,這兩個步驟實質上是執行緒A在向執行緒B傳送訊息,而且這個通訊過程必須要經過主記憶體。JMM通過控制主記憶體與每個執行緒的本地記憶體之間的互動,來為java程式設計師提供記憶體可見性保證。

volatile的作用

    在java記憶體模型中,我們可以看到jvm必須要解決可見性和同步機制才能計算正確,否則當多執行緒併發的時候,不能得到預期的結果。volatile關鍵字就是為了解決可見性。

    可見性是指當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。

     對於可見性,Java提供了volatile關鍵字來保證可見性。 當一個共享變數被volatile修飾時,它會保證修改的值會立即被更新到主存,當有其他執行緒需要讀取時,它會去記憶體中讀取新值。 而普通的共享變數不能保證可見性,因為普通共享變數被修改之後,什麼時候被寫入主存是不確定的,當其他執行緒去讀取時,此時記憶體中可能還是原來的舊值,因此無法保證可見性。 

     另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時刻只有一個執行緒獲取鎖然後執行同步程式碼,並且在釋放鎖之前會將對變數的修改重新整理到主存當中。因此可以保證可見性。

原子操作

處理器如何實現原子操作

    32位IA-32處理器使用基於對快取加鎖或匯流排加鎖的方式來實現多處理器之間的原子操作。首先處理器會自動保證基本的記憶體操作的原子性。處理器保證從系統記憶體中讀取或者寫入一個位元組是原子的,意思是當一個處理器讀取一個位元組時,其他處理器不能訪問這個位元組的記憶體地址。Pentium 6和最新的處理器能自動保證單處理器對同一個快取行裡進行16/32/64位的操作是原子的,但是複雜的記憶體操作處理器是不能自動保證其原子性的,比如跨匯流排寬度、跨多個快取行和跨頁表的訪問。但是,處理器提供匯流排鎖定和快取鎖定兩個機制來保證複雜記憶體操作的原子性。

    第一個機制是通過匯流排鎖保證原子性。如果多個處理器同時對共享變數進行讀改寫操作(i++就是經典的讀改寫操作),那麼共享變數就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之後共享變數的值會和期望的不一致。

    處理器使用匯流排鎖就是來解決這個問題的。所謂匯流排鎖就是使用處理器提供的一個LOCK#訊號,當一個處理器在總線上輸出此訊號時,其他處理器的請求將被阻塞住,那麼該處理器可以獨佔共享記憶體。

    第二個機制是通過快取鎖定來保證原子性。在同一時刻,我們只需保證對某個記憶體地址的操作是原子性即可,但匯流排鎖定把CPU和記憶體之間的通訊鎖住了,這使得鎖定期間,其他處理器不能操作其他記憶體地址的資料,所以匯流排鎖定的開銷比較大,目前處理器在某些場合下使用快取鎖定代替匯流排鎖定來進行優化。頻繁使用的記憶體會快取在處理器的L1、L2和L3快取記憶體裡,那麼原子操作就可以直接在處理器內部快取中進行,並不需要宣告匯流排鎖,在Pentium 6和目前的處理器中可以使用“快取鎖定”的方式來實現複雜的原子性。所謂“快取鎖定”是指記憶體區域如果被快取在處理器的快取行中,並且在Lock操作期間被鎖定,那麼當它執行鎖操作回寫到記憶體時,處理器不在總線上聲言LOCK#訊號,而是修改內部的記憶體地址,並允許它的快取一致性機制來保證操作的原子性,因為快取一致性機制會阻止同時修改由兩個以上處理器快取的記憶體區域資料,當其他處理器回寫已被鎖定的快取行的資料時,會使快取行無效,在如圖2-3所示的例子中,當CPU1修改快取行中的i時使用了快取鎖定,那麼CPU2就不能同時快取i的快取行。

但是有兩種情況下處理器不會使用快取鎖定。
第一種情況是:當操作的資料不能被快取在處理器內部,或操作的資料跨多個快取行(cache line)時,則處理器會呼叫匯流排鎖定。
第二種情況是:有些處理器不支援快取鎖定。對於Intel 486和Pentium處理器,就算鎖定的記憶體區域在處理器的快取行中也會呼叫匯流排鎖定。
針對以上兩個機制,我們通過Intel處理器提供了很多Lock字首的指令來實現。例如,位測試和修改指令:BTS、BTR、BTC;交換指令XADD、CMPXCHG,以及其他一些運算元和邏輯指令(如ADD、OR)等,被這些指令操作的記憶體區域就會加鎖,導致其他處理器不能同時訪問它。

Java如何實現原子操作

使用迴圈CAS實現原子操作
JVM中的CAS操作正是利用了處理器提供的CMPXCHG指令實現的。CAS操作使用計算機原語實現。自旋CAS實現的基本思路就是迴圈進行CAS操作直到成功為止,以下程式碼實現了一個基於CAS執行緒安全的計數器方法safeCount和一個非執行緒安全的計數器count。

public class Counter {
    private AtomicInteger atomicI = new AtomicInteger(0);
    private int i = 0;

    public void main(String[] args) {
      final Counter cas = new Counter();
      List<Thread> ts = new ArrayList<Thread>(600);
      long start = System.currentTimeMillis();
      for (int j = 0; j < 100; j++) {
        Thread t = new Thread(new Runnable() {
          @Override
          public void run() {
            for (int i = 0; i < 10000; i++) {
              cas.count();
              cas.safeCount();
            }
          }
        });
        ts.add(t);
      }
      for (Thread t : ts) {
        t.start();
      }
      // 等待所有執行緒執行完成
      for (Thread t : ts) {
        try {
          t.join();
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
      System.out.println(cas.i);
      System.out.println(cas.atomicI.get());
      System.out.println(System.currentTimeMillis() - start);
    }

    /**
     * 使用CAS實現執行緒安全計數器
     */
    private void safeCount() {
      for (;;) {
        int i = atomicI.get();
        boolean suc = atomicI.compareAndSet(i, ++i);
        if (suc) {
          break;
        }
      }
    }

    /**
     * 非執行緒安全計數器
     */
    private void count() {
      i++;
    }
  }

CAS實現原子操作的三大問題

在Java併發包中有一些併發框架也使用了自旋CAS的方式來實現原子操作,比如LinkedTransferQueue類的Xfer方法。CAS雖然很高效地解決了原子操作,但是CAS仍然存在三大問題。ABA問題,迴圈時間長開銷大,以及只能保證一個共享變數的原子操作。

1)ABA問題。因為CAS需要在操作值的時候,檢查值有沒有發生變化,如果沒有發生變化則更新,但是如果一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。ABA問題的解決思路就是使用版本號。在變數前面追加上版本號,每次變數更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A。從Java 1.5開始,JDK的Atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法的作用是首先檢查當前引用是否等於預期引用,並且檢查當前標誌是否等於期標誌,如果全部相等,則以原子方式將該引用和該標誌的值設定為給定的更新值。

2)迴圈時間長開銷大。
自旋CAS如果長時間不成功,會給CPU帶來非常大的執行開銷。如果JVM能支援處理器提供的pause指令,那麼效率會有一定的提升。pause指令有兩個作用:第一,它可以延遲流水線執行指令(de-pipeline),使CPU不會消耗過多的執行資源,延遲的時間取決於具體實現的版本,在一些處理器上延遲時間是零;第二,它可以避免在退出迴圈的時候因記憶體順序衝突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU的執行效率。

3)只能保證一個共享變數的原子操作。
當對一個共享變數執行操作時,我們可以使用迴圈CAS的方式來保證原子操作,但是對多個共享變數操作時,迴圈CAS就無法保證操作的原子性,這個時候就可以用鎖。還有一個取巧的辦法,就是把多個共享變數合併成一個共享變數來操作。比如,有兩個共享變數i=2,j=a,合併一下ij=2a,然後用CAS來操作ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用物件之間的原子性,就可以把多個變數放在一個物件裡來進行CAS操作。

相關推薦

java原子操作實現原理

在瞭解java原子操作之前我們需要先了解併發程式設計,java記憶體模型,volatile以及CAS演算法。 JMM定義了Java 虛擬機器(JVM)在計算機記憶體(RAM)中的工作方式。JVM是整個計算機虛擬模型,所以JMM是隸屬於JVM的。Java記憶體模

Java原子實現原理分析

upd hat 16px 檢查 () 過程 jvm api 處理 並發包中的原子類可以解決類似num++這樣的復合類操作的原子性問題,相比鎖機制,使用原子類更精巧輕量,性能開銷更小,下面就一起來分析下原子類的實現機理。 悲觀的解決方案(阻塞同步)   我們知道,num++看

java 原子操作實現原理(1)

處理器如何實現原子操作 (1)使用匯流排鎖保證原子性 第一個機制是通過匯流排鎖保證原子性。如果多個處理器同時對共享變數進行讀改寫操作(i++就是經典的讀改寫操作),那麼共享變數就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之後共享變數的值會和期望的不一致

java 原子操作實現原理(2)

Java中可以通過鎖和迴圈CAS的方式來實現原子操作。 這一節只講使用迴圈CAS實現原子操作: (1)JVM中的CAS操作正是利用了處理器提供的CMPXCHG指令實現的。自旋CAS實現的基本思路就是迴圈進行CAS操作直到成功為止。下面是通過CAS執行緒安全的計數器方法

java集合包總結(新增、刪除等操作實現原理)

1.集合包 常用的是Collection與Map兩個介面的實現類。 Collection常用的兩種介面:List和Set。 List實現類:ArrayList、LinkedList、Vector、Stack。 Set實現類:HashSet、TreeSet。 Collect

CPU實現原子操作原理

586之前的CPU, 會通過LOCK鎖匯流排的形式來實現原子操作. 686開始則提供了儲存一致性(Cache coherence),  這是多處理的基礎, 也是原子操作的基礎.   1. 儲存的粒度 儲存的組織形式(粒度)是以CacheLine為單位的, 通常為64位元組甚至更高(早期也有

1.Java集合-HashMap實現原理及源碼分析

int -1 詳細 鏈接 理解 dac hash函數 順序存儲結構 對象儲存   哈希表(Hash Table)也叫散列表,是一種非常重要的數據結構,應用場景及其豐富,許多緩存技術(比如memcached)的核心其實就是在內存中維護一張大的哈希表,而HashMap的實

JAVA泛型實現原理

get tcl ret jdk1.5 use select ace 代碼 特定 1. Java範型時編譯時技術,在運行時不包含範型信息,僅僅Class的實例中包含了類型參數的定義信息。泛型是通過java編譯器的稱為擦除(erasure)的前端處理來實現的。你可以(基本上就是

java nio的實現原理

阻塞 3.3 keys div nbsp select 行數 多路復用 class 1 什麽是java nio java nio就是java非阻塞io。 2 什麽是channel channel是到打開的文件的連接,只要是支持讀寫操作的實體都可以稱為文件,文件可以是硬件設備

通過圖文給你講明白java GC的實現原理

本文原連結 http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html 1. JAVA GC 概述 JAVA GC採用了分代思想,將java堆分成新生代,年老代,永久代。

JAVA 原子操作

上文中,guava程式碼中就用到了,在這裡再專門捋一下 部分內容源自: https://www.jianshu.com/p/712681f5aecd https://www.yiibai.com/java_concurrency/concurrency_atomiclong.html Atomi

Java LinkedList的實現原理詳解

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

Java HashSet的實現原理詳解

分享一下我老師大神的人工智慧教程!零基礎,通俗易懂!http://blog.csdn.net/jiangjunshow 也歡迎大家轉載本篇文章。分享知識,造福人民,實現我們中華民族偉大復興!        

深入理解java註解的實現原理(轉載)

轉自:深入理解java註解的實現原理 今天將從以下4個方面來系統的學習一下java註解 什麼是註解 註解的用途 註解使用演示 註解的實現原理 1,什麼是註解 註解也叫元資料,例如我們常見的@Override和@Deprecated,註解是J

三十五、JAVA泛型實現原理

1. 概述 泛型在java中有很重要的地位,在面向物件程式設計及各種設計模式中有非常廣泛的應用。 什麼是泛型?為什麼要使用泛型? 泛型,即“引數化型別”。一提到引數,最熟悉的就是定義方法時有形參,然後呼叫此方法時傳遞實參。那麼引數化型別怎麼理解呢?顧名思義,就是將型別由原來的具體

Java的跨平臺實現原理(Write Once,Run Anywhere)

Java的跨平臺實現原理 為什麼要跨平臺 在不同點作業系統之間,使用不同的指令集對計算機進行控制。如果沒有跨平臺,我們需要對window,Linux,unix等作業系統的指令集分別進行特定的語言開發 Java如何實現 在不同的作業系統之間,提供不同的虛擬機器,讓虛擬機器實

Java併發程式設計(二)——Java併發底層實現原理

Java程式碼會被編譯後變成Java位元組碼,位元組碼會被類載入器載入到JVM中,JVM執行位元組碼,最終轉化成彙編指令在CPU上執行,Java中所使用的併發機制依賴於JVM的實現和CPU的指令。 volatile 在多執行緒併發程式設計中,synchronized和volatile

根據時間戳轉Date實現Java)及實現原理分析

時間戳是指格林威治時間1970年01月01日00時00分00秒(北京時間1970年01月01日08時00分00秒)起至現在的總秒數。 本次實現跟根據Java.text.* 包中的工具類實現的,示例程式碼: import java.text.SimpleDateFormat; public

Java基礎--forEach實現原理

針對list的forEach // 原始碼 import java.util.ArrayList; import java.util.List; public class ListTest { public static void main(String

JAVA架構-SpringMVC實現原理及解析

                               1、Spring mvc介紹 SpringMVC框架是以請求為驅動,圍繞Servlet設計,將請求發給控制器,然後通過模型物件,分派器來展示請求結果檢視。其中核心類是DispatcherServlet,它是一個S