volatile | CAS| ABA

 

JMM

JMM(Java內存模型Java Memory Model)是一種抽象的概念並不真實存在,它描述的是一組規則或規範,經過這組規範定義了程序中各個變量(包括實例字段、靜態字段和構成數組對象的元素)的訪問方式。java

JMM關於同步的規定:數組

  ①線程解鎖前,必須把共享變量的值刷新回主內存;緩存

  ②線程解鎖前,必須讀取主內存的最新值到本身的工做內存;安全

  ③加鎖解鎖是同一把鎖;服務器

 

①把變量讀到各個線程的工做內存中;②線程運算完以後把變量的值改好,而後把它寫回主內存;③可見性--讓其餘線程立刻知道這個值;多線程

各個線程對主內存中共享變量的操做都是各個線程各自拷貝到本身的工做內存進行操做後再寫回到主內存中的;併發

這就可能存在一個線程AAA修改了共享變量X的值但還未寫回主內存時,另一個線程BBB又對主內存中同一個共享變量X進行操做,但此時A線程工做內存中共享變量x對線程B來講並不可見,這種工做內存與主內存同步延遲現象就形成了可見性問題;高併發

 可見性優化

class MyData{
    volatile int num = 0;
    public void add(){
        this.num = 60;
    }
}
public class TestJMM {
    public static void main(String[] args) {
        MyData myData = new MyData();
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "\tcome in");
            //暫停一會線程
            try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) { e.printStackTrace();}
            myData.add();
            System.out.println(Thread.currentThread().getName() + "\tupdated number value:" + myData.num);

            },"aa").start();
            //第二個線程就是main線程
        while (myData.num == 0){
            //main一直在循環,等待num的值不爲0
        }
        System.out.println(Thread.currentThread().getName() + "\tmission is over, main get num value:" + myData.num);

    }
}
//volatile能夠保證可見性,及時通知其餘線程,主物理內存的值已經被修改;若是不加volatile則主線程就一直在那等待着,程序就卡那了;
 aa come in
aa updated number value:60
main mission is over, main get num value:60
 

 不保證原子性this

class MyData{
    volatile int num = 0;
    public void add(){
        this.num = 60;
    }
    // num前面加了volatile關鍵字,不保證原子性
    public void addPlusPlus(){
        num++;
    }
}
public class TestJMM {
    public static void main(String[] args) {
        MyData myData = new MyData();

        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus(); //最後結果應該是20000
                }
                }, String.valueOf(i)).start();
        }
        //等待上面20個線程計算完以後,再用main線程取得最後結果
        while (Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t finally num is:" + myData.num);
    }
}

main     finally num is:19951 結果是隨機的;

 n++的字節碼過程

public class T1 {
    volatile int n = 0;
    public void add(){
        n++; //n++被拆分紅3個指令:①執行getfield拿到原始值n;②執行iadd進行加1操做;③執行putfield把累加後的值寫回;
    }
}
  public void add();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field n:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field n:I
        10: return
      LineNumberTable:
        line 6: 0
        line 7: 10
      LocalVariableTable:

不保證原子性的解決方法:

class MyData{
     volatile int num = 0;
    public void add(){
        this.num = 60;
    }
    // num前面加了volatile關鍵字,不保證原子性
    public void  addPlusPlus(){
        num++;
    }

    AtomicInteger atomicInteger = new AtomicInteger();
    public void addMyAutomic(){
        atomicInteger.getAndIncrement();   //如何解決原子性,加sync...,它鎖的範圍太廣了;使用JUC下的AtomicInteger
    }
}
public class TestJMM {
    public static void main(String[] args) {
        MyData myData = new MyData();
        //TODO 原子性的解決方法
        for (int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    myData.addPlusPlus(); //最後結果應該是20000
                    myData.addMyAutomic();
                }
                }, String.valueOf(i)).start();
        }
        //等待上面20個線程計算完以後,再用main線程取得最後結果
        while (Thread.activeCount() > 2){ //若是線程活躍個數>2,就暫停當前線程,把CPU資源讓出
            Thread.yield();
        }
        System.out.println(Thread.currentThread().getName() + "\t finally num is:" + myData.num);
        System.out.println(Thread.currentThread().getName() + "\t AtomicInteger type,finally num is:" + myData.atomicInteger);

    }
}

main finally num is:19356
main AtomicInteger type,finally num is:20000

指令作重排

 

public class SingletonDemo {
    private static volatile SingletonDemo instance = null;
    //構造方法,都會執行main    我是構造方法SingletonDemo
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName() + "\t我是構造方法SingletonDemo");
    }
    //單例模式,在高併發下,仍是會出異常,由於存在指令重排  DCL(Double Check Lock)雙端檢索機制,加鎖先後都作判斷 
    public static  SingletonDemo getInstance(){
/*        if (instance == null){
             instance = new SingletonDemo(); //單例模式加synchronized過重,須要控制的只是這一行
        }
        return instance;
        //在上邊代碼會出現如下狀況:
        2    我是構造方法SingletonDemo
        4    我是構造方法SingletonDemo
        3    我是構造方法SingletonDemo
        1    我是構造方法SingletonDemo*/

        if (instance == null){
            synchronized (SingletonDemo.class){
                if (instance == null){
                    instance = new SingletonDemo();
                }
            }
        }
        return instance; //這裏只是拿到了內存地址,數據還沒拿到,仍是有可能發生指定重排的;加volatile可實現禁止指令重排
    }
    public static void main(String[] args) {
        //單線程下
/*        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //true
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //true
        System.out.println(SingletonDemo.getInstance() == SingletonDemo.getInstance()); //true*/

        //併發環境下,狀況發生了很大變化
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                SingletonDemo.getInstance();
            }, String.valueOf(i)).start();
        }
    }
}
--------------------------
1 我是構造方法SingletonDemo

 

volatile實現禁止指令重排優化,從而避免多線程環境下程序出現亂序執行的現象;

概念,內存屏障(Memory Barrier)又稱內存柵欄,是一個CPU指令,它有兩個做用:

  ①保證特定操做的執行順序;

  ②保證某些變量的內存可見性(利用該特性實現volatile的內存可見性);

因爲編譯器和處理器都能執行指令重排。若是在指令間插入一條Memory Barrier則會告訴編譯器和CPU,無論什麼指令都不能和這條Memory Barrier指令重排序,也就是說經過插入內存屏障禁止在內存屏障先後的指令執行重排序優化。內存屏障另一個做用是強制刷出各自CPU的緩存數據,所以任何CPU上的線程都能讀取到這些數據的最新版本。

爲了保證有序,須要禁止指定重排;

線程安全性得到保證:

工做內存與主內存同步延遲現象致使的可見性問題,可使用synchronized或volatile關鍵字解決,它們均可以使一個線程修改後的變量當即對其餘線程可見。

對於執行重排致使的可見性問題和有序性問題,能夠利用volatile關鍵字解決,由於volatile的另一個做用就是禁止重排序優化。

 

volatile只能保證其中的兩個,不保證原子性;因此是低配版的輕量級;

你在哪些地方用過volatile? ①單例模式DCL代碼

單例模式一共6種;懶漢、餓漢

 併發

 DCL(雙端檢鎖)機制不必定線程安全,緣由是有指令重排的存在,加入volatile能夠禁止指令重排;

緣由在於某一個線程執行到第一次檢測,讀取到的instance不爲null時,instance的引用對象可能沒有完成初始化

instance = new SingletonDemo();能夠分爲如下3步完成(僞代碼)

memory = allocate();//1.分配對象內存空間

instance(memory);//2.初始化對象

instance = memory;//3.設置instance指向剛分配的內存地址,此時instance! = null

步驟2和步驟3不存在數據依賴關係,並且不管重排前仍是重排後程序的執行結果在單線程中並無改變,所以這種重排優化是運行的。

memory = allocate();//1.分配對象內存空間

instance = memory;//3.設置instance指向剛分配的內存地址,此時instance! = null,可是對象尚未初始化完成!

instance(memory);//2.初始化對象

可是指令重排只會保證串行語義的執行的一致性(單線程),但並不會關心多線程間的語義一致性。

因此當一條線程訪問instance不爲null時,因爲instance實例未必已初始化完成,也就形成了線程安全問題。

 

單例模式DCL代碼;單例模式volatile分析

  1Unsafe

  是CAS的核心類,因爲java方法沒法直接訪問底層系統,須要經過本地(native)方法來訪問,Unsafe至關於一個後門,基於該類能夠直接操做特定內存的數據。

Unsafe類存在於sun.misc包中,其中內部方法操做能夠像C的指針同樣直接操做內存,由於java中CAS操做的執行依賴於Unsafe類的方法。

注意Unsafe類中的全部方法都是native修飾的,也就是說Unsafe類中的方法都直接調用操做系統底層資源執行相應任務。

2 變量valueOffset,表示該變量值在內存中的偏移地址,由於Unsafe就是根據內存偏移地址獲取數據的。

3.

它時來判斷內存中的某個值是否爲指望值;

 

 

 

 

CAS缺點:

CAS沒有加鎖,要保證數據一致性、原子性,使用do while

引導出ABA

 Unsafe類和CAS思想(自旋)

AtomicInteger--->

ABA狸貓換太子

 

ABA,只管結果無論過程,只要頭跟尾對上就能夠了; 

解決ABA問題?

AtomicReference原子引用
@Getter
@ToString
@AllArgsConstructor
class User{
    String username;
    int age;

}
public class AtomicReferenceDemo {
    public static void main(String[] args) {
        User kris = new User("kris", 22);
        User alex = new User("alex", 23);
        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(kris); //若是想對某個類進行原子性包裝
        System.out.println(atomicReference.compareAndSet(kris, alex) + "\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(kris, alex) + "\t" + atomicReference.get().toString());

/*      true    User(username=alex, age=23)
        false    User(username=alex, age=23)

        */
    }
}

 

時間戳的原子引用  就是解決ABA問題

ABA問題的解決:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABADemo { //ABA問題的解決 AtomicStampedReference
    static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
    //這兩個要寫這裏
    public static void main(String[] args) {

        System.out.println("============ABA問題的產生============");
        new Thread(() -> {
                atomicReference.compareAndSet(100, 101);
                atomicReference.compareAndSet(101, 101);
            },"t1").start();
        new Thread(() -> {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(atomicReference.compareAndSet(101, 2019) + "\t" + atomicReference.get());

            },"t2").start(); //true     2019  成功改成了2019,可是中間是有人動過的;
        //暫停一會線程
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        System.out.println("==================ABA問題的解決#############");
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本號:" + stamp);//1
            //暫停1s t3線程
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第2次版本號:" + atomicStampedReference.getStamp());
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName() + "\t第3次版本號:" + atomicStampedReference.getStamp());},
                "t3").start();

        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t第1次版本號:" + stamp);//1
            //暫停3s 4線程保證上面的t3線程完成了一次ABA操做t3 t4都暫停下就是讓它們的起點同樣,都拿到版本號爲1的數據 100 
            try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
            boolean restlt = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1); //t4提交的版本號2,但服務器上最新的版本號是3;
            System.out.println(Thread.currentThread().getName() + "\t修改爲功否:" + restlt + "\t當前最新版本號:" + atomicStampedReference.getStamp());
            System.out.println(Thread.currentThread().getName() + "\t當前實際最新值:" + atomicStampedReference.getReference());
            },"t4").start();
    }
}

============ABA問題的產生============
true    2019
==================ABA問題的解決#############
t3    第1次版本號:1
t4    第1次版本號:1
t3    第2次版本號:2
t3    第3次版本號:3
t4    修改爲功否:false    當前最新版本號:3
t4    當前實際最新值:100
相關文章
相關標籤/搜索