java并發(fā)無鎖多線程單線程示例詳解
前言
在并發(fā)編程中,多線程的共享資源的修改往往會造成嚴(yán)重的線程安全問題,解決這種問題簡單暴力的方式就是加鎖,加鎖的方式使用簡單易理解,但常常會因為阻塞導(dǎo)致性能問題
有沒有可能做到無鎖還保證線程安全吶?這得看具體情況。得益于CAS技術(shù),有很多情況下我們可以做到不使用鎖也能保證線程的安全
比如今天我最近遇到的場景如下(由于場景比較復(fù)雜,用一個模擬簡化一下)
場景
假設(shè)有一個商店,背后有一個工廠可以生產(chǎn)商品,商店也可以有用戶來購買商品,為了簡化,假設(shè)工廠只能生產(chǎn)一個商品、而用戶也只能買一個商品
需求如下:
- 用戶來購買,如果商品已經(jīng)生產(chǎn)好了,則直接發(fā)貨,完成交易
- 用戶來購買,如果商品還沒生產(chǎn)好,讓用戶填寫一個欠貨單,待工廠生產(chǎn)好后,如果發(fā)現(xiàn)有欠貨,則直接發(fā)貨,完成交易
簡簡單單的一個需求,在多線程環(huán)境下就會出現(xiàn)隱患
單線程
先不考慮多線程情況,這個代碼很好寫,我們用一個ready
變量標(biāo)識是否生產(chǎn)完成,用一個unSupply
變量標(biāo)識是否有欠用戶一個商品,代碼如下
public class SerialShop { private volatile boolean ready; // 商品生產(chǎn)完成 private volatile boolean unSupply; // 是否欠用戶一個商品 public volatile boolean done; // 交易完成 public void send() { // 發(fā)貨 System.out.println("send to user"); done = true; } public void buy() { if (ready) { // 商品生產(chǎn)完成 send(); // 直接發(fā)貨 return; } this.unSupply = true; // 沒有準(zhǔn)備好則填寫一個欠貨單 } public void ready() { this.ready = true; // 標(biāo)識商品準(zhǔn)備完成 if (this.unSupply) { // 如果發(fā)現(xiàn)有欠貨單 send(); // 給用戶發(fā)貨 } } }
這時,我們簡單跑一下
@Test public void buyBeforeReady() { buy(); ready(); } @Test public void buyAfterReady() { ready(); buy(); }
結(jié)果無論先購買再生產(chǎn)完,還是生產(chǎn)完再購買,最終都會走到send
方法,完成交易
多線程
上面的代碼雖然簡單,但在多線程下就會出現(xiàn)問題,用實際的情形描述一下
- 用戶來購買發(fā)現(xiàn)商品沒生產(chǎn)好,則開始準(zhǔn)備填寫欠貨單,由于用戶文盲,填寫的很慢
- 此時工廠恰好生產(chǎn)好了,標(biāo)識已準(zhǔn)備,但一看還沒有欠貨單,所以不發(fā)貨
- 用戶剛剛填寫完欠貨單,沒啥事就回家了
- 最終,用戶付完了錢,工廠也生產(chǎn)完畢,就是沒有發(fā)貨完成交易
畫個時序圖描述一下這個情景
時序圖
因為多線程無法保證有序性,所以這種情況出現(xiàn)的概率很大,而一旦出現(xiàn)就是嚴(yán)重問題
用代碼模擬一下這個場景:
public class UnsafeShop { private volatile boolean ready; // 商品生產(chǎn)完成 private volatile boolean unSupply; // 欠用戶 public volatile boolean done; // 交易完成 public void send() { System.out.println("send to user"); done = true; } public void buy() throws InterruptedException { if (ready) { // 準(zhǔn)備好了 send(); // 直接發(fā)貨 return; } Thread.sleep(100); // 這里手動降低線程速度,為了重現(xiàn)場景 this.unSupply = true; // 沒有準(zhǔn)備好則填寫一個欠貨單 } public void ready() throws InterruptedException { this.ready = true; // 標(biāo)識商品準(zhǔn)備完成 if (this.unSupply) { // 如果發(fā)現(xiàn)有欠貨單 send(); // 給用戶發(fā)貨 } } @Test public void unsafe() throws InterruptedException { // 用戶購買 new Thread(() -> { try { buy(); } catch (InterruptedException e) { } }).start(); Thread.sleep(50); // 工廠生產(chǎn) new Thread(() -> { try { ready(); } catch (InterruptedException e) { } }).start(); while (true) ; } }
執(zhí)行結(jié)果:并沒有走到send方法(上面的代碼通過sleep來降低線程的執(zhí)行速度,是為了100%呈現(xiàn)錯誤,實際中就算不寫sleep也有可能出現(xiàn)這種情況)
悲觀鎖
那么如何避免上面的問題吶,最簡單暴力的方式就是加鎖
上面的問題之所以出現(xiàn),是因為用戶查看是否商品已準(zhǔn)備和標(biāo)識欠貨的兩步操作沒有原子性,導(dǎo)致中間的過程可能被工廠的線程快速完成所有動作和判斷
實際情形下我們可以這么解決問題:在接納用戶的時候,如果工廠來人送貨,讓工廠的人在外面等著,等用戶把該做的都做了,工廠的人再進(jìn)來標(biāo)識準(zhǔn)備完畢并送貨
用代碼模擬一下這個解決方案
public class BlockShop { private volatile boolean ready; // 商品生產(chǎn)完成 private volatile boolean unSupply; // 欠用戶 public volatile boolean done; // 交易完成 public void send() { System.out.println("send to user"); done = true; } public void buy() throws InterruptedException { synchronized (this) { // 接納用戶時不讓工廠人進(jìn)入 if (ready) { // 準(zhǔn)備好了 send(); // 直接發(fā)貨 return; } Thread.sleep(100); this.unSupply = true; // 沒有準(zhǔn)備好則填寫一個欠貨單 } } public void ready() throws InterruptedException { synchronized (this) { // 接納用戶時不讓工廠人進(jìn)入 this.ready = true; // 標(biāo)識商品準(zhǔn)備完成 } if (this.unSupply) { // 如果發(fā)現(xiàn)有欠貨單 send(); // 給用戶發(fā)貨 } } @Test public void block() throws InterruptedException { // 用戶購買 new Thread(() -> { try { buy(); } catch (InterruptedException e) { } }).start(); Thread.sleep(50); // 工廠生產(chǎn) new Thread(() -> { try { ready(); } catch (InterruptedException e) { } }).start(); while (true) ; } }
這時,不會在出現(xiàn)上述問題,徹底的解決了線程安全
而從解決問題的實際場景來看,這種解決問題的方法在現(xiàn)實中簡直是弱智,工廠的人就在外面傻等,這就是阻塞,會降低代碼的執(zhí)行速度
當(dāng)然以上場景的阻塞實際其實很小,但個人認(rèn)為鎖這東西能不用盡量不用,在場景復(fù)雜的時候阻塞的弱點會更加凸顯出來
無鎖
在這種場景下,能不能不使用鎖來達(dá)到線程安全的效果吶?
我想了很多辦法,比如buy時先標(biāo)識已欠貨,再去判斷是否已準(zhǔn)備,或者標(biāo)識完已欠貨再去看一眼是否已準(zhǔn)備好,但都行不通,原因就是無法保證原子性,也無法保證多線程的有序性
冥思苦想后,想到一個解決方案:工廠人員上門后第一件事就是把欠貨單撕了!
- 此時如果用戶正在寫這個欠貨單,那肯定是撕不成的,出現(xiàn)沖突說明用戶已來了,直接發(fā)貨即可
- 如果用戶還沒寫且正準(zhǔn)備寫,發(fā)現(xiàn)欠貨單沒了,出現(xiàn)沖突說明貨來了,直接發(fā)貨即可
此時欠貨單有三個狀態(tài):初始狀態(tài)/被撕了/填寫完,我們用商品的庫存標(biāo)識為:0/1/-1(欠用戶一臺)
private volatile int stock = 0;
而stock==1
也說明貨已到,所以不需要ready
變量了
最終代碼如下
public class NoBlockShop { private volatile int stock = 0; // 庫存量 -1代表虧欠用戶一臺 public volatile boolean done; // 交易完成 final AtomicIntegerFieldUpdater<NoBlockShop> STATUS_UPDATER = AtomicIntegerFieldUpdater.newUpdater(NoBlockShop.class, "stock"); public void send() { done = true; } public void buy() throws InterruptedException { for (;;) { if (stock ==1) { // 有貨 send(); // 直接發(fā)貨 return; } if (STATUS_UPDATER.compareAndSet(this, 0, -1)) {// 標(biāo)識欠貨,如果失敗說明庫存有變動,再回頭查看一下 return; } } } public void ready() throws InterruptedException { if (!STATUS_UPDATER.compareAndSet(this, 0, 1)) { // 標(biāo)識有庫存 send(); // 如果失敗代表用戶來過了,直接發(fā)貨 } } }
不僅解決了線程安全,還無鎖(也可以稱作樂觀鎖),并且代碼還簡潔了,CAS是真香
測試一下線程安全,代碼如下
ExecutorService executorService = Executors.newFixedThreadPool(20); List<NoBlockShop> shops = new ArrayList<>(); for (int i=0;i<100000;i++) { NoBlockShop shop = new NoBlockShop(); shops.add(shop); executorService.execute(()->{ try { shop.buy(); } catch (InterruptedException e) {} }); executorService.execute(()->{ try { shop.ready(); } catch (InterruptedException e) {} }); } Thread.sleep(500); System.out.println(shops.stream().filter(v->!v.done).count());
初始化10萬個shop,然后用不同線程分別buy和ready,最終輸出沒交易的shop個數(shù)
- 如果使用
UnsafeShop
(初版),一般結(jié)果都不是0,且每次執(zhí)行都不一樣,說明有的shop對象出現(xiàn)線程安全問題 - 如果使用
BlockShop
(鎖版),結(jié)果是0,說明線程安全 - 如果使用
NoBlockShop
(CAS版),結(jié)果是0,說明也實現(xiàn)了線程安全
根據(jù)這個可以繼續(xù)改造一下讓商店,讓工廠可以不斷生產(chǎn)商品,用戶也能不斷購買,依然使用stock
,為正代表有n個庫存,為負(fù)代表欠用戶n個商品,并且可以一次性購買/生產(chǎn)多個,不再是一次性買賣了,代碼如下
public class NoBlockSupermarket { private volatile int stock = 0; // 當(dāng)前庫存數(shù)量,為負(fù)代表欠貨 public AtomicInteger deals = new AtomicInteger(0); // 交易量,測試用 final AtomicIntegerFieldUpdater<NoBlockSupermarket> STOCK_UPDATER = AtomicIntegerFieldUpdater.newUpdater(NoBlockSupermarket.class, "stock"); public void send() { deals.incrementAndGet(); // 增加成交數(shù),測試用 } public void buy(int n) { int e = 0; // 已買數(shù)量 while (e != n) { int stock = this.stock; if (!STOCK_UPDATER.compareAndSet(this, stock, stock - 1)) { // 庫存-1 continue; } if (stock > 0) { // 有貨 send(); } e++; } } public void supply(int n) { int e = 0; // 已處理數(shù)量 while (e != n) { int stock = this.stock; if (!STOCK_UPDATER.compareAndSet(this, stock, stock + 1)) {// 庫存+1 continue; } if (stock < 0) { // 欠貨 send(); } e++; } } }
最后
使用CAS可以避免多線程情況下的阻塞,但也并不是所有場景都適用,在沖突嚴(yán)重的情況下樂觀鎖性能可能反而不如悲觀鎖
我所舉例的場景其實就是一個典型的發(fā)布訂閱模式的場景,沖突不高的情況下用樂觀鎖的方式替換悲觀鎖,會達(dá)到性能上質(zhì)的飛躍
以上就是java并發(fā)無鎖多線程單線程示例詳解的詳細(xì)內(nèi)容,更多關(guān)于java并發(fā)無鎖線程的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
java ArrayBlockingQueue阻塞隊列的實現(xiàn)示例
ArrayBlockingQueue是一個基于數(shù)組實現(xiàn)的阻塞隊列,本文就來介紹一下java ArrayBlockingQueue阻塞隊列的實現(xiàn)示例,具有一定的參考價值,感興趣的可以了解一下2024-02-02Spring框架JavaMailSender發(fā)送郵件工具類詳解
這篇文章主要為大家詳細(xì)介紹了Spring框架JavaMailSender發(fā)送郵件工具類,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-04-04淺析Java 常用的 4 種加密方式(MD5+Base64+SHA+BCrypt)
這篇文章主要介紹了Java 常用的 4 種加密方式(MD5+Base64+SHA+BCrypt),本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2019-10-10