java并發(fā)編程的藝術(shù)筆記第六章——java并發(fā)容器和框架

1、ConcurrentHashMap的實現(xiàn)原理與使用

1.1、為什么使用ConcurrentHashMap

  • HashMap非線程安全
  • HashTable讀寫都需要加鎖,效率低下
  • ConcurrentHashMap的鎖分段技術(shù)可以提高并發(fā)效率

1.2、ConcurrentHashMap的結(jié)構(gòu)

ConcurrentHashMap由Segment數(shù)組結(jié)構(gòu)和HashEntry數(shù)組結(jié)構(gòu)組成,Segement是一種可重入鎖,在ConcurrentHashMap扮演著鎖的角色;HashEntry用于存儲鍵值對數(shù)據(jù),一個ConcurrentHashMap中包含一個Segment數(shù)組,它是數(shù)組和鏈表結(jié)構(gòu)。一個Segment里包含一個HashEntry數(shù)組,當(dāng)對HashEntry數(shù)組進行修改操作時必須要獲取它對應(yīng)的Segment鎖。

1.3、ConcurrentHashMap的初始化

1.4、定位Segment

通過散列算法定位Segment,散列沖突

2、ConcurrentLinkedQueue

并發(fā)編程中實現(xiàn)線程安全的隊列有兩種方式,一種是阻塞隊列,一種是非阻塞隊列,非阻塞的實現(xiàn)方式可以通過CAS方式來實現(xiàn)。

ConcurrentLinkedQueue是一個基于鏈接節(jié)點的無界安全隊列。它采用先進先出的方式對節(jié)點進行排序,當(dāng)我們添加一個元素的時候,它會添加至隊列的隊尾,當(dāng)我們獲取元素的時候,它會返回隊列頭部的元素。

3、java中的阻塞隊列

阻塞隊列支持阻塞的添加/移除元素的方法。支持阻塞的插入的意思是:當(dāng)隊列已滿時,隊列會阻塞插入隊列的線程,直到隊列有空位;支持阻塞的移除的意思是:當(dāng)隊列為空時,隊列會阻塞移除隊列元素的線程,直到隊列中有新的元素添加進來。

阻塞隊列場用于生產(chǎn)/消費者模式,生產(chǎn)者是向隊列中添加元素的線程,消費者是從隊列中獲取元素的線程,而阻塞隊列在其中充當(dāng)著容器的角色。
阻塞隊列的插入和移除有四種操作方式,詳情請參考文檔。

java中有7中阻塞隊列,分別是:

  • ArrayBlockingQueue:一個由數(shù)組結(jié)構(gòu)組成的有界阻塞隊列
  • LinkedBlockingQueue:一個由鏈表結(jié)構(gòu)組成的有界阻塞隊列。
  • PriorityBlockingQueue:一個支持優(yōu)先級排序的無界阻塞隊列。
  • DelayQueue:一個支持延時獲取元素的無界阻塞隊列。
  • SynchronousQueue:一個不存儲元素的阻塞隊列。
  • LinkedTransferQueue:一個由鏈表結(jié)構(gòu)組成的無界阻塞隊列。
  • LinkedBlockingDeque:一個由鏈表結(jié)構(gòu)組成的雙向阻塞隊列。

著重介紹下DelayQueue,它可以運用在以下業(yè)務(wù)場景:

  • 緩存系統(tǒng)的設(shè)置:可以用DelayQueue保存元素的有效期,使用一個線程無限循環(huán)查詢DelayQueue,一旦能從DelayQueue中獲取到元素,則說明緩存有效期到了。
  • 使用DelayQueue保存當(dāng)天將會執(zhí)行的任務(wù)和時間,一旦從DelayQueue獲取到任務(wù),就開始執(zhí)行,TimeQueue就是使用DelayQueue來實現(xiàn)的。

在公司有這么一個業(yè)務(wù)場景:訂單支付后要給商戶發(fā)送相應(yīng)的通知,針對同一條通知記錄,如果是第一次發(fā),則需要等待的時間是0分鐘,第二次發(fā)則需要等待1分鐘,第三次發(fā)則需要等待3分鐘,即發(fā)送次數(shù)每+1,則需要等待的時長也要相應(yīng)的增加,那使用DelayQueue就能很好的實現(xiàn)這個功能了。以下是參考實現(xiàn):

package main.java.com.robot.demo;

import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @author: 會跳舞的機器人
 * @date: 2017/8/17 14:43
 * @description: 通知任務(wù)
 */
public class NotifyTask implements Delayed, Runnable {
    /**
     * 任務(wù)名稱
     */
    private String notifyTaskName;

    /**
     * 執(zhí)行時間,單位:毫秒
     */
    private long executeTime;

    public NotifyTask(String notifyTaskName, long executeTime) {
        this.notifyTaskName = notifyTaskName;
        this.executeTime = executeTime;
    }


    /**
     * 獲取延遲時間
     *
     * @param unit
     * @return 返回當(dāng)前元素還需要延長多久時間
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(executeTime - System.currentTimeMillis(), unit.MILLISECONDS);
    }

    /**
     * 用來指定元素的順序,讓延時時間最長的放在隊列的末尾
     *
     * @param o
     * @return
     */
    @Override
    public int compareTo(Delayed o) {
        NotifyTask notifyTask = (NotifyTask) o;
        return executeTime > notifyTask.executeTime ? 1 : (executeTime < notifyTask.executeTime ? -1 : 0);
    }

    @Override
    public void run() {
        System.out.println("當(dāng)前時間毫秒數(shù):" + System.currentTimeMillis() + ",線程:" + this.toString() + "正在執(zhí)行");

    }

    @Override
    public String toString() {
        return "NotifyTask{" +
                "notifyTaskName='" + notifyTaskName + '\'' +
                ", executeTime=" + executeTime +
                '}';
    }
}

測試:

package main.java.com.robot.demo;

import java.util.Random;
import java.util.concurrent.DelayQueue;

/**
 * @author: 會跳舞的機器人
 * @date: 2017/8/17 14:52
 * @description: 延遲隊列DelayQueue測試
 */
public class NotifyTaskTest {
    /**
     * 通知任務(wù)存放的延遲隊列
     */
    private static DelayQueue<NotifyTask> tasks = new DelayQueue<>();


    public static void main(String[] args) {
        Random random = new Random();
        for (int i = 0; i < 5; i++) {
            // 隨機產(chǎn)生一個秒數(shù)
            int seconds = random.nextInt(5);
            NotifyTask notifyTask = new NotifyTask("任務(wù)" + i, System.currentTimeMillis() + (seconds * 1000));
            tasks.put(notifyTask);
        }
        while (true) {
            NotifyTask notifyTask = tasks.poll();
            if (notifyTask != null) {
                notifyTask.run();
            }
            // 如果隊列中的元素全部被取完,則跳出循環(huán)
            if (tasks.size() == 0) {
                break;
            }
        }
    }
}

控制臺輸出:

當(dāng)前時間毫秒數(shù):1502953649855,線程:NotifyTask{notifyTaskName='任務(wù)1', executeTime=1502953649855}正在執(zhí)行
當(dāng)前時間毫秒數(shù):1502953649855,線程:NotifyTask{notifyTaskName='任務(wù)3', executeTime=1502953649855}正在執(zhí)行
當(dāng)前時間毫秒數(shù):1502953650855,線程:NotifyTask{notifyTaskName='任務(wù)0', executeTime=1502953650855}正在執(zhí)行
當(dāng)前時間毫秒數(shù):1502953651855,線程:NotifyTask{notifyTaskName='任務(wù)2', executeTime=1502953651855}正在執(zhí)行
當(dāng)前時間毫秒數(shù):1502953651855,線程:NotifyTask{notifyTaskName='任務(wù)4', executeTime=1502953651855}正在執(zhí)行

從輸出中可以看出任務(wù)的執(zhí)行時間都是我們創(chuàng)建任務(wù)的時候指定的時間。

4、Fork/Join框架

4.1、什么是Fork/Join框架

Fork/Join框架是Java 7提供的一個用于并行執(zhí)行任務(wù)的框架,是一個把大任務(wù)分割成若干
個小任務(wù),最終匯總每個小任務(wù)結(jié)果后得到大任務(wù)結(jié)果的框架。

我們再通過Fork和Join這兩個單詞來理解一下Fork/Join框架。Fork就是把一個大任務(wù)切分
為若干子任務(wù)并行的執(zhí)行,Join就是合并這些子任務(wù)的執(zhí)行結(jié)果,最后得到這個大任務(wù)的結(jié)
果。比如計算1+2+…+10000,可以分割成10個子任務(wù),每個子任務(wù)分別對1000個數(shù)進行求和,
最終匯總這10個子任務(wù)的結(jié)果。

4.2、工作竊取算法

工作竊取(work-stealing)算法是指某個線程從其他隊列里竊取任務(wù)來執(zhí)行。那么,為什么
需要使用工作竊取算法呢?假如我們需要做一個比較大的任務(wù),可以把這個任務(wù)分割為若干
互不依賴的子任務(wù),為了減少線程間的競爭,把這些子任務(wù)分別放到不同的隊列里,并為每個
隊列創(chuàng)建一個單獨的線程來執(zhí)行隊列里的任務(wù),線程和隊列一一對應(yīng)。比如A線程負(fù)責(zé)處理A
隊列里的任務(wù)。但是,有的線程會先把自己隊列里的任務(wù)干完,而其他線程對應(yīng)的隊列里還有
任務(wù)等待處理。干完活的線程與其等著,不如去幫其他線程干活,于是它就去其他線程的隊列
里竊取一個任務(wù)來執(zhí)行。而在這時它們會訪問同一個隊列,所以為了減少竊取任務(wù)線程和被
竊取任務(wù)線程之間的競爭,通常會使用雙端隊列,被竊取任務(wù)線程永遠從雙端隊列的頭部拿
任務(wù)執(zhí)行,而竊取任務(wù)的線程永遠從雙端隊列的尾部拿任務(wù)執(zhí)行。

工作竊取算法的優(yōu)點:充分利用線程進行并行計算,減少了線程間的競爭。

工作竊取算法的缺點:在某些情況下還是存在競爭,比如雙端隊列里只有一個任務(wù)時。并
且該算法會消耗了更多的系統(tǒng)資源,比如創(chuàng)建多個線程和多個雙端隊列。

4.3、使用Fork/Join框架

讓我們通過一個簡單的需求來使用Fork/Join框架,需求是:計算1+2+3+4的結(jié)果。

package main.java.com.robot.demo;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;

/**
 * @author: 會跳舞的機器人
 * @date: 2017/8/17 15:45
 * @description: Fork/Join框架簡單demo
 */
public class CountTask extends RecursiveTask<Integer> {
    /**
     * 閾值
     */
    private static final int THRESHOLD = 2;

    private int start;

    private int end;

    public CountTask(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        // 判斷任務(wù)是否足夠小,足夠小就直接計算
        boolean canCompute = (end - start) <= THRESHOLD;
        if (canCompute) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // 如果任務(wù)大于閾值,就分解成兩個任務(wù)執(zhí)行
            int midel = (end - start) / 2;
            CountTask leftTask = new CountTask(start, midel);
            CountTask rightTask = new CountTask(midel + 1, end);
            // 執(zhí)行子任務(wù)
            leftTask.fork();
            rightTask.fork();
            // 等待計算結(jié)果,合并子任務(wù)
            int leftResult = leftTask.join();
            int rightResult = rightTask.join();
            sum = leftResult + rightResult;
        }
        return sum;
    }

    public static void main(String[] args) {
        ForkJoinPool forkJoinPool = new ForkJoinPool();
        // 生成一個計算任務(wù),負(fù)責(zé)計算1+2+3+4
        CountTask task = new CountTask(1, 4);
        // 執(zhí)行一個任務(wù)
        Future<Integer> result = forkJoinPool.submit(task);
        try {
            System.out.println(result.get());
        } catch (InterruptedException e) {
        } catch (ExecutionException e) {
        }
    }
}

4.4、Fork/Join框架設(shè)計及其實現(xiàn)原理

參考書本

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內(nèi)容