CAS是什么?ABA問題的產生和解決方法


CAS是什么?

比較並交換(compare and swap)是一條CPU並發原語

功能

判斷內存中某個位置的值是否為預期值,如果是則更改為新的值,這個過程是原子的,中間不予許中斷,解決數據一致性問題。

底層原理

Unsafe類

是CAS的核心類,由於java無法直接訪問底層系統,需要通過本地(native)方法訪問,Unsafe相當於一個后門,該類可以直接操作特定的內存數據。 Unsafe類存在於sun.misc包中,其內部方法操作可以像C的指針一樣直接操作內存,因為java中的CAS依賴於Unsafe類中的方法

注意 Unsafe中的所有方法都是native修飾的,就是說Unsafe中的方法都是直接操作系統底層資源執行任務

底層匯編

底層代碼

 // AtomicInteger類中方法:getAndIncrement,調用Unsafe類中的getAndAddInt
 public final int getAndIncrement(){
     return unsafe.getAndAddInt(this,valueOffset,1);
}
//Unsafe類中
 public final int getAndAddInt(Object var1, long var2, int var4) {
     int var5;
     do {
    var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
     return var5;
}

解釋:

  • var1是AtomicInteger對象本身

  • var2是對象值的引用地址

  • var4是需要變動的數值

  • var5是通過var1、var2找出的內存中的真實的值 方法:用對象的值和var5作比較,如果相同,則更新var5+var4並且返回TRUE,如果不同則繼續取值然后再比較,直到更新完成

 

 

 

 

缺點

1、 循環時間開銷大

2、只能保證一個共享變量的原子操作

3、 引出ABA問題

 

ABA問題

CAS算法實現一個重要前提需要提取出內存中某時刻的數據並在當下時刻比較並替換,那么在這個時間差內會導致數據的變化。

舉例:

一個線程one從內存位置V中取出A,這時候另一個線程two也從內存中取出A,並且線程two進行了一些操作將A變成了B,然后又將V位置的數據變成了A,而這時候線程one進行 CAS操作的時候發現內存中仍然是A,然后one線程提示操作成功。

盡管one線程的CAS操作成功,但是不代表這個線程是沒問題的

 

代碼還原

package com.dayu.inter;

import sun.rmi.runtime.NewThreadAction;

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

/**
 * @Author: dayu
 * @Date: 2019/9/24 15:13
 * @Description
 */
public class ABADemo {

    public static void main(String[] args) {
        System.out.println("=========下面是ABA問題的產生==========");
        AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
        new Thread(() -> {
            System.out.println(atomicReference.compareAndSet(100, 101)+"\t"+atomicReference.get());
            System.out.println(atomicReference.compareAndSet(101, 100)+"\t"+atomicReference.get());
        }, "t1").start();

        new Thread(() -> {
            //休息一會,讓線程t1先執行一遍ABA的問題
            try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}

            System.out.println(atomicReference.compareAndSet(100, 2000)+"\t"+atomicReference.get());
        }, "t2").start();

        //休息一會,確保上面兩個線程執行完畢
        try {TimeUnit.SECONDS.sleep(2);} catch (InterruptedException e) {e.printStackTrace();}
        
        System.out.println("=========下面是ABA問題的解決==========");
        //有點類似於樂觀鎖
        //初始值設定100,時間戳(版本號=1)
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 第一次獲取版本號"+stamp);
        
            //休息一會,等待t4獲取版本號
            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();

        //t4和t3最初獲取到的版本號一致,
        new Thread(() -> {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t 第一次獲取版本號"+stamp);

            //休息一會,確保t3完成一次ABA
            try {TimeUnit.SECONDS.sleep(4);} catch (InterruptedException e) {e.printStackTrace();}
            boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+"\t 是否修改成功:"+result+"\t 當前真實的版本號:"+atomicStampedReference.getStamp()
            +"\t 當前真實的值:"+atomicStampedReference.getReference());
        }, "t4").start();


    }
}

 

為了解決ABA問題,在原子引用類上加上版本號,這個有點類似於mysql的樂觀鎖一樣,每個線程更改一次都需要更改版本號,那么多線程同時獲取到同一個版本號的時候也只有一個線程可以更改成功。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM