六、原子操作CAS

一、什么是原子操作?如何實現(xiàn)原子操作?

CAS:Compare And Swap,比較并且交換。隸屬于樂觀鎖機制。
什么是原子操作?
假設現(xiàn)在有A,B兩個操作,如果某個線程執(zhí)行A操作,當另外一個線程執(zhí)行B操作的時候,要么這個B全部執(zhí)行完,要么這個B完全不執(zhí)行,那么對于A、B來講,他們彼此就是原子的。
在數(shù)據(jù)庫層面,這種操作就是事務操作,嚴格意義上來說事務操作也是屬于原子操作的一種
如何實現(xiàn)原子操作
可以利用synchronize關鍵字,但是會引發(fā)一系列問題:

  • 1.synchronize是阻塞式的,一個線程擁有鎖后,其他的線程必須等待
  • 2.等待中的線程優(yōu)先級很高,但是遲遲拿不到鎖怎么辦?
  • 3.等待中的線程競爭很激烈,但是拿到鎖的線程遲遲不釋放鎖怎么辦?
解決辦法CAS

CAS可以完美地解決上述的問題,進而更完美地實現(xiàn)原子操作,它利用了現(xiàn)代處理器都支持的CAS指令,這個指令是CPU級別的指令。

CAS包含的要素

1.內(nèi)存地址v:修改的對象或者變量的內(nèi)存地址
2.期望值A:
3.新值B
當我去改這個內(nèi)存地址上所對應的對象或者變量的時候,我期望在我改的時候,這個值是多少,如果是A,我就把他改成B,如果不是A,那我就不能改。將B值替換為A值。
比較---->交換

用java語言來講,這個操作需要兩個語句,一個是比較,一個是交換。
而在CPU層面,只要你執(zhí)行了這個指令,我可以保證別的指令都被阻塞,只有這一個CAS指令操作完了才允許別的指令進行操作。

在JDK層面來講,用到了循環(huán)(自旋、死循環(huán)),直到成功為止,原理如下:

原理

這種思想就是樂觀鎖

用一句話來概括CAS如何實現(xiàn)線程安全?

CAS在語言層面不作處理,我們把它交給了CPU和內(nèi)存,利用CPU的能力實現(xiàn)硬件層面阻塞,進而實現(xiàn)CAS的線程安全。

二、CAS引起的問題

1.ABA問題

下面的兩種情況下會出現(xiàn)ABA問題。
1.A最開始的內(nèi)存地址是X,然后失效了,又分配了B,恰好內(nèi)存地址是X,這時候通過CAS操作,卻設置成功了
  這種情況在帶有GC的語言中,這種情況是不可能發(fā)生的,為什么呢?拿JAVA舉例,在執(zhí)行CAS操作時,A,B對象肯定生命周期內(nèi),GC不可能將其釋放,那么A指向的內(nèi)存是不會被釋放的,B也就不可能分配到與A相同的內(nèi)存地址,CAS失敗。若在無GC的,A對象已經(jīng)被釋放了,那么B被分配了A的內(nèi)存,CAS成功。
2.線程1準備用CAS將變量的值由A替換為B,在此之前,線程2將變量的值由A替換為C,又由C替換為A,然后線程1執(zhí)行CAS時發(fā)現(xiàn)變量的值仍然為A,所以CAS成功。但實際上這時的現(xiàn)場已經(jīng)和最初不同了,盡管CAS成功,但可能存在潛藏的問題。比如:

現(xiàn)有一個用單向鏈表實現(xiàn)的堆棧,棧頂為A,這時線程T1已經(jīng)知道A.next為B,然后希望用CAS將棧頂替換為B:head.compareAndSet(A,B);在T1執(zhí)行上面這條指令之前,線程T2介入,將A、B出棧,再pushD、C、A。而對象B此時處于游離狀態(tài):此時輪到線程T1執(zhí)行CAS操作,檢測發(fā)現(xiàn)棧頂仍為A,所以CAS成功,棧頂變?yōu)锽,但實際上B.next為null,其中堆棧中只有B一個元素,C和D組成的鏈表不再存在于堆棧中,平白無故就把C、D丟掉了。

以上就是由于ABA問題帶來的隱患,各種樂觀鎖的實現(xiàn)中通常都會用版本戳version來對記錄或對象標記,避免并發(fā)操作帶來的問題,在Java中,AtomicStampedReference<E>也實現(xiàn)了這個作用,它通過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題,例如下面的代碼分別用AtomicInteger和AtomicStampedReference來對初始值為100的原子整型變量進行更新,AtomicInteger會成功執(zhí)行CAS操作,而加上版本戳的AtomicStampedReference對于ABA問題會執(zhí)行CAS失敗。

package concur.lock;

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

public class ABA {
    
    private static AtomicInteger atomicInt = new AtomicInteger(100);
    private static AtomicStampedReference<Integer> atomicStampedRef = 
            new AtomicStampedReference<Integer>(100, 0);
    
    public static void main(String[] args) throws InterruptedException {
        Thread intT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInt.compareAndSet(100, 101);
                atomicInt.compareAndSet(101, 100);
            }
        });
        
        Thread intT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean c3 = atomicInt.compareAndSet(100, 101);
                System.out.println(c3);        //true
            }
        });
        
        intT1.start();
        intT2.start();
        intT1.join();
        intT2.join();
        
        Thread refT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedRef.compareAndSet(100, 101, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
                atomicStampedRef.compareAndSet(101, 100, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
            }
        });
        
        Thread refT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedRef.getStamp();
                System.out.println("before sleep : stamp = " + stamp);    // stamp = 0
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
                System.out.println(c3);        //false
            }
        });
        
        refT1.start();
        refT2.start();
    }

}

如何解決?
增加版本號,也就是說在每個變量前面都要加一個版本號,每次修改的時候都對其版本+1。其實在大多數(shù)開發(fā)過程中,我們是不關心ABA問題的。但是ABA問題在一線互聯(lián)網(wǎng)公司的面試中是經(jīng)常問到的。

  • 1.ABA問題的解決思路是使用版本號,每次變量更新的時候版本號加1,那么A->B->A就會變成1A->2B->3A
  • 2.從jdk1.5開始,jdk的Atomic包里就提供了兩個類來解決ABA問題,一個是AtomicStampedReference,另一個是AtomicMarkableReference,AtomicStampedReference這個類中的compareAndSet方法的作用就是首先檢查當前引用是否等于預期引用,并且檢查當前標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值更新為指定的新值。
    AtomicStampedReferenceAtomicMarkableReference的區(qū)別
    AtomicStampedReference帶了版本號,關心被修改過幾次,AtomicMarkableReference只關心有沒有人修改過。

2.開銷問題

自旋CAS如果長時間不成功,會給CPU帶來非常大的執(zhí)行開銷。如果jvm能支持處理器提供的pause指令,那么效率會有一定的提升。pause指令有兩個作用:

第一,它可以延遲流水線執(zhí)行指令(de-pipeline),使CPU不會消耗過多的執(zhí)行資源,延遲的時間取決于具體實現(xiàn)的版本,在一些處理器上延遲時間是零。

第二,它可以避免在退出循環(huán)的時候因內(nèi)存順序沖突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU的執(zhí)行效率。

3.只能保證一個變量的原子操作

當對一個共享變量執(zhí)行操作時,我們可以使用循環(huán)CAS的方式來保證原子操作,但是多個共享變量操作時,循環(huán)CAS就無法保證操作的原子性,這個時候就可以用鎖。還有一個方法,就是把多個共享變量合并成一個共享變量來操作。比如,有兩個共享變量i=2,j=a合并一下ij=2a,然后用CAS來操作ij。從java1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個變量放在一個對象里來進行CAS操作。

三、原子操作類的使用

jdk中相關原子操作類的使用

  • 更新基本類型類:AtomicBoolean,AtomicInteger,AtomicLong
  • 更新數(shù)組類:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • 更新引用類:AtomicReference,AtomicMarkableReference,AtomicStampeReference
  • 原子更新字段類:AtomicReferenceFiledUpdater,AtomicIntegerFiledUpdater,AtomicLongFiledUpdater

舉例:

import java.util.concurrent.atomic.AtomicInteger;

/**
 *類說明:演示基本類型的原子操作類
 */
public class UseAtomicInt {
    static AtomicInteger ai = new AtomicInteger(10);

    public static void main(String[] args) {
        //返回的是我自增以前的值
        int i =  ai.getAndIncrement(); // i++
        //返回自增以后的值
        int b = ai.incrementAndGet();// ++i
        System.out.println(i +"------"+ b);
        //ai.compareAndSet();
        int fianl = ai.addAndGet(24);
        System.out.println("加了24之后的值為:"+fianl);
    }
}

運行結果:


原子操作類的使用
import java.util.concurrent.atomic.AtomicIntegerArray;


/**
 *類說明: 演示原子操作數(shù)組
 */
public class AtomicArray {
    static int[] value = new int[] { 1, 2 };
    static AtomicIntegerArray ai = new AtomicIntegerArray(value);
    public static void main(String[] args) {
        ai.getAndSet(0, 3);
        System.out.println(ai.get(0));
        System.out.println(value[0]);//原數(shù)組不會變化
        }
}

運行結果:


原子操作數(shù)組
注意:

原子操作只會操作原子類的值,不會操作原數(shù)組,原子操作類的值再怎么變也不會影響原數(shù)組的值

運用原子操作類修改兩個變量的值
import java.util.concurrent.atomic.AtomicReference;

/**
 *類說明:演示引用類型的原子操作類
 */
public class UseAtomicReference {
    static AtomicReference<UserInfo> atomicUserRef;
    public static void main(String[] args) {
        UserInfo user = new UserInfo("Mark", 15);//要修改的實體的實例
        atomicUserRef = new AtomicReference(user);
        UserInfo updateUser = new UserInfo("Bill",17);
        atomicUserRef.compareAndSet(user,updateUser);

        System.out.println(atomicUserRef.get());
        System.out.println(user);
    }
    
    //定義一個實體類
    static class UserInfo {
        private volatile String name;
        private int age;
        public UserInfo(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String getName() {
            return name;
        }
        public int getAge() {
            return age;
        }

        @Override
        public String toString() {
            return "UserInfo{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

}

運行結果:


AtomicReference

這是運用AtomicReference修改兩個變量的值,本質(zhì)上是包裝成一個變量,對這一個變量進行修改。

?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容