夜光:Java成神之路(十二)擅長的語言

夜光序言:

 

天空中最微弱的星 也有權利爭取最美的燦爛。

 

 

 

 
 
正文:
 
                                              以道御術 / 以術識道



schedule (Runnable task, long delay, TimeUnit timeunit)

 
這一方法規劃一個任務將被定期執行。該任務將會在首個 initialDelay 之後得到執行,然後每個 period 時間之後重複執行。
 
 
如果給定任務的執行拋出了異常,該任務將不再執行。
如果沒有任何異常的話,這個任務將會持續循環執行到 ScheduledExecutorService 被關閉。
 
 
如果一個任務佔用了比計劃的時間間隔更長的時候,下一次執行將在當前執行結束執行纔開始。計劃任務在同一時間不會有多個線程同時執行。
 

 

scheduleAtFixedRate (Runnable, long initialDelay, long period, TimeUnit timeunit)

 
這一方法規劃一個任務將被定期執行。該任務將會在首個 initialDelay 之後得到執行,然後每個 period 時間之後重複執行。
 
 
如果給定任務的執行拋出了異常,該任務將不再執行。
如果沒有任何異常的話,這個任務將會持續循環執行到 ScheduledExecutorService 被關閉。
 
如果一個任務佔用了比計劃的時間間隔更長的時候,下一次執行將在當前執行結束執行纔開始。
 
 
計劃任務在同一時間不會有多個線程同時執行。

 

scheduleWithFixedDelay (Runnable, long initialDelay, long period, TimeUnit timeunit)

 
 
除了 period 有不同的解釋之外這個方法和 scheduleAtFixedRate() 非常像。
 
scheduleAtFixedRate() 方法中,period 被解釋爲前一個執行的開始和下一個執行的開始之間的間隔時間。而在本方法中,period 則被解釋爲前一個執行的結束和下一個執行的結束之間的間隔。
 
因此這個延遲是執行結束之間的間隔,而不是執行開始之間的間隔。
 

 

ScheduledExecutorService 的關閉:

 
 
正如 ExecutorService,在你使用結束之後你需要把 ScheduledExecutorService 關閉掉。否則他將導致 JVM 繼續運行,即使所有其他線程已經全被關閉。
 
你 可 以 使 用 從 ExecutorService 接 口 繼 承 來 的 shutdown() 或 shutdownNow() 方 法 將 ScheduledExecutorService 關閉。參見 ExecutorService 關閉部分以獲取更多信息。
 
 

ForkJoinPool 合併和分叉(線程池)

 
ForkJoinPool 在 Java 7 中被引入。它和 ExecutorService 很相似,除了一點不同。
 
ForkJoinPool 讓我們可以很方便地把任務分裂成幾個更小的任務,這些分裂出來的任務也將會提交給 ForkJoinPool。
 
任務可以繼續分割成更小的子任務,只要它還能分割。
 
可能聽起來有些抽象,因此本節中我們將會解釋 ForkJoinPool 是如何工作的,還有任務分割是如何進行的。
 
 
 

合併和分叉的解釋:

 

在我們開始看 ForkJoinPool 之前我們先來簡要解釋一下分叉和合並的原理。分叉和合並原理包含兩個遞歸進行的步驟。
 
兩個步驟分別是分叉步驟和合並步驟。
 

 

分叉:

 
一個使用了分叉和合並原理的任務可以將自己分叉(分割)爲更小的子任務,這些子任務可以被併發執行。如下圖所示:

通過把自己分割成多個子任務,每個子任務可以由不同的 CPU 並行執行,或者被同一個 CPU 上的不同線程執行。
 
只有當給的任務過大,把它分割成幾個子任務纔有意義。
 
把任務分割成子任務有一定開銷,因此對於小型任務,這個分割的消耗可能比每個子任務併發執行的消耗還要大。
 
 
什麼時候把一個任務分割成子任務是有意義的,這個界限也稱作一個閥值。這要看每個任務對有意義閥值的決定。
 
很大程度上取決於它要做的工作的種類。
 
 

 

合併:

 
當一個任務將自己分割成若干子任務之後,該任務將進入等待所有子任務的結束之中。
 
一旦子任務執行結束,該任務可以把所有結果合併到同一個結果。圖示如下:

當然,並非所有類型的任務都會返回一個結果。
 
如果這個任務並不返回一個結果,它只需等待所有子任務執行完 畢。
 
也就不需要結果的合併啦。
 
所以我們可以將 ForkJoinPool 是一個特殊的線程池,它的設計是爲了更好的配合 分叉-和-合併 任務分割的工作。
 
ForkJoinPool 也在 java.util.concurrent 包中,其完整類名爲 java.util.concurrent.ForkJoinPool。
 
 

創建一個 ForkJoinPool:

你可以通過其構造子創建一個 ForkJoinPool。
 
作爲傳遞給 ForkJoinPool 構造子的一個參數,你可以定義你期望的並行級別。並行級別表示你想要傳遞給 ForkJoinPool 的任務所需的線程或 CPU 數量。

以下是一個 ForkJoinPool 示例:
//創建了一個並行級別爲 4 的 ForkJoinPool
ForkJoinPool forkJoinPool = new ForkJoinPool(4);

 

 

提交任務到 ForkJoinPool:

就像提交任務到 ExecutorService 那樣,把任務提交到 ForkJoinPool。你可以提交兩種類型的任務。
 
一種是沒有任何返回值的(一個 “行動”),另一種是有返回值的(一個”任務”)。
 
 
這兩種類型分別由 RecursiveAction 和RecursiveTask 表示。
 
接下來介紹如何使用這兩種類型的任務,以及如何對它們進行提交。

 

RecursiveAction:

 
RecursiveAction 是一種沒有任何返回值的任務。
 
它只是做一些工作,比如寫數據到磁盤,然後就退出了。
 
 
一個RecursiveAction 可以把自己的工作分割成更小的幾塊,這樣它們可以由獨立的線程或者 CPU 執行。
 
 
 
你可以通過繼承來實現一個 RecursiveAction。示例如下
package com.hy.多線程高併發;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveAction;
public class MyRecursiveAction extends RecursiveAction {


    private long workLoad = 0;
    public MyRecursiveAction(long workLoad) {
        this.workLoad = workLoad;
    }

    
    @Override
    protected void compute() {
//if work is above threshold, break tasks up into smaller tasks
        //翻譯:如果工作超過門檻,把任務分解成更小的任務
        if(this.workLoad > 16) {
            System.out.println("Splitting workLoad : " + this.workLoad);
            List<MyRecursiveAction> subtasks =
                    new ArrayList<MyRecursiveAction>();
            subtasks.addAll(createSubtasks());
            for(RecursiveAction subtask : subtasks){
                subtask.fork();
            }
        } else {
            System.out.println("Doing workLoad myself: " + this.workLoad);
        }
    }


    private List<MyRecursiveAction> createSubtasks() {
        List<MyRecursiveAction> subtasks =
                new ArrayList<MyRecursiveAction>();
        MyRecursiveAction subtask1 = new MyRecursiveAction(this.workLoad / 2);
        MyRecursiveAction subtask2 = new MyRecursiveAction(this.workLoad / 2);
        subtasks.add(subtask1);
        subtasks.add(subtask2);
        return subtasks;
    }

    public static void main(String[] args) {
        
    }
    
    
}

 

例子很簡單。MyRecursiveAction 將一個虛構的 workLoad 作爲參數傳給自己的構造子。

 

 
如果 workLoad 高於一個特定閥值,該工作將被分割爲幾個子工作,子工作繼續分割。
 
如果 workLoad 低於特定閥值,該工作將由 MyRecursiveAction 自己執行。

 

 

你可以這樣規劃一個 MyRecursiveAction 的執行:

package com.hy.多線程高併發;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveAction;
public class MyRecursiveAction extends RecursiveAction {


    private long workLoad = 0;
    public MyRecursiveAction(long workLoad) {
        this.workLoad = workLoad;
    }


    @Override
    protected void compute() {
//if work is above threshold, break tasks up into smaller tasks
        //翻譯:如果工作超過門檻,把任務分解成更小的任務
        if(this.workLoad > 16) {
            System.out.println("Splitting workLoad : " + this.workLoad);
            List<MyRecursiveAction> subtasks =
                    new ArrayList<MyRecursiveAction>();
            subtasks.addAll(createSubtasks());
            for(RecursiveAction subtask : subtasks){
                subtask.fork();
            }
        } else {
            System.out.println("Doing workLoad myself: " + this.workLoad);
        }
    }


    private List<MyRecursiveAction> createSubtasks() {
        List<MyRecursiveAction> subtasks =
                new ArrayList<MyRecursiveAction>();
        MyRecursiveAction subtask1 = new MyRecursiveAction(this.workLoad / 2);
        MyRecursiveAction subtask2 = new MyRecursiveAction(this.workLoad / 2);
        subtasks.add(subtask1);
        subtasks.add(subtask2);
        return subtasks;
    }

    public static void main(String[] args) {
        //創建了一個並行級別爲 4 的 ForkJoinPool
        ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//創建一個沒有返回值的任務
        MyRecursiveAction myRecursiveAction = new MyRecursiveAction(24);
//ForkJoinPool 執行任務
        forkJoinPool.invoke(myRecursiveAction);
    }


}

 

RecursiveTask:

 
RecursiveTask 是一種會返回結果的任務。
 
它可以將自己的工作分割爲若干更小任務,並將這些子任務的執行結 果合併到一個集體結果。可以有幾個水平的分割和合並。以下是一個 RecursiveTask 示例:
package com.hy.多線程高併發;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.RecursiveTask;
public class MyRecursiveTask extends RecursiveTask<Long> {


    private long workLoad = 0;

    public MyRecursiveTask(long workLoad) {
        this.workLoad = workLoad;
    }

    protected Long compute() {
//if work is above threshold, break tasks up into smaller tasks
        //hy:you should try you best to 
        if(this.workLoad > 16) {
            System.out.println("Splitting workLoad : " + this.workLoad);
            List<MyRecursiveTask> subtasks =
                    new ArrayList<MyRecursiveTask>();
            subtasks.addAll(createSubtasks());
            for(MyRecursiveTask subtask : subtasks){
                subtask.fork();
            }
            long result = 0;
            for(MyRecursiveTask subtask : subtasks) {
                result += subtask.join();
            }
            return result;
        } else {
            System.out.println("Doing workLoad myself: " + this.workLoad);
            return workLoad * 3;
        }
    }


    private List<MyRecursiveTask> createSubtasks() {
        List<MyRecursiveTask> subtasks =
                new ArrayList<MyRecursiveTask>();
        MyRecursiveTask subtask1 = new MyRecursiveTask(this.workLoad / 2);
        MyRecursiveTask subtask2 = new MyRecursiveTask(this.workLoad / 2);
        subtasks.add(subtask1);
        subtasks.add(subtask2);
        return subtasks;
    }

    public static void main(String[] args) {
        //測試運行一下
    }
    
}

 

除 了 有 一 個 結 果 返 回 之 外 , 這 個 示 例 和 RecursiveAction 的 例 子 很 像 。

 
MyRecursiveTask 類 繼 承 自RecursiveTask<Long>,這也就意味着它將返回一個 Long 類型的結果。
 
MyRecursiveTask 示例也會將工作分割爲子任務,並通過 fork() 方法對這些子任務計劃執行。
 
 
此外,本示例還通過調用每個子任務的 join() 方法收集它們返回的結果。子任務的結果隨後被合併到一個更大的結果,並最終將其返
回。
 
 
對於不同級別的遞歸,這種子任務的結果合併可能會發生遞歸你可以這樣規劃一個 RecursiveTask:

package com.hy.多線程高併發;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
public class MyRecursiveTask extends RecursiveTask<Long> {


    private long workLoad = 0;

    public MyRecursiveTask(long workLoad) {
        this.workLoad = workLoad;
    }

    protected Long compute() {
//if work is above threshold, break tasks up into smaller tasks
        //hy:you should try you best to
        if(this.workLoad > 16) {
            System.out.println("Splitting workLoad : " + this.workLoad);
            List<MyRecursiveTask> subtasks =
                    new ArrayList<MyRecursiveTask>();
            subtasks.addAll(createSubtasks());
            for(MyRecursiveTask subtask : subtasks){
                subtask.fork();
            }
            long result = 0;
            for(MyRecursiveTask subtask : subtasks) {
                result += subtask.join();
            }
            return result;
        } else {
            System.out.println("Doing workLoad myself: " + this.workLoad);
            return workLoad * 3;
        }
    }


    private List<MyRecursiveTask> createSubtasks() {
        List<MyRecursiveTask> subtasks =
                new ArrayList<MyRecursiveTask>();
        MyRecursiveTask subtask1 = new MyRecursiveTask(this.workLoad / 2);
        MyRecursiveTask subtask2 = new MyRecursiveTask(this.workLoad / 2);
        subtasks.add(subtask1);
        subtasks.add(subtask2);
        return subtasks;
    }

    public static void main(String[] args) {
        //測試運行一下
        //創建了一個並行級別爲 4 的 ForkJoinPool
        ForkJoinPool forkJoinPool = new ForkJoinPool(4);
//創建一個有返回值的任務
        MyRecursiveTask myRecursiveTask = new MyRecursiveTask(128);
        //線程池執行並返回結果
        long mergedResult = forkJoinPool.invoke(myRecursiveTask);
        System.out.println("mergedResult = " + mergedResult);
    }

}

 

注意: ForkJoinPool.invoke() 方法的調用來獲取最終執行結果的。


B. 併發隊列-阻塞隊列

 
常用的併發隊列有阻塞隊列和非阻塞隊列,前者使用鎖實現,後者則使用 CAS 非阻塞算法實現
 
PS:至於非阻塞隊列是靠 CAS 非阻塞算法,在這裏不再介紹,大家只用知道,Java 非阻塞隊列是使用 CAS 算法來實現的就可
以。感興趣的童鞋可以維基網上自行學習.
 
 

下面我們先介紹阻塞隊列。

 
阻塞隊列:
 
阻塞隊列 (BlockingQueue)是 Java util.concurrent 包下重要的數據結構,BlockingQueue 提供了線程安全的隊列訪問方式:
 
 
當阻塞隊列進行插入數據時,如果隊列已滿,線程將會阻塞等待直到隊列非滿;
 
 
從阻塞隊列取數據時,如 果隊列已空,線程將會阻塞等待直到隊列非空。併發包下很多高級同步類的實現都是基於 BlockingQueue 實現的。
 

 

BlockingQueue 阻塞隊列

 
 
BlockingQueue 通常用於一個線程生產對象,而另外一個線程消費這些對象的場景。
 
下圖是對這個原理的闡述:
 

一個線程往裏邊放,另外一個線程從裏邊取的一個 BlockingQueue。

 
一個線程將會持續生產新對象並將其插入到隊列之中,直到隊列達到它所能容納的臨界點。
 
也就是說,它是有限的。如果該阻塞隊列到達了其臨界點,負責生產的線程將會在往裏邊插入新對象時發生阻塞。
 
 
它會一直處於阻塞之中, 直到負責消費的線程從隊列中拿走一個對象。
 
 
負責消費的線程將會一直從該阻塞隊列中拿出對象。
 
如果消費線程嘗試去從一個空的隊列中提取對象的話,這個消費線程將會處於阻塞之中,直到一個生產線程把一個對象丟進隊列。
 
 

BlockingQueue 的方法:

 

BlockingQueue 具有 4 組不同的方法用於插入、移除以及對隊列中的元素進行檢查。如果請求的操作不能得到立即執行的話,每個方法的表現也不同。
 

這些方法如下:

 
 
阻塞隊列提供了四種處理方法:

四組不同的行爲方式解釋:
 
拋異常:如果試圖的操作無法立即執行,拋一個異常。
 
特定值:如果試圖的操作無法立即執行,返回一個特定的值(常常是 true / false)。
 
阻塞:如果試圖的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行。
 
超時:如果試圖的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行,但等待時間不會超過給定值。返回一個特定值以告知該操作是否成功(典型的是 true / false)。
 

 

 

無法向一個 BlockingQueue 中插入 null。如果你試圖插入 null,BlockingQueue 將會拋出一個

NullPointerException.

 

 

BlockingQueue 的實現類:

 
 
BlockingQueue 是個接口,你需要使用它的實現之一來使用 BlockingQueue,Java.util.concurrent 包下具有以下 BlockingQueue 接口的實現類:
 
    ArrayBlockingQueue:ArrayBlockingQueue 是一個有界的阻塞隊列,其內部實現是將對象放到一個數組裏。
 
有界也就意味着,它不能夠存儲無限多數量的元素。
 
它有一個同一時間能夠存儲元素數量的上限。
 
可以在對其初始化的時候設定這個上限,但之後就無法對這個上限進行修改了(譯者注:因爲它是基於數組實現的,也就具有數組的特性:一旦初始化,大小就無法修改)。
 
 
    DelayQueue:DelayQueue 對元素進行持有直到一個特定的延遲到期。
 
    注入其中的元素必須實 java.util.concurrent.Delayed 接口。
 
    LinkedBlockingQueue:LinkedBlockingQueue 內部以一個鏈式結構(鏈接節點)對其元素進行存儲。
    如果需要的話,這一鏈式結構可以選擇一個上限。如果沒有定義上限,將使用 Integer.MAX_VALUE 作爲上限。
 
    PriorityBlockingQueue : PriorityBlockingQueue 是 一 個 無 界 的 並 發 隊 列 。 它 使 用 了 和 類 java.util.PriorityQueue 一 樣 的 排 序 規 則 。 你 無 法 向 這 個 隊 列 中 插 入 null 值 。
 
所 有 插 入 到 PriorityBlockingQueue 的元素必須實現 java.lang.Comparable 接口。因此該隊列中元素的排序就取決於你自己的 Comparable 實現。
 
 

 

    SynchronousQueue:SynchronousQueue 是一個特殊的隊列,它的內部同時只能夠容納單個元素。

 
    如果該隊列已有一元素的話,試圖向隊列中插入一個新元素的線程將會阻塞,直到另一個線程將該元素從隊列中抽走。
 
    同樣,如果該隊列爲空,試圖向隊列中抽取一個元素的線程將會阻塞,直到另一個線程向隊列中插入了一條新的元素。據此,把這個類稱作一個隊列顯然是誇大其詞了。它更多像是一個匯合點
 
 

 

 

ArrayBlockingQueue 阻塞隊列

ArrayBlockingQueue 類圖

 

如上圖 ArrayBlockingQueue 內部有個數組 items 用來存放隊列元素,putindex 下標標示入隊元素下標,

 

takeIndex 是出隊下標,count 統計隊列元素個數,從定義可知道並沒有使用 volatile 修飾,這是因爲訪問這些變量使用都是在鎖塊內,並不存在可見性問題。

 

另外有個獨佔鎖 lock 用來對出入隊操作加鎖,這導致同時只有一個線程可以訪問入隊出隊,另外 notEmpty,notFull 條件變量用來進行出入隊的同步。

 

另外構造函數必須傳入隊列大小參數,所以爲有界隊列,默認是 Lock 爲非公平鎖。

public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
    throw new IllegalArgumentException();
    this.items = new Object[capacity];
    lock = new ReentrantLock(fair);
    notEmpty = lock.newCondition();
    notFull = lock.newCondition();
}

ps:

所謂公平鎖:就是在併發環境中,每個線程在獲取鎖時會先查看此鎖維護的等待隊列,如果爲空,或者當前線程線程是等待隊列的第一個,就佔有鎖,否則就會加入到等待隊列中,以後會按照 FIFO 的規則從隊列中取到自己。
 
非公平鎖:比較粗魯,上來就直接嘗試佔有鎖,如果嘗試失敗,就再採用類似公平鎖那種方式
 

 

ArrayBlockingQueue 方法

offer 方法

 
在隊尾插入元素,如果隊列滿則返回 false,否者入隊返回 true。
public boolean offer(E e) {
//e 爲 null,則拋出 NullPointerException 異常
checkNotNull(e);
//獲取獨佔鎖
final ReentrantLock lock = this.lock;
lock.lock();
try {
//如果隊列滿則返回 false
if (count == items.length)
return false;
else {
//否者插入元素
insert(e);
return true;
}
} finally {
//釋放鎖
lock.unlock();
} }
private void insert(E x) {
//元素入隊
items[putIndex] = x;
//計算下一個元素應該存放的下標
putIndex = inc(putIndex);
++count;
notEmpty.signal();
}
//循環隊列,計算下標
final int inc(int i) {
return (++i == items.length) ? 0 : i;
}
 
這裏由於在操作共享變量前加了鎖,所以不存在內存不可見問題,加過鎖後獲取的共享變量都是從主內存獲取的,
 
而不是在 CPU 緩存或者寄存器裏面的值,釋放鎖後修改的共享變量值會刷新會主內存中。
 
另外這個隊列是使用循環數組實現,所以計算下一個元素存放下標時候有些特殊。
 
另外 insert 後調用notEmpty.signal();
 
是爲了激活調用 notEmpty.await()阻塞後放入 notEmpty 條件隊列中的線程。
 
 

Put 操作

 

在隊列尾部添加元素,如果隊列滿則等待隊列有空位置插入後返回。

public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
//獲取可被中斷鎖
lock.lockInterruptibly();
try {
//如果隊列滿,則把當前線程放入 notFull 管理的條件隊列
while (count == items.length)
notFull.await();
//插入元素
insert(e);
} finally {
lock.unlock();
} 
}

 

需要注意的是如果隊列滿了那麼當前線程會阻塞,知道出隊操作調用了 notFull.signal 方法激活該線程。
 
代碼邏輯很簡單,但是這裏需要思考一個問題爲啥調用 lockInterruptibly 方法而不是 Lock 方法。
 
我的理解是因爲調用了條件變量的 await()方法,而 await()方法會在中斷標誌設置後拋出 InterruptedException 異常後退出,所以還不如在加鎖時候先看中斷標誌是不是被設置了,如果設置了直接拋出 InterruptedException 異常,就不用再去獲取鎖了。
 
 
然後 看了其他併發類裏面凡是調用了 await 的方法獲取鎖時候都是使用的 lockInterruptibly 方法而不是 Lock 也驗證了這個想法

 

Poll 操作

 

從隊頭獲取並移除元素,隊列爲空,則返回 null

public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//當前隊列爲空則返回 null,否者
return (count == 0) ? null : extract();
} finally {
lock.unlock();
} 
}
private E extract() {
final Object[] items = this.items;
//獲取元素值
E x = this.<E>cast(items[takeIndex]);
//數組中值值爲 null;
items[takeIndex] = null;
//隊頭指針計算,隊列元素個數減一
takeIndex = inc(takeIndex);
--count;
//發送信號激活 notFull 條件隊列裏面的線程
notFull.signal();
return x;
}

Take 操作

 
 
從隊頭獲取元素,如果隊列爲空則阻塞直到隊列有元素。
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//隊列爲空,則等待,直到隊列有元素
while (count == 0)
notEmpty.await();
return extract();
} finally {
lock.unlock();
} 

}
 

需要注意的是如果隊列爲空

 
當前線程會被掛起放到 notEmpty 的條件隊列裏面,直到入隊操作執行調用 notEmpty.signal 後當前線程纔會被激活,await 纔會返回
 

Peek 操作

返回隊列頭元素但不移除該元素,隊列爲空,返回 null。
 
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//隊列爲空返回 null,否者返回頭元素
return (count == 0) ? null : itemAt(takeIndex);
} finally {
lock.unlock();
} 
}
final E itemAt(int i) {
return this.<E>cast(items[i]);
}

Size 操作

 
 
獲取隊列元素個數,非常精確因爲計算 size 時候加了獨佔鎖,其他線程不能入隊或者出隊或者刪除元素。
 
public int size() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return count;
} finally {
lock.unlock();
} 
}

 

 

ArrayBlockingQueue 小結

 
ArrayBlockingQueue 通過使用全局獨佔鎖實現同時只能有一個線程進行入隊或者出隊操作,這個鎖的粒度比較大,有點類似在方法上添加 synchronized 的意味。
 
 
其中 offer,poll 操作通過簡單的加鎖進行入隊出隊操作,而 put,take 則使用了條件變量實現如果隊列滿則等待,如果隊列空則等待,然後分別在出隊和入隊操作中發送信號激活等待線程實現同步。
 
 
另外相比 LinkedBlockingQueue,ArrayBlockingQueue 的 size 操作的結果是精確的,因爲計算前加了全局鎖。
 

 

 

ArrayBlockingQueue 示例

 
需求:在多線程操作下,一個數組中最多隻能存入 3 個元素。多放入不可以存入數組,或等待某線程對數組中某個元素取走才能放入,要求使用 java 的多線程來實現。
 
package com.hy.多線程高併發;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
* @Description:
 * ArrayBlockingQueue 示例
 * 需求:在多線程操作下,一個數組中最多隻能存入 3 個元素。
 * 多放入不可以存入數組,或等待某線程對數組中某個元素取走才能放入
 * 要求使用 java 的多線程來實現。
* @Param:
* @return:
* @Author: Hy
* @Date: 2019
*/
public class BlockingQueueTest {

    public static void main(String[] args) {

        final BlockingQueue queue = new ArrayBlockingQueue(3);
        for(int i=0;i<2;i++){
            new Thread(){
                public void run(){
                    while(true){
                        try {
                            Thread.sleep((long)(Math.random()*1000));
                            System.out.println(Thread.currentThread().getName() + "準備放數據!");
                            queue.put(1);
                            System.out.println(Thread.currentThread().getName() + "已經放了數據," +
                                    "隊列目前有" + queue.size() + "個數據");
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        } } }
            }.start();
        }
        new Thread(){
            public void run(){
                while(true){
                    try {
//將此處的睡眠時間分別改爲 100 和 1000,觀察運行結果
                        Thread.sleep(100);
                        System.out.println(Thread.currentThread().getName() + "準備取數據!");
                        System.err.println(queue.take());
                        System.out.println(Thread.currentThread().getName() + "已經取走數據," +
                                "隊列目前有" + queue.size() + "個數據");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();
    }
}

LinkedBlockingQueue 阻塞隊列

 

LinkedBlockingQueue 類圖

 
LinkedBlockingQueue 中也有兩個 Node 分別用來存放首尾節點,並且裏面有個初始值爲 0 的原子變量 count用來記錄隊列元素個數,另外裏面有兩個 ReentrantLock 的獨佔鎖,分別用來控制元素入隊和出隊加鎖,其中 takeLock 用來控制同時只有一個線程可以從隊列獲取元素,其他線程必須等待
 
 
putLock 控制同時只能有一個線程可以獲取鎖去添加元素,其他線程必須等待。另外 notEmpty 和 notFull 用來實現入隊和出隊的同步。
 
 
另外由於出入隊是兩個非公平獨佔鎖,所以可以同時又一個線程入隊和一個線程出隊,其實這個是個生產者-消費者模型,如下類圖:
 
 
/** 通過 take 取出進行加鎖、取出 */
    private final ReentrantLock takeLock = new ReentrantLock();
    /** 等待中的隊列等待取出 */
    private final Condition notEmpty = takeLock.newCondition();
    /*通過 put 放置進行加鎖、放置*/
    private final ReentrantLock putLock = new ReentrantLock();
    /** 等待中的隊列等待放置 */
    private final Condition notFull = putLock.newCondition();
    /* 記錄集合中的個數(計數器) */
    private final AtomicInteger count = new AtomicInteger(0);

 

LinkedBlockingQueue 方法

 

ps:下面介紹 LinkedBlockingQueue 用到很多 Lock 對象。詳細可以查找 Lock 對象的介紹

 

帶時間的 Offer 操作-生產者

 
 
在 ArrayBlockingQueue 中已經簡單介紹了 Offer()方法,LinkedBlocking 的 Offer 方法類似,在此就不過多去介紹。
 
 
這次我們從介紹下帶時間的 Offer 方法
 

    public boolean offer(E e, long timeout, TimeUnit unit)
            throws InterruptedException {
//空元素拋空指針異常
        if (e == null) throw new NullPointerException();
        long nanos = unit.toNanos(timeout);
        int c = -1;
        final ReentrantLock putLock = this.putLock;
        final AtomicInteger count = this.count;
//獲取可被中斷鎖,只有一個線程克獲取
        putLock.lockInterruptibly();
        try {
//如果隊列滿則進入循環
            while (count.get() == capacity) {
//nanos<=0 直接返回
                if (nanos <= 0)
                    return false;
//否者調用 await 進行等待,超時則返回<=0(1)
                nanos = notFull.awaitNanos(nanos);
            }
//await 在超時時間內返回則添加元素(2)
            enqueue(new Node<E>(e));
            c = count.getAndIncrement();
//隊列不滿則激活其他等待入隊線程(3)
            if (c + 1 < capacity)
                notFull.signal();
        } finally {
//釋放鎖
            putLock.unlock();
        }
//c==0 說明隊列裏面有一個元素,這時候喚醒出隊線程(4)
        if (c == 0)
            signalNotEmpty();
        return true;
    }
    private void enqueue(Node<E> node) {
        last = last.next = node;
    }
    private void signalNotEmpty() {
        final ReentrantLock takeLock = this.takeLock;
        takeLock.lock();
        try {
            notEmpty.signal();
        } finally {
            takeLock.unlock();
        } 
    }

 

帶時間的 poll 操作-消費者

 
 
獲取並移除隊首元素,在指定的時間內去輪詢隊列看有沒有首元素有則返回,否者超時後返回 null。
 
 public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        E x = null;
        int c = -1;
        long nanos = unit.toNanos(timeout);
        final AtomicInteger count = this.count;
        final ReentrantLock takeLock = this.takeLock;
//出隊線程獲取獨佔鎖
        takeLock.lockInterruptibly();
        try {
//循環直到隊列不爲空
            while (count.get() == 0) {
//超時直接返回 null
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
//出隊,計數器減一
            x = dequeue();
            c = count.getAndDecrement();
//如果出隊前隊列不爲空則發送信號,激活其他阻塞的出隊線程
            if (c > 1)
                notEmpty.signal();
        } finally {
//釋放鎖
            takeLock.unlock();
        }
//當前隊列容量爲最大值-1 則激活入隊線程。
        if (c == capacity)
            signalNotFull();
        return x;
    }

 

首先獲取獨佔鎖,然後進入循環噹噹前隊列有元素纔會退出循環,或者超時了,直接返回 null。
 
超時前退出循環後,就從隊列移除元素,然後計數器減去一,如果減去 1 前隊列元素大於 1 則說明當前移除後隊列還有元素,那麼就發信號激活其他可能阻塞到當前條件信號的線程。
 

 

最後如果減去 1 前隊列元素個數=最大值,那麼移除一個後會騰出一個空間來,這時候可以激活可能存在的入隊阻塞線程。

 

 

 
 
 
 
 
 
 
 
 
 
 
 
 
 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章