多線程6一CAS與自旋鎖
1、什么是CAS
CAS 即 compare and swap 比較并交換, 涉及到三個參數,內存值V, 預期值A, 要更新為的值B, 拿著預期值A與內存值V比較,相等則符合預期,將內存值V更新為B, 不相等,則不能更新V。
為什么預期值A與內存值V不一樣了呢?
在多線程環境下,對于臨界區的共享資源,所有線程都可以訪問修改,這時為了保證數據不會發生錯誤,通常會對訪問臨界區資源加鎖,同一時刻最多只能讓一個線程訪問(獨占模式下),這樣會讓線程到臨界區時串行執行,加鎖操作可能會導致并發性能降低,而循環CAS可以實現讓多個線程不加鎖去訪問共享資源,卻也可以保證數據正確性。 如 int share = 1,線程A獲取到share的值1,想要將其修改為2,這時線程B搶先修改share = 3了,線程A這時拿著share =1 預期值與實際內存中已經變為3的值比較, 不相等,cas失敗,這時就重新獲取最新的share再次更新,需要不斷循環,直到更新成功;這里可能會存在線程一直在進行循環cas,消耗cpu資源。
cas缺點:
1、存在ABA問題
2、循環cas, 可能會花費大量時間在循環,浪費cpu資源
3、只能更新一個值(也可解決,AtomicReference 原子引用類泛型可指定對象,實現一個對象中包含多個屬性值來解決只能更新一個值的問題)
2、原子類 Atomic
原子類在JUC的atomic包下提供了 AtomicInteger,AtomicBoolean, AtomicLong等基本數據類型原子類,還有可傳泛型的AtomicReference, 以及帶有版本號的 AtomicStampedReference , 可實現對象的原子更新, 其具體是怎樣保證在多線程環境下,不加鎖的情況也可以原子操作, 是其內部借助了Unsafe類,來保證更新的原子性。
類圖結構如下:
分別用AtomicInteger和 Integer 演示多個線程執行自增操作,是否能夠保證原子性,執行結果是否正確
代碼如下:
/**
* @author zdd
* 2019/12/22 10:47 上午
* Description: 演示AtomicInteger原子類原子操作
*/
public class CasAtomicIntegerTest {
static final Integer THREAD_NUMBER = 10;
static AtomicInteger atomicInteger = new AtomicInteger(0);
static volatile Integer integer = 0;
public static void main(String[] args) throws InterruptedException {
ThreadTask task = new ThreadTask();
Thread[] threads = new Thread[THREAD_NUMBER];
//1,開啟10個線程
for (int j = 0; j < THREAD_NUMBER; j++) {
Thread thread = new Thread(task);
threads[j]= thread;
}
for (Thread thread:threads) {
//開啟線程
thread.start();
//注: join 為了保證主線程在所有子線程執行完畢后再打印結果,否則主線程就阻塞等待
// thread.join();
}
// 主線程休眠5s, 等待所有子線程執行完畢再打印
TimeUnit.SECONDS.sleep(5);
System.out.println("執行完畢,atomicInteger的值為: "+ atomicInteger.get());
System.out.println("執行完畢,integer的值為 : "+ integer);
}
public static void safeIncr() {
atomicInteger.incrementAndGet();
}
public static void unSafeIncr() {
integer ++;
}
static class ThreadTask implements Runnable{
@Override
public void run() {
// 任務體,分別安全和非安全方式自增1000次
for (int i = 0; i < 1000; i++) {
safeIncr();
}
for (int i = 0; i < 1000; i++) {
unSafeIncr();
}
}
}
}
執行結果如下:
疑問: 上文代碼中注,我本想讓主線程調用每個子線程 join方法,保證主線程在所有子線程執行完畢之后再執行打印結果,然而這樣執行導致非安全的Integer自增結果也正確,猜想是在執行join方法,導致這10個子線程排隊有序在執行了? 因此注釋了該行代碼 ,改為讓主線程休眠幾秒來保證在子線程執行后再打印。
AtomicInteger如何保證原子性,AtomicInteger持有Unsafe對象,其大部分方法是本地方法,底層實現可保證原子操作。
public class AtomicInteger extends Number implements java.io.Serializable {
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
來看一下 AtomicInteger 的自增方法 incrementAndGet(),先自增,再返回增加后的值。
代碼如下:
public final int incrementAndGet() {
//調用unsafe的方法
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
繼續看unsafe如何實現
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
//1.獲取當前對象的內存中的值A
var5 = this.getIntVolatile(var1, var2);
//2. var1,var2聯合獲取內存中的值V,var5是期望中的值A, var5+var4 是將要更新為的新值
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
//3. 更新成功,跳出while循環,返回更新成功時內存中的值(可能下一刻就被其他線程修改)
return var5;
}
執行流程圖如下:
Unsafe 的compareAndSwapInt是本地方法,可原子地執行更新操作,更新成功返回true,否則false
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
3、CAS的ABA問題
什么是ABA問題?
例如 線程A獲取變量atomicInteger =100, 想要將其修改為2019 (此時還未修改), 這時線程B搶先進來將atomicInteger先修改為101,再修改回atomicInteger =100,這時線程A開始去更新atomicInteger的值了,此時預期值和內存值相等,更新成功atomicInteger =2019;但是線程A 并不知道這個值其實已經被人修改過了。
代碼演示如下:
/**
* zdd
* Description: cas的ABA問題
*/
public class CasTest1 {
// static AtomicInteger atomicInteger = new AtomicInteger(100);
/* 這里使用原子引用類,傳入Integer類型,
* 和AtomicInteger一樣,AtomicReference使用更靈活,泛型可指定任何引用類型。
* 也可用上面注釋代碼
*/
static AtomicReference<Integer> reference = new AtomicReference<>(100);
public static void main(String[] args) {
//1.開啟線程A
new Thread(()-> {
Integer expect = reference.get();
try {
//模擬執行任務,讓線程B搶先修改
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println( "執行3s任務后, 修改值是否成功 "+ reference.compareAndSet(expect,2019)+ " 當前值為: "+ reference.get());
},"A").start();
//2.開啟線程B
new Thread(()-> {
// expect1 =100
Integer expect1 = reference.get();
//1,先修改為101,再修改回100,產生ABA問題
reference.compareAndSet(expect1,101);
//expect2 =101
Integer expect2 = reference.get();
reference.compareAndSet(expect2, 100);
},"B").start();
}
}
執行結果如下:可見線程A修改成功
A 執行3s任務后, 修改值是否成功:true 當前值為: 2019
4、ABA問題的解決方式
解決CAS的ABA問題,是參照數據庫樂觀鎖,添加一個版本號,每更新一次,次數+1,就可解決ABA問題了。
AtomicStampedReference
/**
* zdd
* 2019/11/4 6:30 下午
* Description:
*/
public class CasTest1 {
//設置初始值和版本號
static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
//2,采用帶有版本號的
new Thread(()-> {
Integer expect = stampedReference.getReference();
int stamp = stampedReference.getStamp();
try {
//休眠3s,讓線程B執行完ABA操作
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
//此時 stamp=1,與實際版本號3不等,這里更新失敗就是stamp沒有獲取到最新的
System.out.println("是否修改成功: "+stampedReference.compareAndSet(expect, 101, stamp, stamp +1));
System.out.println("當前 stamp 值: " + stampedReference.getStamp()+ "當前 reference: " +stampedReference.getReference());
},"A").start();
new Thread(()-> {
Integer expect = stampedReference.getReference();
int stamp = stampedReference.getStamp();
try {
//休眠1s,讓線程A獲取都舊的值和版本號
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 1,100 -> 101, 版本號 1-> 2
stampedReference.compareAndSet(expect, 101 , stamp, stamp+1);
//2, 101 ->100, 版本號 2->3
Integer expect2 = stampedReference.getReference();
stampedReference.compareAndSet(expect2, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
},"B").start();
}
}
執行結果如下:
是否修改成功: false
當前 stamp 值: 3 當前 reference: 100
5、利用cas實現自旋鎖
package cas;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
/**
* @author zdd
* 2019/12/22 9:12 下午
* Description: 利用cas手動實現自旋鎖
*/
public class SpinLockTest {
static AtomicReference<Thread> atomicReference = new AtomicReference<>();
public static void main(String[] args) {
SpinLockTest spinLockTest = new SpinLockTest();
//測試使用自旋鎖,達到同步鎖一樣的效果 ,開啟2個子線程
new Thread(()-> {
spinLockTest.lock();
System.out.println(Thread.currentThread().getName()+" 開始執行,startTime: "+System.currentTimeMillis());
try {
//休眠3s
TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 結束執行,endTime: "+System.currentTimeMillis());
spinLockTest.unLock();
},"線程A").start();
new Thread(()-> {
spinLockTest.lock();
System.out.println(Thread.currentThread().getName()+" 開始執行,startTime: "+System.currentTimeMillis());
try {
//休眠3s
TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" 結束執行,endTime: "+System.currentTimeMillis());
spinLockTest.unLock();
},"線程B").start();
}
public static void lock() {
Thread currentThread = Thread.currentThread();
for (;;) {
boolean flag =atomicReference.compareAndSet(null,currentThread);
//cas更新成功,則跳出循環,否則一直輪詢
if(flag) {
break;
}
}
}
public static void unLock() {
Thread currentThread = Thread.currentThread();
Thread momeryThread = atomicReference.get();
//比較內存中線程對象與當前對象,不等拋出異常,防止未獲取到鎖的線程調用unlock
if(currentThread != momeryThread) {
throw new IllegalMonitorStateException();
}
//釋放鎖
atomicReference.compareAndSet(currentThread,null);
}
}
執行結果如下圖:
6、總結
通過全文,我們可以知道cas的概念,它的優缺點;原子類的使用,內部借助Unsafe類循環cas更新操作實現無鎖情況下保證原子更新操作,進一步我們能夠自己利用循環cas實現自旋鎖SpinLock,它與同步鎖如ReentrantLock等區別在于自旋鎖是在未獲取到鎖情況,一直在輪詢,線程時非阻塞的,對cpu資源占用大,適合查詢多修改少場景,并發性能高;同步鎖是未獲取到鎖,阻塞等待,兩者各有適用場景。
道阻且長,且歌且行!
每天一小步,踏踏實實走好腳下的路,文章為自己學習總結,不復制黏貼,就是想讓自己的知識沉淀一下,也希望與更多的人交流,如有錯誤,請批評指正!
智能推薦
多線程與高并發(三):基于CAS實現的各種鎖
共享數據遞增的場景實現: 1,synchronized同步的方式 2,原子類的方式 3,LongAdder的方式 那么問題是,他們的效率都是怎么樣的呢的? 經過一些代碼的測試發現同步方式<原子類操作<LongAdder操作 LongAdder的實現:內部是使用了分段鎖的概念: 其實就是先將數據存到一個數組中,將所有的線程使用分而治之的思想進行同時操作,最后將結果加一下. Reentra...
freemarker + ItextRender 根據模板生成PDF文件
1. 制作模板 2. 獲取模板,并將所獲取的數據加載生成html文件 2. 生成PDF文件 其中由兩個地方需要注意,都是關于獲取文件路徑的問題,由于項目部署的時候是打包成jar包形式,所以在開發過程中時直接安照傳統的獲取方法沒有一點文件,但是當打包后部署,總是出錯。于是參考網上文章,先將文件讀出來到項目的臨時目錄下,然后再按正常方式加載該臨時文件; 還有一個問題至今沒有解決,就是關于生成PDF文件...
電腦空間不夠了?教你一個小秒招快速清理 Docker 占用的磁盤空間!
Docker 很占用空間,每當我們運行容器、拉取鏡像、部署應用、構建自己的鏡像時,我們的磁盤空間會被大量占用。 如果你也被這個問題所困擾,咱們就一起看一下 Docker 是如何使用磁盤空間的,以及如何回收。 docker 占用的空間可以通過下面的命令查看: TYPE 列出了docker 使用磁盤的 4 種類型: Images:所有鏡像占用的空間,包括拉取下來的鏡像,和本地構建的。 Con...
requests實現全自動PPT模板
http://www.1ppt.com/moban/ 可以免費的下載PPT模板,當然如果要人工一個個下,還是挺麻煩的,我們可以利用requests輕松下載 訪問這個主頁,我們可以看到下面的樣式 點每一個PPT模板的圖片,我們可以進入到詳細的信息頁面,翻到下面,我們可以看到對應的下載地址 點擊這個下載的按鈕,我們便可以下載對應的PPT壓縮包 那我們就開始做吧 首先,查看網頁的源代碼,我們可以看到每一...
猜你喜歡
Linux C系統編程-線程互斥鎖(四)
互斥鎖 互斥鎖也是屬于線程之間處理同步互斥方式,有上鎖/解鎖兩種狀態。 互斥鎖函數接口 1)初始化互斥鎖 pthread_mutex_init() man 3 pthread_mutex_init (找不到的情況下首先 sudo apt-get install glibc-doc sudo apt-get install manpages-posix-dev) 動態初始化 int pthread_...
統計學習方法 - 樸素貝葉斯
引入問題:一機器在良好狀態生產合格產品幾率是 90%,在故障狀態生產合格產品幾率是 30%,機器良好的概率是 75%。若一日第一件產品是合格品,那么此日機器良好的概率是多少。 貝葉斯模型 生成模型與判別模型 判別模型,即要判斷這個東西到底是哪一類,也就是要求y,那就用給定的x去預測。 生成模型,是要生成一個模型,那就是誰根據什么生成了模型,誰就是類別y,根據的內容就是x 以上述例子,判斷一個生產出...
styled-components —— React 中的 CSS 最佳實踐
https://zhuanlan.zhihu.com/p/29344146 Styled-components 是目前 React 樣式方案中最受關注的一種,它既具備了 css-in-js 的模塊化與參數化優點,又完全使用CSS的書寫習慣,不會引起額外的學習成本。本文是 styled-components 作者之一 Max Stoiber 所寫,首先總結了前端組件化樣式中的最佳實踐原則,然后在此基...