• <noscript id="e0iig"><kbd id="e0iig"></kbd></noscript>
  • <td id="e0iig"></td>
  • <option id="e0iig"></option>
  • <noscript id="e0iig"><source id="e0iig"></source></noscript>
  • [java多線程]-狀態依賴

    概述

    在單線程中調用某個方法,而方法中依賴對象的狀態前置條件沒有滿足,那么這個條件將無法滿足。然而在多線程中,一個線程依賴的對象狀態的前置條件沒有滿足,另一個線程可能修改對象狀態,從而使得前置條件滿足。

    多線程中對于依賴狀態的操作一般是前置條件不滿足,阻塞線程,當前置條件滿足,喚醒線程繼續執行。

    在生產者-消費者的設計中經常有像BlockedQueue的有界的阻塞隊列。我們下面就以有界阻塞隊列來講解狀態依賴。

    有界阻塞隊列中需要有兩個接口put用于添加元素,take接口獲取元素處理。而put的前置條件為隊列未滿,take的前置條件為隊列不為空。

    內置條件隊列

    條件隊列是它使得一組線程能夠通過某種方式等待特定的條件為真,條件隊列中的元素是一個個正在等待前置條件的隊列。

    每個對象可以作為一個條件隊列,而waitnotifynotifyAll接口構成了API。

    那我們使用內置的條件隊列來實現有界阻塞隊列。

    public class BoundedBlockedQueue<E> {
        private final Object[] items;
        private int takeIndex;
        private int putIndex;
        private int count;
    
        public BoundedBlockedQueue(Object[] items) {
            this.items = items;
        }
    
        public synchronized E take() throws InterruptedException {
            while (count == 0)
                wait();
            return doTake();
        }
    
        private E doTake() {
            final Object[] items = this.items;
            @SuppressWarnings("unchecked")
            E x = (E) items[takeIndex];
            items[takeIndex] = null;
            if (++takeIndex == items.length) takeIndex = 0;
            count--;
            notifyAll();
            return x;
        }
    
        public synchronized void put(E e) throws InterruptedException {
            while (count == items.length) {
                wait();
            }
            doPut(e);
        }
        private void doPut(E e) {
            // assert lock.getHoldCount() == 1;
            // assert items[putIndex] == null;
            final Object[] items = this.items;
            items[putIndex] = e;
            if (++putIndex == items.length) putIndex = 0;
            count++;
            notifyAll();
        }
    }

    在條件等待中存在一種重要的三元關系,包括加鎖,wait方法和一個條件謂語(條件謂語是某個操作的狀態依賴的前提條件,比如有界阻塞隊列中,take方法的條件謂語就是“緩存不為空”)。在條件謂語中包含多個狀態變量,而狀態變量由一個鎖來保護,因此在測試條件謂語之前必須持有這個鎖。鎖對象與條件隊列對象必須是同一對象。

    摘錄自《Java并發編程實戰》

     public synchronized E take() throws InterruptedException {
            while (count == 0)
                wait();
            return doTake();
        }

    take方法中,條件謂語是“集合不為空”,進入synchronizedtake方法獲取鎖,while檢測集合為空,為空就進入等待。這里使用while來檢測是有兩個原因:

    1. 由于添加元素和移除元素都會通知,因此從wait中喚醒后,不一定是take的前置條件滿足了,因此需要再次檢測。
    2. 由于wait會釋放鎖并且喚醒后會嘗試著重新獲取鎖再進一步操作,當使用notifyAll會喚醒同一對象上的wait的線程,多個線程時存在競爭,一個線程已經完成了take之后,其他線程獲取獲取鎖后需要再次檢測,因為此時前置條件已經不滿足了。

    為什么使用notifyAll而不是notify呢?

    首先解釋一下兩個API的不同,notifyAll是喚醒同一個對象的上的所有線程,而notify只是從同一個對象的線程中挑選一個喚醒。再來看一下我們的有界阻塞隊列,我們的線程可能在等待take的條件謂語集合不為空,也可能在等待put的條件謂語集合未滿,假如在take操作中完成了添加元素之后調用了notify,那我們喚醒的可能只是等待take條件謂語的線程,導致了put一直在等待。因此需要使用notifyAll

    那什么時候使用notify呢?

    只用滿足以下兩個條件時,才能使用單一的notify而不是notifyAll

    • 所有的等待線程的類型相同,只有一個條件謂語與條件隊列相關,并且每個線程在從wait返回后將執行相同的操作。
    • 單進單出,在條件變量上的每次通知,最多只能喚醒一個線程來執行。

    內置條件隊列有它的缺陷,每個內置鎖只能有一個相關聯的條件隊列,像有界阻塞隊列中takeput兩個不同條件謂語只使用同一個條件隊列,導致了notifyAll方法通知會喚醒所有的線程,效率很低。

    后面我們將會使用顯示條件隊列來解決內置條件的問題。

    顯示條件隊列

    Condition是廣義的顯示條件隊列。

    public interface Condition {
        void await() throws InterruptedException;
        void awaitUninterruptibly();
        long awaitNanos(long nanosTimeout) throws InterruptedException;
        boolean await(long time, TimeUnit unit) throws InterruptedException;
        boolean awaitUntil(Date deadline) throws InterruptedException;
        void signal();
        void signalAll();
    }

    與內置條件隊列相同,一個條件隊列需要并且只能與一個內置鎖關聯在一起,一個Condition也需要和一個Lock關聯。條件謂語,LockCondition也存在三元關系,條件謂語中包含的狀態變量必須Lock來保護,并且在檢測條件謂語以及調用awaitsignal時,必須持有Lock

    與內置條件隊列不同的是,對于一個Lock,可以有任意數量的Condition對象,通過LocknewCondition()來創建Condition對象。

    ConditionawaitsignalsignalAll分別與Object對象的waitnotifynotifyAll對應。

    有些Lock并不支持Condition,需要留意。

    public class ConditionBoundedBlockedQueue<E> {
        final Object[] items;
        int takeIndex;
        int putIndex;
        int count;
        final ReentrantLock lock;
        private final Condition notEmpty;
        private final Condition notFull;
    
        public ConditionBoundedBlockedQueue(int capacity) {
            this.items = new Object[capacity];
            //創建鎖,并從鎖中創建條件隊列。
            lock = new ReentrantLock();
            notEmpty = lock.newCondition();
            notFull =  lock.newCondition();
        }
    
    
        public E take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                while (count == 0)
                    notEmpty.await();
                return doTake();
            } finally {
                lock.unlock();
            }
        }
    
        private E doTake() {
            final Object[] items = this.items;
            @SuppressWarnings("unchecked")
            E x = (E) items[takeIndex];
            items[takeIndex] = null;
            if (++takeIndex == items.length) takeIndex = 0;
            count--;
            notFull.signal();
            return x;
        }
    
        public void put(E e) throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                while (count == items.length)
                    notFull.await();
                doPut(e);
            } finally {
                lock.unlock();
            }
        }
        private void doPut(E e) {
            final Object[] items = this.items;
            items[putIndex] = e;
            if (++putIndex == items.length) putIndex = 0;
            count++;
            notEmpty.signal();
        }
    }

    takeput的兩個條件謂語分開并且放到兩個等待線程集中,Condition使其更滿足單次通知的需求。signalsignalAll更高效,它能極大地減少在每次緩存操作中發生的上下文切換與鎖的請求次數。

    Notice: java并發庫里面的阻塞隊列基本是使用顯示阻塞隊列來設計的。通常我們不需要使用自定義的阻塞隊列,java的并發庫已經有了。

    何時使用Condition

    內置鎖與顯示鎖選擇一樣,只有用到Condition的高級特性的時候才會使用它,包括公平性,同一個鎖上多個等待線程集(也就是有多個條件謂語的時候),沒有用到高級特性時候,優先選擇內置條件隊列。

    參考資料

    • 《Java并發編程實戰》
    • 《Java并發編程-設計原則與模式》
    版權聲明:本文為wfeii原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接和本聲明。
    本文鏈接:https://blog.csdn.net/wfeii/article/details/79392353

    智能推薦

    Java多線程 的狀態問題

    線程狀態轉換 下面的這個圖非常重要!你如果看懂了這個圖,那么對于多線程的理解將會更加深刻!   1、新建狀態(New):新創建了一個線程對象。 2、就緒狀態(Runnable):線程對象創建后,其他線程調用了該對象的start()方法。該狀態的線程位于可運行線程池中,變得可運行,等待獲取CPU的使用權。 3、運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。 4、阻塞...

    JAVA多線程的五種狀態

    在JAVA中線程的狀態一般可以分為:五種. 分別為: 具體圖解: 1.被創建,-----一般我們都是繼承Thread然后覆蓋run()方法或者實現Runnable接口,然后覆蓋run()接口. 2.運行-----不管是通過繼承的線程還是通過實現接口的線程,通過start()方法來運行. 這里的具備執行資格可以理解為可以被CPU處理,但是還沒處理,在處理隊列中等待著. 具備執行權可以理解為正在被CP...

    數組刪除其中某個對象的方法

    數組刪除其中的對象或元素,在前端是比較常見的需求。 我現在比較常用的方法如下: 這種方法只適合刪除具有唯一標識的對象。 有沒有想要脫單的小伙伴,加入我們的脫單星球,認識更多優秀的小哥哥小姐姐 特此聲明,星球是免費的,但是創建星球的時候說是必須輸入金額,所以只能先私聊,我再加你免費加入!...

    圖床搭建以及圖床工具的使用

    為什么要用圖床和圖床工具? 比較下面三種md中的圖片url地址(均免費),你會使用哪一種? 選1?由于是本地路徑,文檔分享后給其他人打開后很可能顯示圖片加載失敗。 選2?雖然分享后可以顯示圖片,但能保證加載速度? 選3?我肯定選這種,即兼容2的瀏覽器訪問,又能保證訪問速度。 這樣就可以回答上面的問題了!保證瀏覽器訪問要用圖床,保證加載速度要用圖床工具,又不花錢想想就開心。 除此之外本篇博客還會講解...

    并發編程理論篇

    一、必備知識回顧 計算機又叫電腦,即通電的大腦,發明計算機是為了讓他通電之后能夠像人一樣去工作,并且它比人的工作效率更高,因為可以24小時不間斷 計算機五大組成部分 控制器 運算器 存儲器 輸入設備 輸出設備 計算機的核心真正干活的是CPU(控制器+運算器=中央處理器) 程序要想被計算機運行,它的代碼必須要先由硬盤讀到內存,之后cpu取指再執行 并發 看起來像同時運行的就可以稱之為并發 并行 真正...

    猜你喜歡

    Java LinkedHashMap

    Java LinkedHashMap 前言 Map是我們在實際使用過程中常用的集合,HashMap在Java的實際開發中出鏡率很高,它通過hash算法實現了高效的非線程安全的集合,它有一個缺點就是,用戶插入集合的數據時無序,在我們需要一些有序的map的時候,我們就需要引入另外一個集合:LinkedHashMap。 LinkedHashMap是一個有序的非線程安全的集合,它是HashMap的子類,基...

    Spark Streaming處理文件(本地文件以及hdfs上面的文件)

    標題介紹文件流之前先介紹一下Dstream 下面是來自官網一段的說明,Discretized Streams或DStream是Spark Streaming提供的基本抽象。它表示連續的數據流,可以是從源接收的輸入數據流,也可以是通過轉換輸入流生成的已處理數據流。在內部,DStream由一系列連續的RDD表示,這是Spark對不可變的分布式數據集的抽象(有關更多詳細信息,請參見Spark編程指南)。...

    《痞子衡嵌入式半月刊》 第 8 期

    痞子衡嵌入式半月刊: 第 8 期 這里分享嵌入式領域有用有趣的項目/工具以及一些熱點新聞,農歷年分二十四節氣,希望在每個交節之日準時發布一期。 本期刊是開源項目(GitHub: JayHeng/pzh-mcu-bi-weekly),歡迎提交 issue,投稿或推薦你知道的嵌入式那些事兒。 上期回顧 :《痞子衡嵌入式半月刊: 第 7 期》 嘮兩句 今天是小滿,小滿節氣意味著進入了大幅降水的雨季。痞子...

    (C++)二叉樹的線索化 / 線索二叉樹

    好久不見,朋友們!雖然我知道沒人看我的博客,但我還是想叨逼叨一下。啊,好久沒編程了(其實也就一周沒編),但你們知道,程序員一天不編程那能叫程序員么???雖然我不是程序員哈哈哈哈哈,但還是要有基本素養嘛。 繼續寫二叉樹,給自己立一個flag,就是這幾天要寫完之前沒做完的幾道題,和二叉樹紅黑樹各種樹之類的~~雖然有這個flag,但我還是很實誠地遵從自己的內心,買了一張明天的電影票,等我回來告訴你們好不...

    Linux內存管理:分頁機制

    《Linux內存管理:內存描述之內存節點node》 《Linux內存管理:內存描述之內存區域zone》 《Linux內存管理:內存描述之內存頁面page》 《Linux內存管理:內存描述之高端內存》 《Linux內存管理:分頁機制》 《內存管理:Linux Memory Management:MMU、段、分頁、PAE、Cache、TLB》 目錄 1 分頁機制 1.1 為什么使用多級頁表來完成映射 ...

    精品国产乱码久久久久久蜜桃不卡