併發無鎖工具類——原子類

1. 原子類的簡單運用和原理

首先回顧一下經典的累加器的案例:

class My {
    public int count = 0;
    public void run() {
        for(int i=0; i<10000; i++) {
            count++;
        }
    }
}
public class Test2 {
    public static void main(String[] args) throws Exception{
        My my = new My();
        for(int i=0; i<10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    my.run();
                }
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(my.count);
    }
}

測試輸出的結果一般是小於100000的,因爲count存在可見性和原子性的問題(指count++存在原子性問題),所以會出現以上結果;

以往的解決方案就是把run方法那一塊代碼加上鎖,就保證了線程安全;
**那麼只將count變量用volatile修飾能保證正確結果嗎?**答案是不會的,volatile只保證了可見性,並沒有保證原子性,所以結果同樣會小於100000;

1. 用原子類來解決簡單的原子性問題

對於上面的案例,就是一個簡單的原子性問題,此時我們可以藉助juc包下的原子類來解決,代碼如下:

class My {
    AtomicInteger count = new AtomicInteger(0);
    public void run() {
        for(int i=0; i<10000; i++) {
            count.getAndIncrement();
        }
    }
}
public class Test2 {
    public static void main(String[] args) throws Exception{
        My my = new My();
        for(int i=0; i<10; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    my.run();
                }
            }).start();
        }
        Thread.sleep(2000);
        System.out.println(my.count);  //一定是100000
    }
}

上面我們使用的AtomicInteger就是一個原子類(JDK5提供),原子類是無鎖方案實現的,所以上面的代碼比我們使用鎖的代碼的效率要高得多;具體體現在:

  • 互斥鎖方案爲了保證互斥性,需要執行加鎖、解鎖操作,而這兩個操作本身就消耗性能;同時拿不到鎖的線程還會進入阻塞狀態,進而觸發線程切換,線程切換也會帶來性能的消耗;所以相比之下,無鎖方案性能是遠遠高於它的;

2. 無鎖方案的實現原理

上面簡單使用了原子類,在保證線程安全的情況下還性能大大優於互斥鎖方案,那原子類到底是如何實現的呢?

原子類的實現原理其實就是CAS操作; ⭐ ⭐(CAS操作是樂觀鎖的一個實現方式!

  • CAS⽐較交換的過程可以通俗的理解爲CAS(V,O,N),包含三個值分別爲:V 內存地址存放的實際值;O預期的值(舊值);N 更新的新值;
    • 當V和O相同時,也就是說舊值和內存中實際的值相同表明該值沒有被其他線程更改過,即該舊值O就是⽬前來說最新的值了,⾃然⽽然可以將新值N賦值給V;
    • 反之,V和O不相同,表明該值已經被其他線程改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當多個線程使⽤CAS操作⼀個變量時,只有⼀個線程會成功,併成功更新,其餘會失敗。失敗的線程會重新嘗試,當然也可以選擇掛起線程;

下面通過一段代碼來進行模擬實現CAS操作:

class MyCAS {
    int count;
    synchronized int cas(int O, int N) {
        //獲取內存中真實的值
        int V = count;
        //如果期待的值與內存中的值一致,則代表期間沒有其他線程修改過這個值;
        if(O == V) {
            count = N;
        }
        //返回修改前的值
        return V;
    }
}

那麼獲取你會問,上面那個synchronized不就是鎖嗎?不是叫無鎖操作嗎?

  • 注意啦,那個鎖是我們硬件所提供支持的,也就是說每個CAS操作都是由硬件首先保證是互斥的,整個CAS操作是硬件提供的一組指令集,這個是保證原子性的,我們在使用其他的方法就是在調用這個cas方法的基礎上,比如上面的getAndIncrement()方法,這個方法肯定是無鎖的,但這個方法裏面使用了cas方法,相當於cas方法是系統提供的;
    CAS的實現需要硬件指令集的⽀撐,在JDK1.5後虛擬機纔可以使⽤處理器提供的CMPXCHG指令實現

3. 案例具體細節解釋

那麼上面展示了原子類的實現原理,到底是怎樣來保證線程安全的呢?

使用 CAS 來解決併發問題,一般都會伴隨着自旋,而所謂自旋,其實就是循環嘗試。例如,實現一個線程安全的count += 1操作,“CAS+ 自旋”的實現方案如下所示,首先計算 newValue = count+1,如果 cas(count,newValue) 返回的值不等於 count,則意味着線程在執行完代碼①處之後,執行代碼②處之前,count 的值被其他線程更新過。那此時該怎麼處理呢?可以採用自旋方案,就像下面代碼中展示的,可以重新讀 count 最新的值來計算 newValue 並嘗試再次更新,直到成功;

class MyCAS {
    int count;
    public void addOne() {
        int newValue;
        do {
            newValue = count + 1;  //①
        }while (count != 
                cas(count, newValue)  //②
               );
    }
    synchronized int cas(int O, int N) {
        //獲取內存中真實的值
        int V = count;
        //如果期待的值與內存中的值一致,則代表期間沒有其他線程修改過這個值;
        if(O == V) {
            count = N;
        }
        //返回修改前的值
        return V;
    }
}

上面如果有點繞的話,這裏來一波細節: ⭐⭐
假如有兩個線程A、B來同時執行addOne()方法,此時假設count爲5,此時A、B都執行到了while語句,括號裏面讀到的count都是5,接下來兩個線程都要去執行cas方法,由於這個方法是加鎖的,所以只能有一個線程能夠進入,假如A線程進入了,來到cas方法內部,讀取到V值爲5,接下來判斷o==v是成立的,所以將count更新爲了6,然後返回了5,此時線程A就將addOne方法執行結束了,而此時B才獲取到鎖,進入了cas方法內部,此時獲取到了V爲6,if語句不成立,返回6,此時線程B在while那判斷5 != 6,所以再次循環一次,操作跟上面一樣,最終count變爲7;

4. Java中實現CAS的源碼⭐

在上面的案例中,我們用AtomicInteger來實現了線程安全的count++操作,這個操作內部具體是怎麼實現的呢,下面來看看源碼:

	public final int getAndIncrement() {
	//this和valueoffset可以唯一確定共享變量的內存地址,參數1代表加一;
    	return unsafe.getAndAddInt(this, valueOffset, 1);
	}
	
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
        	//獲取當前內存中的值
            var5 = this.getIntVolatile(var1, var2);
            //傳入va1,var是爲了在下面這個CAS方法中再次獲取內存中的真實值,var5就是期待的值
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
        
//更新成功則返回true,條件是內存中的值等於期待的值,即上面傳進來的var5要等於待會在這個方法裏面根據算出的
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

上面這個模型跟3中總結的模型其實是一樣的,這裏的compareAndSwapInt其實就是CAS,內部操作就是比較期待的值和內存值是否一樣,一樣就更新內存,返回true,否則不更新內存,返回false,此時回到getAndIncrement中,繼續循環,此時新的循環會讀取最新內存中的值,繼續執行下去!!

2. 原子類概覽

在這裏插入圖片描述

在juc包下,有一個包爲atomic包,其下提供了很多豐富的原子類,我們可以將他們分爲五個類別:

  • 原子化的基本數據類型
  • 原子化的對象引用類型
  • 原子化數組
  • 原子化對象屬性更新器
  • 原子化的累加器

JDK從1.5開始提供了java.util.concurrent.atomic包;
作用:通過包中的原子操作類能夠線程安全地更新一個變量;
包含4種類型的原子更新方式:基本類型、數組、引用、對象中字段更新;

1. 原子化的基本數據類型

這一類包含的類有:(這些類都是juc.atomic包下的類

  • AtomicInteger
  • AtomicBoolean
  • AtomicLong

這三個類的實現和作用都很相似,來看看他們的方法就行了(他們提供的方法都大致一樣)

getAndIncrement() // 原子化 i++
getAndDecrement() // 原子化的 i--
incrementAndGet() // 原子化的 ++i
decrementAndGet() // 原子化的 --i
// 當前值 +=delta,返回 += 前的值
getAndAdd(delta) 
// 當前值 +=delta,返回 += 後的值
addAndGet(delta)
//CAS 操作,返回是否成功
compareAndSet(expect, update)
// 以下四個方法
// 新值可以通過傳入 func 函數來計算
getAndUpdate(func)
updateAndGet(func)
getAndAccumulate(x,func)
accumulateAndGet(x,func)

2. 原子化的對象引用類型

在juc包下的atomic包下有如下實現:

  • AtomicReference
  • AtomicMarkableReference
  • AtomicStampedReference

利用這幾個類可以實現對象引用的原子化更新;

對象引用的更新需要重要關注ABA問題,AtomicMarkableReferenceAtomicStampedReference 這兩個類可以解決ABA問題,其解決方案就是在每次更新中添加一個版本號;

具體使用可以看下面這個案例:

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "[name: " + this.name + ", age: " + this.age + "]";
    }
}
public class Test2 {
    // 普通引用
    private static Person person;

    public static void main(String[] args) throws InterruptedException {
        person = new Person("Tom", 18);

        System.out.println("Person is " + person.toString());

        Thread t1 = new Thread(new Task1());
        Thread t2 = new Thread(new Task2());

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Now Person is " + person.toString());
    }

    static class Task1 implements Runnable {
        public void run() {
            person.setAge(person.getAge() + 1);
            person.setName("Tom1");

            System.out.println("Thread1 Values :"
                    + person.toString());
        }
    }

    static class Task2 implements Runnable {
        public void run() {
            person.setAge(person.getAge() + 2);
            person.setName("Tom2");

            System.out.println("Thread2 Values :"
                    + person.toString());
        }
    }
}

可能輸出:
Person is [name: Tom, age: 18]
Thread2 Values [name: Tom1, age: 21]
Thread1 Values [name: Tom1, age: 21]
Now Person is [name: Tom1, age: 21]

假如我們使用了原子類的對象引用,就不會出現上述情況:

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String toString() {
        return "[name: " + this.name + ", age: " + this.age + "]";
    }
}
public class Test2 {
    // 原子引用
    private static AtomicReference<Person> personAtomicReference;
    public static void main(String[] args) throws InterruptedException {
        Person person = new Person("Tom", 18);
        personAtomicReference = new AtomicReference<Person>(person);

        System.out.println("Person is " + person.toString());

        Thread t1 = new Thread(new Task1());
        Thread t2 = new Thread(new Task2());

        t1.start();
        t2.start();

        t1.join();
        t2.join();

        System.out.println("Now Person is " + person.toString());
    }

    static class Task1 implements Runnable {
        public void run() {
            personAtomicReference.getAndSet(new Person("Tom1", personAtomicReference.get().getAge() + 1));

            System.out.println("Thread1 Atomic References "
                    + personAtomicReference.get().toString());
        }
    }

    static class Task2 implements Runnable {
        public void run() {
            personAtomicReference.getAndSet(new Person("Tom2", personAtomicReference.get().getAge() + 2));

            System.out.println("Thread2 Atomic References "
                    + personAtomicReference.get().toString());
        }
    }
}

輸出:
Person is [name: Tom, age: 18]
Thread1 Atomic References [name: Tom1, age: 19]
Thread2 Atomic References [name: Tom2, age: 21]
Now Person is [name: Tom, age: 18]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章