并发编程-04线程安全性之原子性Atomic包的4种类型详解
文章目錄
- 線程安全性文章索引
- 腦圖
- 概述
- 原子更新基本類型
- Demo
- AtomicBoolean 場景舉例
- 原子更新數組
- Demo
- 原子更新引用類型
- Demo
- 原子更新字段類型
- 使用注意事項:
- Demo
- 原子類線程安全的原理
- 代碼
線程安全性文章索引
并發編程-03線程安全性之原子性(Atomic包)及原理分析
并發編程-04線程安全性之原子性Atomic包的4種類型詳解
并發編程-05線程安全性之原子性【鎖之synchronized】
并發編程-06線程安全性之可見性 (synchronized + volatile)
并發編程-07線程安全性之有序性
腦圖
概述
在實際應用中,當我們更新一個變量時,在并發環境下,如果多個線程同時去更新這個變量,更新后的值可能不是我們期望的值。
如果理解Java內存模型 JMM的原理的話,上面這個結論就很容易理解了。可參考下前面寫的這篇文章
并發編程-02并發基礎CPU多級緩存和Java內存模型JMM
舉個例子:
【多線程場景】假設有個變量a在主內存中的初始值為1,線程A和線程B同時從主內存中獲取到了a的值,線程A更新a+1,線程B也更新a+1,經過線程AB更新之后可能a不等于3,而是等于2。因為A和B線程在更新變量a的時候從主內存中拿到的a都是1,而不是等A更新完刷新到主內存后,線程B再從主內存中取a的值去更新a,所以這就是線程不安全的更新操作.
解決辦法
- 使用鎖 1. 使用synchronized關鍵字synchronized會保證同一時刻只有一個線程去更新變量. 2、Lock接口【篇幅原因先不討論synchronized和lock,另開篇介紹】
- 使用JDK1.5開始提供的java.util.concurrent.atomic包,該包中的原子操作類提供了一種用法簡單、性能高效、線程安全地更新一個變量的方式。
這里我們使用的JDK版本JDK8
原子更新基本類型
使用原子的方式更新基本類型,Atomic包提供了以下3個類。
- AtomicBoolean:原子更新布爾類型
- AtomicInteger:原子更新整型
- AtomicLong:原子更新長整型
以AtomicInteger為例看下JDK8源碼中提供的方法
列舉幾個常用的方法:
- int addAndGet(int delta):以原子方式將輸入的數值與實例中的值(AtomicInteger里的 value)相加,并返回結果
- boolean compareAndSet(int expect,int update):如果輸入的數值等于預期值,則以原子方式將該值設置為輸入的值
- int getAndIncrement():以原子方式將當前值加1,注意,這里返回的是自增前的值
- int incrementAndGet():以原子方式將當前值加1,注意,這里返回的是自增后的值
- void lazySet(int newValue):最終會設置成newValue,使用lazySet設置值后,可能導致其他線程在之后的一小段時間內還是可以讀到舊的值
Demo
package com.artisan.example.atomic;import java.util.concurrent.atomic.AtomicInteger;import lombok.extern.slf4j.Slf4j;@Slf4j public class AtomicIntegerDemo {// 實例化一個初始值為5的AtomicIntegerprivate static AtomicInteger value = new AtomicInteger(5);public static void main(String[] args) {// 以原子方式將輸入的數值與value 相加,并返回結果 log.info("value的值:{}" ,value.addAndGet(3));// 獲取value的值log.info("value的值:{}",value.get());// 如果輸入的數值等于預期值,則以原子方式將該值設置為輸入的值log.info("執行結果:{}" ,value.compareAndSet(8, 11)); // 因為經過了addAndGet,操作之前value的值為8,這里會將value更新成11,返回truelog.info("執行結果:{}" ,value.compareAndSet(5, 11)); // 因為經過了addAndGet,操作之前value的值為8,并不等于5,因此不會更新為11,返回falselog.info("value的值:{}" ,value.get());log.info("value 增長前的值:{}",value.getAndIncrement());log.info("value的值:{}" ,value.get());log.info("value 增長后的值:{}",value.incrementAndGet());log.info("value的值:{}" ,value.get());// 最終會設置成newValue,使用lazySet設置值后,可能導致其他線程在之后的一小段時間內還是可以讀到舊的值value.lazySet(99);log.info("lazyset value: {}",value.get());} }執行結果:
AtomicBoolean 場景舉例
假設流程中的某個方法只能執行一次初始化操作, 我們可以設置個flag, 使用AtomicBoolean去更新flag的值,執行方法前調用compareAndSet方法來判斷如果該值為flase,更新為true,并執行該方法。 因為是線程安全的,所以后續的訪問flag均為true,不滿足if條件,所以均不會執行該方法。
package com.artisan.example.atomic;import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicBoolean;import lombok.extern.slf4j.Slf4j;@Slf4j public class AtomicBooleanDemo {//總請求數private static int clientTotal = 5000;// 同一時間允許執行的線程數private static int threadTotal = 200;// 標識 ,使用原子包中的AtomicBoolean 初始化為falseprivate static AtomicBoolean atomicBooleanFlag = new AtomicBoolean(false);public static void main(String[] args) throws Exception {// 線程池ExecutorService executorService = Executors.newCachedThreadPool();// 信號量 同一時間允許threadTotal個請求同時執行 即初始化threadTotal個信號量Semaphore semaphore = new Semaphore(threadTotal);//定義clientTotal個線程需要執行完,主線程才能繼續執行CountDownLatch countDownLatch = new CountDownLatch(clientTotal);// 循環for (int i = 0; i < clientTotal; i++) {executorService.execute(() ->{try {semaphore.acquire();doSomething();semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}// 減一countDownLatch.countDown();});}// 當全部線程都調用了countDown方法,count的值等于0,然后主線程就能通過await()方法,恢復執行自己的任務。countDownLatch.await();// 關閉線程池executorService.shutdown();log.info("flag:{}" ,atomicBooleanFlag.get());}private static void doSomething() {// 如果flag為flase就將其設置為trueif (atomicBooleanFlag.compareAndSet(false, true)) {log.info("doSomething executed");}}}輸出
原子更新數組
通過原子的方式更新數組里的某個元素,Atomic包提供了以下3個類。
- AtomicIntegerArray : 原子更新整型數組里的元素
- AtomicLongArray : 原子更新長整型數組里的元素
- AtomicReferenceArray : 原子更新引用類型數組里的元素
我們以AtomicIntegerArray為例來演示下用法
常用方法
- int addAndGet(int i,int delta):以原子方式將輸入值與數組中索引i的元素相加
- boolean compareAndSet(int i,int expect,int update):如果當前值等于預期值,則以原子方式將數組位置i的元素設置成update值
Demo
package com.artisan.example.atomic;import java.util.concurrent.atomic.AtomicIntegerArray;import lombok.extern.slf4j.Slf4j;@Slf4j public class AtomicIntegerArrayDemo {private static int[] array = new int[] { 11, 22 };private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(array);public static void main(String[] args) {// 給array[0]的值增加33 ,然后返回增長后的值 44log.info("atomicIntegerArray addAndGet :{}", atomicIntegerArray.addAndGet(0, 33)); // 44// 輸出 atomicIntegerArray中log.info("atomicIntegerArray get :{}", atomicIntegerArray.get(0)); // 44// 數組value通過構造方法傳遞進去,然后AtomicIntegerArray會將當前數組 復制一份,// 所以當AtomicIntegerArray對內部的數組元素進行修改時,不會影響傳入的數組log.info("array[0]的值: {}", array[0]); // 11// 先get 然后再add ,返回add之前的值log.info("atomicIntegerArray getAndAdd :{}", atomicIntegerArray.getAndAdd(0, 33)); // 44log.info("atomicIntegerArray .get(0) :{}", atomicIntegerArray.get(0)); // 44+33log.info("array[0]的值: {}", array[0]); // 11// 先get ,然后再set,返回set之前的數據log.info("atomicIntegerArray getAndSet :{}", atomicIntegerArray.getAndSet(0, 33));// 77log.info("atomicIntegerArray get(0) :{}", atomicIntegerArray.get(0)); // 33log.info("array[0] :{}", array[0]); // 11 }}數組value通過構造方法傳遞進去,然后AtomicIntegerArray會將當前數組復制一份,所以當AtomicIntegerArray對內部的數組元素進行修改時,不會影響傳入的數組
原子更新引用類型
原子更新基本類型的AtomicInteger,只能更新一個變量,如果要原子更新多個變量,就需要使用這個原子更新引用類型提供的類.
Atomic包提供了以下3個類
- AtomicReference:原子更新引用類型
- AtomicReferenceFieldUpdater:原子更新引用類型里的字段
- AtomicMarkableReference:原子更新帶有標記位的引用類型。可以原子更新一個布爾類型的標記位和引用類型
以AtomicReference為例,來看下用法
Demo
package com.artisan.example.atomic;import java.util.concurrent.atomic.AtomicReference;import lombok.extern.slf4j.Slf4j;@Slf4j public class AtomicReferenceDemo {private static AtomicReference<Artisan> atomicReferenceArtisan = new AtomicReference<Artisan>();public static void main(String[] args) {Artisan expectedArtisan = new Artisan();expectedArtisan.setName("小工匠");expectedArtisan.setAge(20);// 將expectedArtisan設置到atomicReferenceArtisanatomicReferenceArtisan.set(expectedArtisan);Artisan updateArtisan = new Artisan();updateArtisan.setName("小工匠Update");updateArtisan.setAge(99);// compareAndSet方法進行原子更新操作,如果是expectedArtisan,則更新為updateArtisanboolean mark2 = atomicReferenceArtisan.compareAndSet(new Artisan(), updateArtisan);log.info("更新是否成功:{}", mark2);// falseboolean mark = atomicReferenceArtisan.compareAndSet(expectedArtisan, updateArtisan);log.info("更新是否成功:{}", mark); // truelog.info("atomicReferenceArtisan name:{}", atomicReferenceArtisan.get().getName());log.info("atomicReferenceArtisan age:{}", atomicReferenceArtisan.get().getAge());}}結果
原子更新字段類型
如果需原子地更新某個類里的某個字段時,就需要使用原子更新字段類.
Atomic包提供 了以下3個類進行原子字段更新
- AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
- AtomicLongFieldUpdater:原子更新長整型字段的更新器。
- AtomicStampedReference:原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用于原子的更新數據和數據的版本號,可以解決使用CAS進行原子更新時可能出現的ABA問題。
使用注意事項:
Demo
package com.artisan.example.atomic;import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;import lombok.extern.slf4j.Slf4j;@Slf4j public class AtomicIntegerFieldUpdaterDemo {// 創建原子更新器,并設置需要更新的對象類和對象的屬性private static AtomicIntegerFieldUpdater<Artisan2> atomicIntegerFieldUpdaterArtisan = AtomicIntegerFieldUpdater.newUpdater(Artisan2.class, "age");public static void main(String[] args) {Artisan2 artisan = new Artisan2();artisan.setAge(20);log.info("目前atomicIntegerFieldUpdaterArtisan中age的值為:{}",atomicIntegerFieldUpdaterArtisan.get(artisan));if (atomicIntegerFieldUpdaterArtisan.compareAndSet(artisan, 20, 99)) {log.info("artisan的age屬性的值期望為20,則將20更新為99,更新成功");}// 獲取artisan更新后的age的值log.info("目前atomicIntegerFieldUpdaterArtisan中age的值為:{}",atomicIntegerFieldUpdaterArtisan.get(artisan));if (atomicIntegerFieldUpdaterArtisan.compareAndSet(artisan, 20, 99)) {log.info("artisan的age屬性的值期望為20,則將20更新為99,更新成功");}else {log.info("artisan的age屬性的值期望為20,則將20更新為99,更新失敗. 目前age的值為:{}",atomicIntegerFieldUpdaterArtisan.get(artisan));}} } package com.artisan.example.atomic;import lombok.Data;@Data public class Artisan2 {String name;public volatile int age; }結果:
原子類線程安全的原理
請參考上篇博客,主要是UnSafe 以及compareAndSwapInt ( CAS). 【JDK8和8以前的實現方式略有不同】
https://blog.csdn.net/yangshangwei/article/details/87489745#incrementAndGetUnSafe__compareAndSwapInt_CAS_54
代碼
https://github.com/yangshangwei/ConcurrencyMaster
總結
以上是生活随笔為你收集整理的并发编程-04线程安全性之原子性Atomic包的4种类型详解的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 并发编程-03线程安全性之原子性(Ato
- 下一篇: 并发编程-05线程安全性之原子性【锁之s