java併發-緩存一致性

硬件緩存一致性

是 CPU、內存、以及 I/O 設備(磁盤)讀取性能不一致,爲了平衡三者的速度差異,最大化的利用 CPU 提升性能,從硬件、操作系統、編譯器等方面都做出了很多的優化

  1. CPU 增加了高速緩存
  2. 操作系統增加了進程、線程。通過 CPU 的時間片切換最大化的提升 CPU 的使用率
  3. 編譯器的指令優化
    在這裏插入圖片描述
    通過高速緩存的存儲交互很好的解決了處理器與內存的速度矛盾,但帶來了緩存一致性問題

爲了解決緩存不一致的問題,在 CPU 層面做了很多事情,主要提供了兩種解決辦法

  • 總線鎖
  • 緩存鎖

總線鎖,簡單來說就是,在多 cpu 下,當其中一個處理器要對共享內存進行操作的時候,在總線上發出一個 LOCK#信號,這個信號使得其他處理器無法通過總線來訪問到共享內存中的數據,總線鎖定把 CPU 和內存之間的通信鎖住
了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大,這種機制顯然是不合適的
如何優化呢?最好的方法就是控制鎖的保護粒度,我們只需要保證對於被多個 CPU 緩存的同一份數據是一致的就行。所以引入了緩存鎖,它核心機制是基於緩存一致性協議來實現的。

JMM

Java Memory Model

JMM 屬於語言級別的抽象內存模型,可以簡單理解爲對硬件模型的抽象,它定義了共享內存中多線程程序讀寫操作的行爲規範:在虛擬機中把共享變量存儲
到內存以及從內存中取出共享變量的底層實現細節

通過這些規則來規範對內存的讀寫操作從而保證指令的正確性,它解決了 CPU 多級緩存、處理器優化、指令重排序導致的內存訪問問題,保證了併發場景下的可見性。

JMM 並沒有限制執行引擎使用處理器的寄存器或者高速緩存來提升指令執行速度,也沒有限制編譯器對指令進行重排序,也就是說在 JMM 中,也會存在緩存一致性問題和指令重排序問題。只是 JMM 把底層的問題抽象到 JVM 層面,再基於 CPU 層面提供的內存屏障指令,以及限制編譯器的重排序來解決併發問題

在這裏插入圖片描述
JMM 抽象模型分爲主內存、工作內存;主內存是所有線程共享的,一般是實例對象、靜態字段、數組對象等存儲在堆內存中的變量。工作內存是每個線程獨佔的,線程對變量的所有操作都必須在工作內存中進行,不能直接讀寫主
內存中的變量,線程之間的共享變量值的傳遞都是基於主內存來完成
在這裏插入圖片描述
Java 內存模型底層實現可以簡單的認爲:通過內存屏障(memory barrier)禁止重排序,即時編譯器根據具體的底層體系架構,將這些內存屏障替換成具體的 CPU 指令。對於編譯器而言,內存屏障將限制它所能做的重排序優化。而對於處理器而言,內存屏障將會導致緩存的刷新操作。比如,對於 volatile,編譯器將在 volatile 字段的讀寫操作前後各插入一些內存屏障

JMM解決可見性有序性問題:
volatile、synchronized、final;
以及通過happenbefore規則解決順序一致性問題

synchronized

在這裏插入圖片描述
Java中的每個對象都可以把它當作一個同步鎖來使用,叫作監視器鎖
內存語義:
解決共享變量的內存可見性,原子性,但會引起線程上下文切換並帶來線程調度開銷

HappenBefore

程序順序規則

	class VolatileDemo {
		int a = 0; 
		volatile boolean flag = false;
		public void writer() {
			a = 1 ; // 1
			flag  true; //
		}
		public void reader() {
			if(flag) { 	  // 3
				int i = a; // 4
			}
		}
	}
  1. 順序性規則
    一個線程中的每個操作,happens-before 於該線程中的
    任意後續操作; 可以簡單認爲是 as-if-serial。單個線程
    中的代碼順序不管怎麼變,對於結果來說是不變的
    順序規則表示 1 happenns-before 2; 3 happensbefore 4
  2. volatile變量規則
    對於volatile修飾的變量的寫的操作,一定happen-before後續對於volatile變量的讀操作;根據volatile規則,2happens before 3
  3. 傳遞性規則
    如果1 happens-before 2; 3happens-before 4;
    那麼傳遞性規則表示:1 happens-before 4;
  4. start規則,如果線程A執行操作ThreadB.start(),那麼線程A的ThreadB.start()操作happens-before線程B中的任意操作
  5. join規則
    如果線程A執行操作ThreadB.join()併成功返回,那麼線程B中的任意操作happens-before於線程A從ThreadB.join()操作成功返回。
  6. 監視器鎖的規則
    對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖

可見性解決

volatile

volatile int i = 0;
i++; // 不保證原子性

解決變量的可見性問題,不解決原子性,案例:單例的雙檢查

重排序

爲了提高程序的執行性能,編譯器和處理器都會對指令做重排序,其中處理器的重排序在前面已經分析過了。所謂的重排序其實就是指執行的指令順序。

編譯器的重排序指的是程序編寫的指令在編譯之後,指令可能會產生重排序來優化程序的執行性能。從源代碼到最終執行的指令,可能會經過三種重排序。

在這裏插入圖片描述
2 和 3 屬於處理器重排序。這些重排序可能會導致可見性問題。

編譯器的重排序,JMM 提供了禁止特定類型的編譯器重排序。

處理器重排序,JMM 會要求編譯器生成指令時,會插入內存屏障來禁止處理器重排序

導致可見性問題的根本原因是緩存以及重排序

CAS操作

CAS即Compareand Swap,其是JDK提供的非阻塞原子性操作,它通過硬件保證了比較—更新操作的原子性

JDK裏面的Unsafe類提供了一系列的compareAndSwap*方法

/**
 * @param obj 需要操作的對象
 * @param valueOffset 對象obj中內存偏移量
 * @param expect 變量預期值
 * @param longupdate 新的值
 */
boolean compareAndSwapLong(Object obj, long valueOffset, long expect, longupdate);

ABA問題:AtomicStampedReference解決

Unsafe

JDK的rt.jar包中的Unsafe類提供了硬件級別的原子性操作,Unsafe類中的方法都是native方法,它們使用JNI的方式訪問本地C++ 實現庫

錯誤使用

public class TestUnSafe {
    static final Unsafe unsafe = Unsafe.getUnsafe();
    // 記錄變量state在類TestUnSafe中的偏移值
    static long stateOffset;
    // 變量
    private volatile long state = 0;
    static {
        try {
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        TestUnSafe test = new TestUnSafe();
        Boolean success = unsafe.compareAndSwapInt(test,stateOffset,0,1);
        System.out.println(success);
    }
}

輸出

java.lang.ExceptionInInitializerError
Caused by: java.lang.SecurityException: Unsafe
	at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)
	at com.drank.TestUnSafe.<clinit>(TestUnSafe.java:6)

原因:

// sun.misc.Unsafe#getUnsafe
@CallerSensitive
  public static Unsafe getUnsafe() {
  	  // 獲取調用getUnsafe這個方法的對象Class
      Class localClass = Reflection.getCallerClass();
      // 判斷是不是Bootstrap類加載器加載的localClass
      // 在這裏 TestUnSafe.class 是使用了AppClassLoader加載,所以拋異常
      if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
          throw new SecurityException("Unsafe");
      } else {
          return theUnsafe;
      }
  }

正確打開方式

import sun.misc.Unsafe;
import java.lang.reflect.Field;
public class TestUnSafe {
    static final Unsafe unsafe;
    // 記錄變量state在類TestUnSafe中的偏移值
    static final long stateOffset;
    // 變量
    private volatile long state = 0;
    static {
        try {
            // 使用反射獲取Unsafe的成員變量theUnsafe
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.getDeclaredField("state"));
        } catch (Exception e) {
            e.printStackTrace();
            throw new Error(e);
        }
    }
    public static void main(String[] args) {
        TestUnSafe test = new TestUnSafe();
        Boolean success = unsafe.compareAndSwapInt(test,stateOffset,0,1);
        System.out.println(success);
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章