欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

java并發(fā)無鎖多線程單線程示例詳解

 更新時間:2023年07月13日 11:35:01   作者:pq217  
這篇文章主要為大家介紹了java并發(fā)無鎖多線程單線程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

前言

在并發(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)文章

  • mybatis使用mapper代理開發(fā)方式

    mybatis使用mapper代理開發(fā)方式

    使用MyBatis代理開發(fā)模式時,需要注意定義與映射配置文件同名的接口類,確保namespace屬性與接口路徑一致,接口方法名和映射文件中的id名稱相同,返回類型保持一致,在mybatis-config.xml中配置映射文件路徑,保證結(jié)構(gòu)一致,可通過注解@Param傳遞多個參數(shù)
    2024-10-10
  • Spring源碼解析之Configuration

    Spring源碼解析之Configuration

    今天帶大家來學(xué)習(xí)Java Spring相關(guān)知識,文中對Configuration源碼介紹的非常詳細(xì),有非常多的圖文解說及代碼示例,對正在學(xué)習(xí)java的小伙伴們很有幫助,需要的朋友可以參考下
    2021-05-05
  • Java如何讀取XML文件 具體實現(xiàn)

    Java如何讀取XML文件 具體實現(xiàn)

    這篇文章主要介紹了Java如何讀取XML文件 具體實現(xiàn),有需要的朋友可以參考一下
    2013-12-12
  • java ArrayBlockingQueue阻塞隊列的實現(xiàn)示例

    java ArrayBlockingQueue阻塞隊列的實現(xiàn)示例

    ArrayBlockingQueue是一個基于數(shù)組實現(xiàn)的阻塞隊列,本文就來介紹一下java ArrayBlockingQueue阻塞隊列的實現(xiàn)示例,具有一定的參考價值,感興趣的可以了解一下
    2024-02-02
  • 在Java內(nèi)存模型中測試并發(fā)程序代碼

    在Java內(nèi)存模型中測試并發(fā)程序代碼

    這篇文章主要介紹了在Java內(nèi)存模型中測試并發(fā)程序代碼,輔以文中所提到的JavaScript庫JCStress進(jìn)行,需要的朋友可以參考下
    2015-07-07
  • 詳解Spring 注解之@Import 注入的各種花活

    詳解Spring 注解之@Import 注入的各種花活

    這篇文章主要介紹了詳解Spring 注解之@Import 注入的各種花活,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2020-01-01
  • MyBatis中resultType屬性的使用

    MyBatis中resultType屬性的使用

    這篇文章主要介紹了MyBatis中resultType屬性的使用,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教
    2024-09-09
  • spring中的FactoryBean代碼示例

    spring中的FactoryBean代碼示例

    這篇文章主要介紹了spring中的FactoryBean代碼示例,涉及FactoryBean的實現(xiàn)等相關(guān)內(nèi)容,具有一定參考價值,需要的朋友可以了解下。
    2017-10-10
  • Spring框架JavaMailSender發(fā)送郵件工具類詳解

    Spring框架JavaMailSender發(fā)送郵件工具類詳解

    這篇文章主要為大家詳細(xì)介紹了Spring框架JavaMailSender發(fā)送郵件工具類,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2019-04-04
  • 淺析Java 常用的 4 種加密方式(MD5+Base64+SHA+BCrypt)

    淺析Java 常用的 4 種加密方式(MD5+Base64+SHA+BCrypt)

    這篇文章主要介紹了Java 常用的 4 種加密方式(MD5+Base64+SHA+BCrypt),本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下
    2019-10-10

最新評論