多線程與高併發05-原子操做CAS

原子操做CAS-無鎖優化樂觀鎖

什麼是原子操做,如何實現原子操做

假定有兩個操做A和B,若是從執行A的線程來看,當另外一個線程執行B時,要麼將B所有執行完,要麼徹底不執行B,那麼A和B對彼此來講是原子的java

實現原子操做可使用,鎖機制,知足基本的需求是沒有問題的了,可是有的時候咱們的需求並不是這麼簡單,咱們須要更有效,更加靈活的機制,synchronized關鍵字是基於阻塞的鎖機制(鎖升級),也就是說當一個線程擁有鎖的時候,訪問同一資源的其它線程須要等待,直到該線程釋放鎖程序員

這裏會有些問題:首先,若是被阻塞的線程優先級很高很重要怎麼辦?其次,若是得到鎖的線程一直不釋放鎖怎麼辦?(這種狀況是很是糟糕的)。還有一種狀況,若是有大量的線程來競爭資源,那CPU將會花費大量的時間和資源來處理這些競爭,同時,還有可能出現一些例如死鎖之類的狀況,最後,其實鎖機制是一種比較粗糙,粒度比較大的機制,相對於像計數器這樣的需求有點兒過於笨重算法

實現原子操做還可使用當前的處理器基本都支持CAS()的指令,只不過每一個廠家所實現的算法並不同,每個CAS操做過程都包含三個運算符:數組

  • 一個內存地址V
  • 一個指望的值A
  • 一個新值B

操做的時候若是這個地址上存放的值等於這個指望的值A,則將地址上的值賦爲新值B,不然不作任何操做安全

CAS的基本思路就是,若是這個地址上的值和指望的值相等,則給其賦予新值,不然不作任何事兒,可是要返回原值是多少。循環CAS就是在一個循環裏不斷的作cas操做,直到成功爲止
image.png架構

CAS是怎麼實現線程的安全呢:ide

語言層面不作處理,咱們將其交給硬件—CPU和內存,利用CPU的多處理能力,實現硬件層面的阻塞,再加上volatile變量的特性便可實現基於原子操做的線程安全

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

ABA問題

由於CAS須要在操做值的時候,檢查值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了優化

ABA問題的解決思路就是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加1,那麼A→B→A就會變成1A→2B→3A。舉個通俗點的例子,你倒了一杯水放桌子上,幹了點別的事,而後同事把你水喝了又給你從新倒了一杯水,你回來看水還在,拿起來就喝,若是你無論水中間被人喝過,只關心水還在,這就是ABA問題。this

若是你是一個講衛生講文明的小夥子,不但關心水在不在,還要在你離開的時候水被人動過沒有,由於你是程序員,因此就想起了放了張紙在旁邊,寫上初始值0,別人喝水前麻煩先作個累加才能喝水atom

ABA問題只是一種現象,並不必定是問題,在架構設計中也有這種樂觀鎖的使用(冪等性操做)

循環時間長開銷大

自旋CAS若是長時間不成功,會給CPU帶來很是大的執行開銷

只能保證一個共享變量的原子操做

當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖

還有一個取巧的辦法,就是把多個共享變量合併成一個共享變量來操做。好比,有兩個共享變量i=2,j=a,合併一下ij=2a,而後用CAS來操做ij。從Java 1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性,就能夠把多個變量放在一個對象裏來進行CAS操做

JDK中相關原子操做類的使用

AtomicInteger

  • int addAndGet(int delta):以原子方式將輸入的數值與實例中的值(AtomicInteger裏的value)相加,並返回結果
  • boolean compareAndSet(int expect,int update):若是輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值
  • int getAndIncrement():以原子方式將當前值加1,注意,這裏返回的是自增前的值
  • int getAndSet(int newValue):以原子方式設置爲newValue的值,並返回舊值

示例代碼:

/**
 * 解決一樣的問題的更高效的方法,使用AtomXXX類
 * AtomXXX類自己方法都是原子性的,但不能保證多個方法連續調用是原子性的
 */
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class T01_AtomicInteger {
    /*volatile*/ //int count1 = 0;
    
    AtomicInteger count = new AtomicInteger(0);

    /*synchronized*/ void m() {
        for (int i = 0; i < 10000; i++)
            //if count1.get() < 1000
            count.incrementAndGet();
            //count1++;
    }

    public static void main(String[] args) {
        T01_AtomicInteger t = new T01_AtomicInteger();

        List<Thread> threads = new ArrayList<Thread>();

        for (int i = 0; i < 10; i++) {
            threads.add(new Thread(t::m, "thread-" + i));
        }

        threads.forEach((o) -> o.start());

        threads.forEach((o) -> {
            try {
                o.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        System.out.println(t.count);

    }

}

運行結果:

100000
上面例子能夠看出用AtomicInteger,和加synchronized鎖效果是同樣的,並且CAS效率更高

除了synchronized、AtomicInteger以外還有LongAdder,下面代碼比較一下他們各自的效率:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

public class T02_AtomicVsSyncVsLongAdder {
    static long count2 = 0L;
    static AtomicLong count1 = new AtomicLong(0L);
    static LongAdder count3 = new LongAdder();

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[1000];

        for(int i=0; i<threads.length; i++) {
            threads[i] =
                    new Thread(()-> {
                        for(int k=0; k<100000; k++) count1.incrementAndGet();
                    });
        }

        long start = System.currentTimeMillis();

        for(Thread t : threads ) t.start();

        for (Thread t : threads) t.join();

        long end = System.currentTimeMillis();

        //TimeUnit.SECONDS.sleep(10);

        System.out.println("Atomic: " + count1.get() + " time " + (end-start));
        //-----------------------------------------------------------
        Object lock = new Object();

        for(int i=0; i<threads.length; i++) {
            threads[i] =
                new Thread(new Runnable() {
                    @Override
                    public void run() {

                        for (int k = 0; k < 100000; k++)
                            synchronized (lock) {
                                count2++;
                            }
                    }
                });
        }

        start = System.currentTimeMillis();

        for(Thread t : threads ) t.start();

        for (Thread t : threads) t.join();

        end = System.currentTimeMillis();


        System.out.println("Sync: " + count2 + " time " + (end-start));


        //----------------------------------
        for(int i=0; i<threads.length; i++) {
            threads[i] =
                    new Thread(()-> {
                        for(int k=0; k<100000; k++) count3.increment();
                    });
        }

        start = System.currentTimeMillis();

        for(Thread t : threads ) t.start();

        for (Thread t : threads) t.join();

        end = System.currentTimeMillis();

        //TimeUnit.SECONDS.sleep(10);

        System.out.println("LongAdder: " + count1.longValue() + " time " + (end-start));

    }

    static void microSleep(int m) {
        try {
            TimeUnit.MICROSECONDS.sleep(m);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

運行結果:

Atomic: 100000000 time 2255
Sync: 100000000 time 3257
LongAdder: 100000000 time 509

AtomicIntegerArray

主要是提供原子的方式更新數組裏的整型,其經常使用方法以下:

  • int addAndGet(int i,int delta):以原子方式將輸入值與數組中索引i的元素相加
  • boolean compareAndSet(int i,int expect,int update):若是當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。

須要注意的是:

數組value經過構造方法傳遞進去,而後 AtomicIntegerArray會將 當前數組複製一份,因此當AtomicIntegerArray對內部的數組元素進行修改時,不會影響傳入的數組

示例代碼:

import java.util.concurrent.atomic.AtomicIntegerArray;
/**
 *類說明:
 */
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]);//原數組不會變化
        }
}

運行結果:

3
1

更新引用類型

原子更新基本類型的AtomicInteger,只能更新一個變量,若是要原子更新多個變量,就須要使用這個原子更新引用類型提供的類。Atomic包提供瞭如下3個類

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 +
                    '}';
        }
    }
}

運行結果:

UserInfo{name='Bill', age=17}
UserInfo{name='Mark', age=15}
鎖的效率
LongAdder > Atomic > synchronized
  |            |         | 
分段鎖+CAS     CAS        普通鎖
線程多優點大
分段線程加CAS
最後把結果合起來
AtomicStampedReference

利用版本戳的形式記錄了每次改變之後的版本號,這樣的話就不會存在ABA問題了。這就是AtomicStampedReference的解決方案。AtomicMarkableReferenceAtomicStampedReference差很少, AtomicStampedReference是使用pairint stamp做爲計數器使用,AtomicMarkableReferencepair使用的是boolean mark。 仍是那個水的例子,AtomicStampedReference可能關心的是動過幾回,AtomicMarkableReference關心的是有沒有被人動過,方法都比較簡單
示例代碼:

import java.util.concurrent.atomic.AtomicStampedReference;

/**
 *類說明:演示帶版本戳的原子操做類
 */
public class UseAtomicStampedReference {
    static AtomicStampedReference<String> asr
            = new AtomicStampedReference("mark",0);

    public static void main(String[] args) throws InterruptedException {
        //拿到當前的版本號(舊)
        final int oldStamp = asr.getStamp();
        final String oldReference = asr.getReference();
        System.out.println(oldReference+"============"+oldStamp);

        Thread rightStampThread = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":當前變量值:"
                        +oldReference + "-當前版本戳:" + oldStamp + "-"
                  + asr.compareAndSet(oldReference,
                        oldReference + "+Java", oldStamp,
                        oldStamp + 1));
            }
        });

        Thread errorStampThread = new Thread(new Runnable() {
            @Override
            public void run() {
                String reference = asr.getReference();
                System.out.println(Thread.currentThread().getName()
                        +":當前變量值:"
                        +reference + "-當前版本戳:" + asr.getStamp() + "-"
                        + asr.compareAndSet(reference,
                        reference + "+C", oldStamp,
                        oldStamp + 1));
            }
        });
        rightStampThread.start();
        rightStampThread.join();
        errorStampThread.start();
        errorStampThread.join();

        System.out.println(asr.getReference()+"============"+asr.getStamp());
    }
}

運行結果:

mark============0
Thread-0:當前變量值:mark-當前版本戳:0-true
Thread-1:當前變量值:mark+Java-當前版本戳:1-false
mark+Java============1
AtomicMarkableReference

原子更新帶有標記位的引用類型。能夠原子更新一個布爾類型的標記位和引用類型。構造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)

原子更新字段類(不經常使用)

若是需原子地更新某個類裏的某個字段時,就須要使用原子更新字段類,Atomic包提供瞭如下3個類進行原子字段更新

要想原子地更新字段類須要兩步。第一步,由於原子更新字段類都是抽象類,每次使用的時候必須使用靜態方法newUpdater()建立一個更新器,而且須要設置想要更新的類和屬性。第二步,更新類的字段(屬性)必須使用public volatile修飾符

AtomicIntegerFieldUpdater

原子更新整型的字段的更新器

AtomicLongFieldUpdater

原子更新長整型字段的更新器

AtomicReferenceFieldUpdater

原子更新引用類型裏的字段

相關文章
相關標籤/搜索