java併發機制的底層實現原理

  java代碼在編譯後變成java字節碼,字節碼被類加載器加載到jvm裏,jvm執行字節碼,最終須要轉化爲彙編指令在cpu上執行,java中所使用的併發機制依賴於jvm的實現和cpu指令。java

二、1 volatile的應用編程

  在多線程併發編程中,synchronized和volatile都扮演着重要的角色,volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的"可見性",可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀取到這個修改的值。若是volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,覺得它不會引發線程上下文的切換和調度。本文將深刻分析在硬件層面上的intel處理器是如何實現volatile的,經過深刻分析幫助咱們正確的使用volatile變量。
緩存

一、volatile實現原理安全

 咱們在x86處理器下經過工具獲取JIT編譯器生成的彙編指令來查看對volatile進行寫操做時,cpu會作什麼事情。java代碼以下:
多線程

    instance = new Singleton();    //instance是volatile變量
架構

轉變爲彙編代碼以下:併發

    0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24:lock $0x0,(%esp)
框架

有volatile修飾的共享變量進行寫操做的時候會多出第二行彙編代碼,經過查IA-32架構軟件開發者手冊可知,lock前綴的執行在多核處理器下會引起兩件事情。jvm

1)、將當前處理器緩存行的數據寫會系統內存ide

2)、這個寫回內存的操做會使在其餘cpu裏緩存了該內存地址的數據無效

爲了提升處理速度,處理器不直接和內存進行通訊,而是先將系統內存的數據讀取到內部緩存(L一、L2或其餘)後再進行操做,但操做完不知道什麼時候會寫到內存。若是對聲明瞭volatile的變量進行寫操做,jvm就會向處理器發送一條lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。可是就算寫回到內存,若是其餘處理器緩存的值仍是舊的,在執行計算操做就會有問題,因此,在多處理器下,爲了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每一個處理器經過嗅探在總線上傳播的數據來檢查本身緩存的值是否是過時了,當處理器發現本身緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操做的時候,會從新從系統內存中把數據讀處處理器緩存中。

  下面具體講解volatile的兩條實現原則。

1)、lock前綴指令會引發處理器緩存寫回內存。

  lock前綴指令致使在執行指令期間,聲言處理器的LOCK#信號。在多處理器環境中,LOCK#信號確保在聲言該信號期間,處理器能夠獨佔任何共享內存。(由於它會鎖住總線,致使其餘cpu不能訪問總線,不能訪問總線就覺得着不能訪問系統內存)。可是在最近的處理器裏,LOCK#信號通常不鎖總線,而是鎖緩存,畢竟鎖總線開銷比較大,目前的處理器中,若是訪問的內存區域已經緩存在處理器內部,則不會聲言LOCK#信號。相反,它會鎖定這塊內存區域的緩存並寫回到內存,並使用緩存一致性機制來確保修改的原子性,此操做被稱爲"緩存鎖定",緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據。

2)、一個處理器的緩存寫回到內存會致使其餘處理器的緩存無效。

  IA-32處理器和Intel64處理器使用MESI(修改、獨佔、共享、無效)控制協議去維護內部緩存和其餘處理器緩存的一致性。在多核處理器系統中進行操做的時候,IA-32和Intel64處理器能嗅探其餘處理器訪問系統內存和它們的內部緩存。處理器使用嗅探基數保證它的內部緩存、系統內存和其餘處理器的緩存的數據在總線上保持一致。如在Pentium和P6 family處理器中,若是經過嗅探一個處理器來監測其餘處理器打算寫回內存地址,而這個地址當前處於共享狀態,那麼正在嗅探的處理器將使它的緩存行無效,在下次訪問相同內存地址時,強制執行緩存行填充。

二、2 synchronized的實現原理和應用

  在多線程併發編程中synchronized一直是元老級角色。

先來看看利用synchronized實現同步的基礎:java中的每個對象均可以做爲鎖,具體表現爲如下3種形式。

1)、對於普通同步方法,鎖是當前實例對象

2)、對於靜態同步方法,鎖是當前類的Class對象

3)、對於同步方法塊,鎖是Synchonized括號裏配置的 對象

  當一個線程試圖訪問同步代碼塊時,它首先必須獲得鎖,退出或者拋出異常時必須釋放鎖,那麼鎖到底存在哪裏呢?所裏面會存儲哪些信息呢?

 從jvm規範中能夠看到Synchonized在jvm裏的實現原理,jvm基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但二者的實現細節不同。代碼塊同步是使用monitorenter和monitorexit指令實現的,而方法同步是使用另一種方式實現的,細節在jvm規範裏沒有詳細說明。可是,方法的同步一樣可使用者兩個指令來實現。

 monitorenter指令是在編譯後插入到同步代碼塊的開始位置,而monitorexit指令是插入到方法結束和異常處,jvm保證每一個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,而且一個monitor被持有後,它處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的全部權,即嘗試得到對象的鎖。


 synchronized用的鎖是存在java對象頭裏的。


二、3 原子操做的實現原理

  原子(atomic)本意是"不能被進一步分割的最小粒子",而原子操做意爲"不可能被中斷的一個或一系列操做",在處理器上實現原子操做就變得有些複雜。

一、處理器如何實現原子操做

  a)、使用總線鎖保證原子性

  b)、使用緩鎖保證原子性

二、java如何實現原子操做

  在java中能夠經過鎖和循環CAS的方式來實現原子操做

  a)、使用循環CAS實現原子操做

    jvm中的CAS操做正是利用了處理器提供的CMPXCHG指令實現的,循環CAS實現的基本思路就是循環進行CAS操做直到成功爲止,一下代碼實現了一個基於CAS線程安全的計數器方法safeCount和一個非線程安全的計數器count。

/**
 * 計數器
 */
public class Counter {

    private AtomicInteger atomicI = new AtomicInteger(0);
    private int i = 0;

    public static 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++;
    }

}

java1.5開始,jdk的併發包中提供了一些支持原子操做的類。 

 b)、CAS實現原子操做的三大問題

在java併發包中有一些併發框架也使用了循環CAS的方式來實現原子操做,CAS雖然很高效的解決了原子操做,可是CAS仍然存在三大問題。ABA問題,循環時間長開銷大,以及只能保證一個共享變量的原子操做。

ABA問題:使用AtomicStampedReference來解決。

循環時間長開銷大問題

只能保證一個共享變量的原子操做:能夠把多個共享變量合併成一個共享變量來操做。

同時JDK提供AtomicReference類來保證引用對象之間的原子性,就能夠把多個變量放在一個對象裏來進行CAS操做。

 c)、使用鎖機制來實現原子操做

   鎖機制保證了只有得到鎖的線程纔可以操做鎖定的內存區域。jvm內部實現了不少種鎖機制,有偏向鎖、輕量級鎖和互斥鎖。除了偏向鎖,jvm實現鎖的方式都用了循環CAS,即當一個線程想進入同步塊的時候使用循環CAS的方式獲取鎖,當它退出同步塊的時候使用循環CAS釋放鎖。

相關文章
相關標籤/搜索