java并發(fā)無(wú)鎖多線程單線程示例詳解
前言
在并發(fā)編程中,多線程的共享資源的修改往往會(huì)造成嚴(yán)重的線程安全問(wèn)題,解決這種問(wèn)題簡(jiǎn)單暴力的方式就是加鎖,加鎖的方式使用簡(jiǎn)單易理解,但常常會(huì)因?yàn)樽枞麑?dǎo)致性能問(wèn)題
有沒(méi)有可能做到無(wú)鎖還保證線程安全吶?這得看具體情況。得益于CAS技術(shù),有很多情況下我們可以做到不使用鎖也能保證線程的安全
比如今天我最近遇到的場(chǎng)景如下(由于場(chǎng)景比較復(fù)雜,用一個(gè)模擬簡(jiǎn)化一下)
場(chǎng)景
假設(shè)有一個(gè)商店,背后有一個(gè)工廠可以生產(chǎn)商品,商店也可以有用戶來(lái)購(gòu)買商品,為了簡(jiǎn)化,假設(shè)工廠只能生產(chǎn)一個(gè)商品、而用戶也只能買一個(gè)商品
需求如下:
- 用戶來(lái)購(gòu)買,如果商品已經(jīng)生產(chǎn)好了,則直接發(fā)貨,完成交易
- 用戶來(lái)購(gòu)買,如果商品還沒(méi)生產(chǎn)好,讓用戶填寫一個(gè)欠貨單,待工廠生產(chǎn)好后,如果發(fā)現(xiàn)有欠貨,則直接發(fā)貨,完成交易
簡(jiǎn)簡(jiǎn)單單的一個(gè)需求,在多線程環(huán)境下就會(huì)出現(xiàn)隱患
單線程
先不考慮多線程情況,這個(gè)代碼很好寫,我們用一個(gè)ready變量標(biāo)識(shí)是否生產(chǎn)完成,用一個(gè)unSupply變量標(biāo)識(shí)是否有欠用戶一個(gè)商品,代碼如下
public class SerialShop {
private volatile boolean ready; // 商品生產(chǎn)完成
private volatile boolean unSupply; // 是否欠用戶一個(gè)商品
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; // 沒(méi)有準(zhǔn)備好則填寫一個(gè)欠貨單
}
public void ready() {
this.ready = true; // 標(biāo)識(shí)商品準(zhǔn)備完成
if (this.unSupply) { // 如果發(fā)現(xiàn)有欠貨單
send(); // 給用戶發(fā)貨
}
}
}
這時(shí),我們簡(jiǎn)單跑一下
@Test
public void buyBeforeReady() {
buy();
ready();
}
@Test
public void buyAfterReady() {
ready();
buy();
}
結(jié)果無(wú)論先購(gòu)買再生產(chǎn)完,還是生產(chǎn)完再購(gòu)買,最終都會(huì)走到send方法,完成交易
多線程
上面的代碼雖然簡(jiǎn)單,但在多線程下就會(huì)出現(xiàn)問(wèn)題,用實(shí)際的情形描述一下
- 用戶來(lái)購(gòu)買發(fā)現(xiàn)商品沒(méi)生產(chǎn)好,則開(kāi)始準(zhǔn)備填寫欠貨單,由于用戶文盲,填寫的很慢
- 此時(shí)工廠恰好生產(chǎn)好了,標(biāo)識(shí)已準(zhǔn)備,但一看還沒(méi)有欠貨單,所以不發(fā)貨
- 用戶剛剛填寫完欠貨單,沒(méi)啥事就回家了
- 最終,用戶付完了錢,工廠也生產(chǎn)完畢,就是沒(méi)有發(fā)貨完成交易
畫個(gè)時(shí)序圖描述一下這個(gè)情景

時(shí)序圖
因?yàn)槎嗑€程無(wú)法保證有序性,所以這種情況出現(xiàn)的概率很大,而一旦出現(xiàn)就是嚴(yán)重問(wèn)題
用代碼模擬一下這個(gè)場(chǎng)景:
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); // 這里手動(dòng)降低線程速度,為了重現(xiàn)場(chǎng)景
this.unSupply = true; // 沒(méi)有準(zhǔn)備好則填寫一個(gè)欠貨單
}
public void ready() throws InterruptedException {
this.ready = true; // 標(biāo)識(shí)商品準(zhǔn)備完成
if (this.unSupply) { // 如果發(fā)現(xiàn)有欠貨單
send(); // 給用戶發(fā)貨
}
}
@Test
public void unsafe() throws InterruptedException {
// 用戶購(gòu)買
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é)果:并沒(méi)有走到send方法(上面的代碼通過(guò)sleep來(lái)降低線程的執(zhí)行速度,是為了100%呈現(xiàn)錯(cuò)誤,實(shí)際中就算不寫sleep也有可能出現(xiàn)這種情況)
悲觀鎖
那么如何避免上面的問(wèn)題吶,最簡(jiǎn)單暴力的方式就是加鎖
上面的問(wèn)題之所以出現(xiàn),是因?yàn)橛脩舨榭词欠裆唐芬褱?zhǔn)備和標(biāo)識(shí)欠貨的兩步操作沒(méi)有原子性,導(dǎo)致中間的過(guò)程可能被工廠的線程快速完成所有動(dòng)作和判斷
實(shí)際情形下我們可以這么解決問(wèn)題:在接納用戶的時(shí)候,如果工廠來(lái)人送貨,讓工廠的人在外面等著,等用戶把該做的都做了,工廠的人再進(jìn)來(lái)標(biāo)識(shí)準(zhǔn)備完畢并送貨
用代碼模擬一下這個(gè)解決方案
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) { // 接納用戶時(shí)不讓工廠人進(jìn)入
if (ready) { // 準(zhǔn)備好了
send(); // 直接發(fā)貨
return;
}
Thread.sleep(100);
this.unSupply = true; // 沒(méi)有準(zhǔn)備好則填寫一個(gè)欠貨單
}
}
public void ready() throws InterruptedException {
synchronized (this) { // 接納用戶時(shí)不讓工廠人進(jìn)入
this.ready = true; // 標(biāo)識(shí)商品準(zhǔn)備完成
}
if (this.unSupply) { // 如果發(fā)現(xiàn)有欠貨單
send(); // 給用戶發(fā)貨
}
}
@Test
public void block() throws InterruptedException {
// 用戶購(gòu)買
new Thread(() -> {
try {
buy();
} catch (InterruptedException e) {
}
}).start();
Thread.sleep(50);
// 工廠生產(chǎn)
new Thread(() -> {
try {
ready();
} catch (InterruptedException e) {
}
}).start();
while (true) ;
}
}這時(shí),不會(huì)在出現(xiàn)上述問(wèn)題,徹底的解決了線程安全
而從解決問(wèn)題的實(shí)際場(chǎng)景來(lái)看,這種解決問(wèn)題的方法在現(xiàn)實(shí)中簡(jiǎn)直是弱智,工廠的人就在外面傻等,這就是阻塞,會(huì)降低代碼的執(zhí)行速度
當(dāng)然以上場(chǎng)景的阻塞實(shí)際其實(shí)很小,但個(gè)人認(rèn)為鎖這東西能不用盡量不用,在場(chǎng)景復(fù)雜的時(shí)候阻塞的弱點(diǎn)會(huì)更加凸顯出來(lái)
無(wú)鎖
在這種場(chǎng)景下,能不能不使用鎖來(lái)達(dá)到線程安全的效果吶?
我想了很多辦法,比如buy時(shí)先標(biāo)識(shí)已欠貨,再去判斷是否已準(zhǔn)備,或者標(biāo)識(shí)完已欠貨再去看一眼是否已準(zhǔn)備好,但都行不通,原因就是無(wú)法保證原子性,也無(wú)法保證多線程的有序性
冥思苦想后,想到一個(gè)解決方案:工廠人員上門后第一件事就是把欠貨單撕了!
- 此時(shí)如果用戶正在寫這個(gè)欠貨單,那肯定是撕不成的,出現(xiàn)沖突說(shuō)明用戶已來(lái)了,直接發(fā)貨即可
- 如果用戶還沒(méi)寫且正準(zhǔn)備寫,發(fā)現(xiàn)欠貨單沒(méi)了,出現(xiàn)沖突說(shuō)明貨來(lái)了,直接發(fā)貨即可
此時(shí)欠貨單有三個(gè)狀態(tài):初始狀態(tài)/被撕了/填寫完,我們用商品的庫(kù)存標(biāo)識(shí)為:0/1/-1(欠用戶一臺(tái))
private volatile int stock = 0;
而stock==1也說(shuō)明貨已到,所以不需要ready變量了
最終代碼如下
public class NoBlockShop {
private volatile int stock = 0; // 庫(kù)存量 -1代表虧欠用戶一臺(tái)
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)識(shí)欠貨,如果失敗說(shuō)明庫(kù)存有變動(dòng),再回頭查看一下
return;
}
}
}
public void ready() throws InterruptedException {
if (!STATUS_UPDATER.compareAndSet(this, 0, 1)) { // 標(biāo)識(shí)有庫(kù)存
send(); // 如果失敗代表用戶來(lái)過(guò)了,直接發(fā)貨
}
}
}不僅解決了線程安全,還無(wú)鎖(也可以稱作樂(lè)觀鎖),并且代碼還簡(jiǎn)潔了,CAS是真香
測(cè)試一下線程安全,代碼如下
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萬(wàn)個(gè)shop,然后用不同線程分別buy和ready,最終輸出沒(méi)交易的shop個(gè)數(shù)
- 如果使用
UnsafeShop(初版),一般結(jié)果都不是0,且每次執(zhí)行都不一樣,說(shuō)明有的shop對(duì)象出現(xiàn)線程安全問(wèn)題 - 如果使用
BlockShop(鎖版),結(jié)果是0,說(shuō)明線程安全 - 如果使用
NoBlockShop(CAS版),結(jié)果是0,說(shuō)明也實(shí)現(xiàn)了線程安全
根據(jù)這個(gè)可以繼續(xù)改造一下讓商店,讓工廠可以不斷生產(chǎn)商品,用戶也能不斷購(gòu)買,依然使用stock,為正代表有n個(gè)庫(kù)存,為負(fù)代表欠用戶n個(gè)商品,并且可以一次性購(gòu)買/生產(chǎn)多個(gè),不再是一次性買賣了,代碼如下
public class NoBlockSupermarket {
private volatile int stock = 0; // 當(dāng)前庫(kù)存數(shù)量,為負(fù)代表欠貨
public AtomicInteger deals = new AtomicInteger(0); // 交易量,測(cè)試用
final AtomicIntegerFieldUpdater<NoBlockSupermarket> STOCK_UPDATER =
AtomicIntegerFieldUpdater.newUpdater(NoBlockSupermarket.class, "stock");
public void send() {
deals.incrementAndGet(); // 增加成交數(shù),測(cè)試用
}
public void buy(int n) {
int e = 0; // 已買數(shù)量
while (e != n) {
int stock = this.stock;
if (!STOCK_UPDATER.compareAndSet(this, stock, stock - 1)) { // 庫(kù)存-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)) {// 庫(kù)存+1
continue;
}
if (stock < 0) { // 欠貨
send();
}
e++;
}
}
}最后
使用CAS可以避免多線程情況下的阻塞,但也并不是所有場(chǎng)景都適用,在沖突嚴(yán)重的情況下樂(lè)觀鎖性能可能反而不如悲觀鎖
我所舉例的場(chǎng)景其實(shí)就是一個(gè)典型的發(fā)布訂閱模式的場(chǎng)景,沖突不高的情況下用樂(lè)觀鎖的方式替換悲觀鎖,會(huì)達(dá)到性能上質(zhì)的飛躍
以上就是java并發(fā)無(wú)鎖多線程單線程示例詳解的詳細(xì)內(nèi)容,更多關(guān)于java并發(fā)無(wú)鎖線程的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 注冊(cè)或者點(diǎn)擊按鈕時(shí),怎么防止用戶重復(fù)提交數(shù)據(jù)(實(shí)例講解)
- Java多線程循環(huán)柵欄CyclicBarrier正確使用方法
- Java多線程正確使用倒計(jì)時(shí)協(xié)調(diào)器CountDownLatch方法詳解
- Java多線程場(chǎng)景解析volatile和AtomicLong區(qū)別原理
- ReentrantLock從源碼解析Java多線程同步學(xué)習(xí)
- Java多線程編程基石ThreadPoolExecutor示例詳解
- java多線程事務(wù)加鎖引發(fā)bug用戶重復(fù)注冊(cè)解決分析
相關(guān)文章
mybatis使用mapper代理開(kāi)發(fā)方式
使用MyBatis代理開(kāi)發(fā)模式時(shí),需要注意定義與映射配置文件同名的接口類,確保namespace屬性與接口路徑一致,接口方法名和映射文件中的id名稱相同,返回類型保持一致,在mybatis-config.xml中配置映射文件路徑,保證結(jié)構(gòu)一致,可通過(guò)注解@Param傳遞多個(gè)參數(shù)2024-10-10
java ArrayBlockingQueue阻塞隊(duì)列的實(shí)現(xiàn)示例
ArrayBlockingQueue是一個(gè)基于數(shù)組實(shí)現(xiàn)的阻塞隊(duì)列,本文就來(lái)介紹一下java ArrayBlockingQueue阻塞隊(duì)列的實(shí)現(xiàn)示例,具有一定的參考價(jià)值,感興趣的可以了解一下2024-02-02
在Java內(nèi)存模型中測(cè)試并發(fā)程序代碼
這篇文章主要介紹了在Java內(nèi)存模型中測(cè)試并發(fā)程序代碼,輔以文中所提到的JavaScript庫(kù)JCStress進(jìn)行,需要的朋友可以參考下2015-07-07
Spring框架JavaMailSender發(fā)送郵件工具類詳解
這篇文章主要為大家詳細(xì)介紹了Spring框架JavaMailSender發(fā)送郵件工具類,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-04-04
淺析Java 常用的 4 種加密方式(MD5+Base64+SHA+BCrypt)
這篇文章主要介紹了Java 常用的 4 種加密方式(MD5+Base64+SHA+BCrypt),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-10-10

