JUC併發編程之:簡單概述(四)

JUC併發編程之:簡單概述(四)

##本章內容:
無鎖併發--樂觀鎖(非阻塞)

·CAS與volatile
·原子整數
·原子引用
·原子數組
·字段更新器
·原子累加器
·Unsafe

一、CAS與volatile

1、保護共享資源

·有一個賬戶,有1萬元,現在有1000個線程,每個線程每次取10元,最後會剩下0元

##錯誤實現 與  加鎖實現
@Slf4j
public class CasAndVolatile01 {

    public static void main(String[] args) {

        //unsafe實現
        Account account1 = new AccountUnsafe(10000);
        Account.demo(account1);

        //加鎖實現
        Account account2 = new AccountLock(10000);
        Account.demo(account2);


    }
}

//加鎖實現
class AccountLock implements Account{

    Integer balance;

    public AccountLock(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        synchronized (this){
            return this.balance;
        }
    }

    @Override
    public void withDraw(Integer amount) {
        synchronized (this){
            this.balance -= amount;
        }
    }
}

//錯誤實現
class AccountUnsafe implements Account{

    Integer balance;

    public AccountUnsafe(Integer balance) {
        this.balance = balance;
    }

    @Override
    public Integer getBalance() {
        return this.balance;
    }

    @Override
    public void withDraw(Integer amount) {
        this.balance -= amount;
    }
}

interface Account{

    //獲取餘額
    Integer getBalance();

    //取款
    void withDraw(Integer amount);

    //方法內啓動1000個線程,每個線程-10元
    //如果總額爲10000,那麼餘額將會爲0
    static void demo(Account account){
        List<Thread> ts = new ArrayList<>();
        for(int i=0;i<1000;i++){
            ts.add(new Thread(()->{
                account.withDraw(10);
            }));
        }
        //計算起始時間
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account.getBalance()
        +"  cost : "+(end-start)/1000_000+"ms");

    }
}
##結果:
310  cost : 80ms  ##錯誤實現
0  cost : 69ms  ##加鎖實現

##無鎖實現
//無鎖實現
class AccountCas implements Account{

    AtomicInteger balance;

    public AccountCas(Integer balance) {
        this.balance = new AtomicInteger(balance);
    }

    @Override
    public Integer getBalance() {
        return balance.get(); //底層是volatile
    }

    @Override
    public void withDraw(Integer amount) {
        while(true){
            //餘額最新值
            Integer prev = balance.get();
            //修改後的餘額
            Integer next = prev - amount;
            //同步到主存--比較並設置
            if(balance.compareAndSet(prev,next)){
                break;
            }
        }
    }
}
##結果:
160  cost : 85ms ##錯誤實現
0  cost : 73ms   ##加鎖實現
0  cost : 65ms   ##無鎖實現

2、CAS與volatile

CAS:

·上面AtomicInteger的解決方法,內部並沒有使用鎖來保護共享變量的線程安全,它的實現原理:

@Override
public void withDraw(Integer amount) {
    while(true){
        //餘額最新值
        Integer prev = balance.get();
        //修改後的餘額
        Integer next = prev - amount;
        //同步到主存---比較並設置
        if(balance.compareAndSet(prev,next)){
        	break;
        }
    }
}

其中compareAndSet,它的簡稱就是CAS(也稱 compare and swap),它必須是原子操作

·CAS底層是lock cmpxchg指令(X86架構),在單個CPU和多核CPU下都能夠保證【比較-交換】的原子
性

·在多核狀態下,某個執行到帶lock的指令時,CPU會讓總線鎖住,當這個核把此指令執行完畢,再開啓
總線,這個過程中不會被線程的調度機制所打斷,保證了多個線程對內存操作的準確性,是原子的。

【CAS會在修改前判斷 自己共享變量修改之前讀到的數據和當前的數據是否一致,如果不一致則返回
·false,重新修改,如果一致則返回true修改成功】

volatile:

·獲取共享變量時,爲了保證該變量的可見性,需要使用volatile修飾。

·它可以用來修飾成員變量和靜態成員變量,它可以避免線程從自己的工作緩存中查找變量的值,必須到
主存中獲取他的值,線程操作volatile變量都是直接操作主存。即一個線程對volatile變量的修改,
對另一個線程可見。

##注意:
volatile僅保證了共享變量的可見性,讓其他線程能夠看到最新值,但不能解決指令交錯問題
(不能保證原子性)

·【CAS必須藉助volatile才能讀取到共享變量的最新值來實現【比較並交換】的效果】

##AtomicInteger部分源碼:

public class AtomicInteger{
  private volatile int value;
}

3、CAS效率分析

##爲什麼無鎖效率高?

·無鎖情況下,即使重試失敗,線程始終在高速運行,沒有停歇,而synchronized會讓線程在沒有獲得
鎖的時候,發生上下文切換,進入阻塞
(打個比方:線程就好像高速跑道上的賽車,高速運行時,速度超快,一旦發生上下文切換,就好比賽車減
速,熄火,等被喚醒又得重新打火、啓動、加速恢復到高速運行,代價比較大)

·但無鎖情況下因爲線程要保持運行,需要額外CPU的支持,CPU在這裏就好比高速跑道,沒有額外的跑
道,線程想高速運行也無從談起,雖然不會進入阻塞,但由於沒有分到時間片,仍然會進入可運行狀態,
還是會導致上下文切換。

4、CAS特點

·CAS結合volatile可以實現無鎖併發,適用於線程數少,多核CPU的場景下:

>CAS是基於樂觀鎖的思想:最樂觀的估計,不怕別的線程來修改共享變量,就算改了也沒關係,我喫點虧
再重試唄

>synchronized是基於悲觀鎖的思想:最悲觀的估計,得防着其他線程來修改共享變量,我上了鎖你們
都別想改,我改完了解開鎖,你們纔有機會

>CAS體現的是無鎖併發,無阻塞併發:
  >>因爲沒有使用synchronized,所以縣城不會陷入阻塞,這是效率提升的因素之一
  >>但如果競爭激烈,可以想到重試必然發生,反而會影響效率

二、原子整數

##JUC併發包提供了一些供CAS實現工具類:
·AtomicBoolean
·AtomicInteger
·AtomicLong

##ActomicInteger常用方法:
@Slf4j
public class ActomicIntegerTest {
    public static void main(String[] args) {
        AtomicInteger i = new AtomicInteger(0);

        System.out.println(i.incrementAndGet());//++i
        System.out.println(i.getAndIncrement());//i++

        System.out.println(i.addAndGet(5));//先+5然後get
        System.out.println(i.getAndAdd(5));//先get然後+5

        System.out.println(i.updateAndGet(x->x*5));//先乘以5後get
        System.out.println(i.getAndUpdate(x->x*5));//先get然後乘以5

        System.out.println(i.get());
    }
}

updateAndGet( )底層源碼:

public final int updateAndGet(IntUnaryOperator updateFunction) {
    int prev, next;
    do {
        prev = get();
        next = updateFunction.applyAsInt(prev);
    } while (!compareAndSet(prev, next));
    return next;
}

三、原子引用

##由於我們的共享變量並不一定都是基本數據類型,比如說BigDecimal,我們就可以使用原子引用
保證該共享變量的線程安全

·AtomicReference
·AtomicMarkableReference
·AtomicStampedReference  有版本號的

3.1、AtomicReference

public class CasAndVolatile02 {

    public static void main(String[] args) {
        DecimalAccount account = new BigDecimalAccountCas(new BigDecimal("10000"));
        DecimalAccount.demo(account);
    }
}

class BigDecimalAccountCas implements DecimalAccount{

    private AtomicReference<BigDecimal> balance;

    public BigDecimalAccountCas(BigDecimal balance) {
        this.balance = new AtomicReference<>(balance);
    }

    @Override
    public BigDecimal getBalance() {
        return balance.get();
    }

    @Override
    public void withDraw(BigDecimal amount) {
        while (true){
            BigDecimal prev = balance.get();
            BigDecimal next = prev.subtract(amount);
            if(balance.compareAndSet(prev,next)){
                break;
            }
        }
    }
}

interface DecimalAccount{

    //獲取餘額
    BigDecimal getBalance();

    //取款
    void withDraw(BigDecimal amount);

    //方法內啓動1000個線程,每個線程-10元
    //如果總額爲10000,那麼餘額將會爲0
    static void demo(DecimalAccount account2){
        List<Thread> ts = new ArrayList<>();
        for(int i=0;i<1000;i++){
            ts.add(new Thread(()->{
                account2.withDraw(BigDecimal.TEN);
            }));
        }
        //計算起始時間
        long start = System.nanoTime();
        ts.forEach(Thread::start);
        ts.forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(account2.getBalance()
                +"  cost : "+(end-start)/1000_000+"ms");

    }
}

3.2、ABA問題:

##主線程將共享變量從A---修改成---->C
##但在主線程修改過程中t線程將共享變量從A---修改成---->B---又修改成--->A
##主線程還是會修改成功,但主線程是無法感知共享變量的變化的

##代碼如下:
@Slf4j
public class AtomicReferenceABA01 {

    static AtomicReference<String> at = new AtomicReference("A");
    public static void main(String[] args) {
        log.debug("main start...");
        String prev = at.get();
        other();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("main change A-->C : {}",at.compareAndSet(prev,"C"));
    }

    private static void other(){
        new Thread(()->{
            log.debug("t1 change A-->B : {}",at.compareAndSet("A","B"));
        },"t1").start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(()->{
            log.debug("t1 change B-->A : {}",at.compareAndSet("B","A"));
        },"t2").start();
    }
}
##結果:
10:06:54.024 [main] DEBUG xxx - main start...
10:06:54.119 [t1] DEBUG xxx - t1 change A-->B : true
10:06:54.620 [t2] DEBUG xxx - t1 change B-->A : true
10:06:55.634 [main] DEBUG xxx - main change A-->C : true

##如何才能讓main線程感知到 共享變量被修改呢?
##只要有其他線程動過了共享變量,那麼自己的CAS就算失敗,這時僅比較值是不夠的,
需要再增加一個版本號

3.3、AtomicStampedReference

@Slf4j
public class AtomicStampedReferenceABA02 {

    static AtomicStampedReference<String> asr =new AtomicStampedReference<>("A",0);
    public static void main(String[] args) {
        log.debug("main start....");
        String prev = asr.getReference();
        int stamp = asr.getStamp();
        log.debug("main prev:{} , stamp:{}",prev,stamp);
        other();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.debug("main change A-->C : {}",asr.compareAndSet(prev,"C",stamp,stamp+1));
    }

    private static void other(){
        new Thread(()->{
            String prev = asr.getReference();
            int stamp = asr.getStamp();
            log.debug("t1 prev:{} , stamp:{}",prev,stamp);
            log.debug("t1 change A-->B : {}",asr.compareAndSet(prev,"B",stamp,stamp+1));
        },"t1").start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        new Thread(()->{
            String prev = asr.getReference();
            int stamp = asr.getStamp();
            log.debug("t2 prev:{} , stamp:{}",prev,stamp);
            log.debug("t2 change B-->A : {}",asr.compareAndSet(prev,"A",stamp,stamp+1));
        },"t2").start();
    }
}
##結果:
10:25:43.158 [main] DEBUG xxx - main start....
10:25:43.172 [main] DEBUG xxx - main prev:A , stamp:0
10:25:43.217 [t1] DEBUG xxx - t1 prev:A , stamp:0
10:25:43.217 [t1] DEBUG xxx - t1 change A-->B : true
10:25:44.227 [t2] DEBUG xxx - t2 prev:B , stamp:1
10:25:44.227 [t2] DEBUG xxx - t2 change B-->A : true
10:25:45.242 [main] DEBUG xxx - main change A-->C : false

3.4、AtomicMarkableReference

##AtomicStampedReference 通過版本號 可以記錄 是否被修改了,和修改了幾次
##AtomicMarkableReference 通過boolean標記 是否被修改
@Slf4j
public class AtomicMarkableReferenceTest {

    public static void main(String[] args) {
        GarbageBag bag = new GarbageBag("一個滿了的垃圾袋");
        AtomicMarkableReference<GarbageBag> amr = new AtomicMarkableReference<>(bag,true);
        log.debug("main start...");
        GarbageBag prev = amr.getReference();
        log.debug("main prev:{}",prev);

        new Thread(()->{
            log.debug("清潔阿姨 更換垃圾袋: {}",amr.compareAndSet(prev,new GarbageBag("清潔阿姨放了一個新的垃圾袋"),true,false));
        },"清潔阿姨").start();


        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        log.debug("main 更換垃圾袋: {}",amr.compareAndSet(prev,new GarbageBag("main放了一個新的垃圾袋"),true,false));

    }

}

@Data
@ToString
@AllArgsConstructor
class GarbageBag{
    private String desc;
}
##結果:
10:41:53.070 [main] DEBUG xxx - main start...
10:41:53.086 [main] DEBUG xxx - main prev:GarbageBag(desc=一個滿了的垃圾袋)
10:41:53.150 [清潔阿姨] DEBUG xxx - 清潔阿姨 更換垃圾袋: true
10:41:54.149 [main] DEBUG xxx - main 更換垃圾袋: false

四、原子數組

·AtomicIntegerArray
·AtomicLongArray
·AtomicReferenceArray

##原子數組可以在多線程中保護數組裏的元素
@Slf4j
public class AtomicIntegerArrayTest {

    public static void main(String[] args) {
        //創建了容量爲10個int的AtomicIntegerArray
        AtomicIntegerArray array = new AtomicIntegerArray(10);
        log.debug("第2個值:{}",array.get(1));


        int[] array2 = new int[]{1,2,3,4,5,6,7,8,9,10};
        AtomicIntegerArray array3 = new AtomicIntegerArray(array2);
        log.debug("第2個值:{}",array3.get(1));
        log.debug("第2個值+1:{}",array3.getAndIncrement(1));
        log.debug("第2個值:{}",array3.get(1));
        log.debug("第3個值+5:{}",array3.getAndAdd(2,5));
        log.debug("第3個值:{}",array3.get(2));

        log.debug("第4個值*7:{}",array3.getAndUpdate(3,t->t*7));
        log.debug("第3個值:{}",array3.get(3));
        
        log.debug("修改第5個值爲999");
        array3.set(4,999);
        log.debug("第5個值:{}",array3.get(4));
    }
}
##結果:
11:45:37.301 [main] DEBUG xxx - 第2個值:0
11:45:37.316 [main] DEBUG xxx - 第2個值:2
11:45:37.317 [main] DEBUG xxx - 第2個值+1:2
11:45:37.317 [main] DEBUG xxx - 第2個值:3
11:45:37.317 [main] DEBUG xxx - 第3個值+5:3
11:45:37.317 [main] DEBUG xxx - 第3個值:8
11:45:37.361 [main] DEBUG xxx - 第4個值*7:4
11:45:37.361 [main] DEBUG xxx - 第3個值:28
11:47:45.114 [main] DEBUG xxx - 修改第5個值爲999
11:47:45.114 [main] DEBUG xxx - 第5個值:999

五、字段更新器

·AtomicReferenceFieldUpdater
·AtomicIntegerFieldUpdater
·AtomicLongFieldUpdater

##字段更新器可以在多線程中保護對象的某個屬性\成員變量
@Slf4j
public class AtomicReferenceFieldUpdaterTest {
    public static void main(String[] args) {
        Student st = new Student("李四");

        //參數:類,要更新字段的類型,要更新字段的名稱
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class,String.class,"name");

        log.debug("更新名稱爲張三:{}",updater.compareAndSet(st,"李四","張三"));
        log.debug("st : {}",st);
    }
}

@Data
@ToString
@AllArgsConstructor
class Student{
    volatile String name;
}

六、原子累加器

##ActomicLong和LongAdder累加的性能比較
@Slf4j
public class LongAdderTest {
    public static void main(String[] args) {
        for (int i = 0; i <5 ; i++) {
            demo(
                    ()->new AtomicLong(0),
                    (adder)->adder.getAndIncrement()
            );
        }
        log.debug("-------------");
        for (int i = 0; i <5 ; i++) {
            demo(
                    ()->new LongAdder(),
                    (adder)->adder.increment()
            );
        }
    }

    private static <T> void demo(Supplier<T> supplier, Consumer<T> consumer){
        T adder = supplier.get();
        List<Thread> ts = new ArrayList<>();
        //4個線程,每人累加50萬
        for (int i=0;i<4;i++){
           ts.add( new Thread(()->{
                for(int j=0;j<500000;j++){
                    consumer.accept(adder);
                }
           }));
        }

        long start = System.nanoTime();
        ts.forEach(t->t.start());
        ts.forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        log.debug("adder {}, cost : {} ms",adder,(end-start)/1000_000);

    }
}
##結果:
14:57:20.170 [main] DEBUG xxx - adder 2000000, cost : 49 ms
14:57:20.228 [main] DEBUG xxx - adder 2000000, cost : 43 ms
14:57:20.265 [main] DEBUG xxx - adder 2000000, cost : 36 ms
14:57:20.303 [main] DEBUG xxx - adder 2000000, cost : 37 ms
14:57:20.344 [main] DEBUG xxx - adder 2000000, cost : 41 ms
14:57:20.344 [main] DEBUG xxx - -------------
14:57:20.359 [main] DEBUG xxx - adder 2000000, cost : 13 ms
14:57:20.364 [main] DEBUG xxx - adder 2000000, cost : 5 ms
14:57:20.370 [main] DEBUG xxx - adder 2000000, cost : 5 ms
14:57:20.377 [main] DEBUG xxx - adder 2000000, cost : 6 ms
14:57:20.382 [main] DEBUG xxx - adder 2000000, cost : 4 ms

##LongAdder性能提升的原因:

LongAdder性能提升的原因很簡單,就是在有競爭時,設置多個累加單元,Thread-0累加Cell[0]
而Thread-1累加Cell[1]...最後將結果彙總,這樣他們在累加時操作的不同的Cell變量,因此減
少了CAS重試失敗,從而提高性能

七、Unsafe

·Unsafe對象提供了非常底層的,操作內存、線程的方法
·Unsafe對象不能直接調用,只能通過反射獲得

獲取Unsafe:

@Slf4j
public class UnsafeAccessor {
    public static void main(String[] args) {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe) theUnsafe.get(null);
        log.debug("unsafe:{}",unsafe);
    }
}

Unsafe CAS操作:

/**
* 使用Unsafe線程安全的操作Teacher對象的成員變量
* (之前使用AtomicReferenceFieldUpdater)
*/
@Slf4j
public class UnsafeAccessor {

    public static void main(String[] args) {
        try {
            //獲取Unsafe
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            Unsafe unsafe = (Unsafe) theUnsafe.get(null);
            log.debug("unsafe:{}",unsafe);

            Teacher tc = new Teacher(1,"張三");
            //獲取域的偏移地址
            long idOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("id"));
            long nameOffset = unsafe.objectFieldOffset(Teacher.class.getDeclaredField("name"));

            //執行CAS操作
            unsafe.compareAndSwapInt(tc,idOffset,1,2);
            unsafe.compareAndSwapObject(tc,nameOffset,"張三","李四");

            //檢驗
            log.debug("tc : {}",tc);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
}

@Data
@ToString
@AllArgsConstructor
class Teacher{
    volatile int id;
    volatile String name;
}

Unsafe模擬原子整數:

@Slf4j
public class UnsafeForAtomicInteger {

    public static void main(String[] args) {
        MyAtomicInteger mat = new MyAtomicInteger(10000);

        List<Thread> ts = new ArrayList<>();
        //1000個線程,每個線程減去10
        for(int i=0;i<1000;i++){
            ts.add(new Thread(()->{
                mat.decrement(10);
            }));
        }

        ts.forEach(t->t.start());
        ts.forEach(t->{
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        log.debug("mat : {}",mat.get());
    }
}

class MyAtomicInteger{

    private volatile Integer value;
    private static final long valueOffset;
    private static final Unsafe UNSAFE;

    static{
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            UNSAFE = (Unsafe) theUnsafe.get(null);

            valueOffset = UNSAFE.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
            throw new RuntimeException();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
            throw new RuntimeException();
        }
    }

    public MyAtomicInteger(Integer value) {
        this.value = value;
    }

    public Integer get(){
        return value;
    }

    public void decrement(Integer num){
        while (true){
            Integer prev = this.value;
            Integer next = prev - num;
            if(UNSAFE.compareAndSwapObject(this,valueOffset,prev,next)){
                break;
            }
        }
    }
}

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