偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

記一個(gè) ConcurrentHashMap 使用不當(dāng)導(dǎo)致的并發(fā)事故

開發(fā)
對(duì)于這類涉及并發(fā)操作的重構(gòu),建議梳理清晰的數(shù)據(jù)流向并結(jié)合源碼工作流程加以推斷分析,最終明確問題風(fēng)險(xiǎn)點(diǎn)直接進(jìn)行邏輯修復(fù)并及時(shí)提測(cè)。

我們都知道ConcurrentHashMap可以保證鍵值對(duì)并發(fā)插入安全,因?yàn)槠鋕ey值唯一性的原因,所以hutool對(duì)其進(jìn)行了進(jìn)一步的封裝實(shí)現(xiàn)了一個(gè)ConcurrentHashSet,代碼如下,即判斷put后是否返回null,若是null則說明是第一次插入,反之就是存在重復(fù)元素,返回已存在的元素值。從而保證并發(fā)插入元素線程安全且唯一。

//hutool的ConcurrentHashSet通過判斷返回null得知之前是否插入過重復(fù)元素
@Override
 public boolean add(E e) {
  return map.put(e, PRESENT) == null;
 }

但是如果對(duì)于這些映射容器的鍵使用不當(dāng)就可能導(dǎo)致唯一鍵值對(duì)多次插入的情況,所以本文將基于筆者前段時(shí)間遇到的經(jīng)典的例子為切入點(diǎn),深入剖析該問題的原因和解決思路。

一、詳解ConcurrentHashMap并發(fā)重復(fù)插入問題

1. 需求說明

我們現(xiàn)在有這樣一個(gè)需求,大體是通過數(shù)據(jù)庫(kù)獲取要處理的任務(wù)并按照如下步驟執(zhí)行:

  • 從數(shù)據(jù)庫(kù)讀取未完成(status為0)的任務(wù),將其采用并發(fā)容器(ConcurrentHashSet)存放,key為這個(gè)任務(wù)對(duì)象
  • 工作線程處理,并在內(nèi)存中將其設(shè)置為1
  • 定時(shí)任務(wù)線程從容器中讀取這些任務(wù)并移除
  • 將已完成任務(wù)狀態(tài)寫回庫(kù)中

2. 落地代碼

對(duì)應(yīng)任務(wù)表的實(shí)體類封裝如下,我們的加載到ConcurrentHashSet會(huì)被多個(gè)線程并發(fā)的調(diào)度處理,處理過程中會(huì)并發(fā)更新狀態(tài)。

@Data
publicclass Task {
    
    privateint id;

    /**
     * 任務(wù)名稱
     */
    private String taskName;

    /**
     * 0.未開始
     * 1.進(jìn)行中
     * 2.已完成
     */
    privateint status;


}

對(duì)應(yīng)的實(shí)現(xiàn)代碼如下,可以看到從數(shù)據(jù)庫(kù)讀取未開始的任務(wù),線程1將其更新為處理完成后更新為處理中,線程2處理完成后更新為已完成:

public static void main(String[] args) throws InterruptedException {
        ConcurrentHashSet<Task> set = new ConcurrentHashSet<>();
        CountDownLatch countDownLatch = new CountDownLatch(2);

        //假設(shè)從數(shù)據(jù)庫(kù)讀取一個(gè)task
        Task task = new Task();
        task.setId(1);
        task.setTaskName("任務(wù)1");
        task.setStatus(0);
        set.add(task);


        //模擬多線程并發(fā)更新

        //線程1更新為處理中
        new Thread(() -> {
            log.info("線程1處理中....");
            task.setStatus(1);
            set.add(task);

            countDownLatch.countDown();
        }, "t1").start();

        //線程2更新為已完成
        new Thread(() -> {
            log.info("線程2處理中....");
            task.setStatus(2);
            set.add(task);

            countDownLatch.countDown();
        }, "t2").start();


        countDownLatch.await();

        log.info("set size:{}", set.size());
    }

輸出結(jié)果如下,可以看到明明同一個(gè)對(duì)象,結(jié)果插入了3次:

00:44:32.637 [main] INFO com.sharkChili.webTemplate.Main - set size:3

調(diào)試查看set內(nèi)部,3個(gè)元素都指向我們的唯一的任務(wù)-1。

3. 事故原因

我們都知道JDK8版本無論是HashMap還是ConcurrentHashMap底層采用數(shù)組+鏈表/紅黑樹,元素進(jìn)行插入前都需要進(jìn)行hash運(yùn)算定位數(shù)組索引,然后使用equal和hashCode比較的過程元素是否存在。 很明顯,我們上文并發(fā)操作元素時(shí)修改了status字典,導(dǎo)致每次得出的hashCode結(jié)果值改變了,進(jìn)而導(dǎo)致同一個(gè)元素因?yàn)椴煌膆ashCode插入到不同的位置,出現(xiàn)去重失敗:

對(duì)應(yīng)筆者也給出ConcurrentHashMap的put方法底層實(shí)現(xiàn):

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) thrownew NullPointerException();
        //計(jì)算key的hash值,因?yàn)槲覀儎?dòng)態(tài)修改了status導(dǎo)致hash值不同
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //因?yàn)閔ash值不同每次定位到的i位置不同,最終存到不同的位置
            elseif ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
  }
  .....
}

4. 解決方案

很明顯出現(xiàn)這個(gè)問題的原因就是因?yàn)椴l(fā)操作修改的status影響了hashcode計(jì)算結(jié)果,進(jìn)而導(dǎo)致并發(fā)操作變得無效,因?yàn)閕d是全局唯一的,所以直接重寫hashCode和equals方法,讓Task對(duì)象的計(jì)算和比對(duì)都通過id進(jìn)行:

@Data
publicclass Task {

//......略

   @Override
    public boolean equals(Object o) {
        if (this == o) returntrue;
        if (o == null || getClass() != o.getClass()) returnfalse;
        Task task = (Task) o;
        return id == task.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

二、小結(jié)

總的來說,對(duì)于這類涉及并發(fā)操作的重構(gòu),建議梳理清晰的數(shù)據(jù)流向并結(jié)合源碼工作流程加以推斷分析,最終明確問題風(fēng)險(xiǎn)點(diǎn)直接進(jìn)行邏輯修復(fù)并及時(shí)提測(cè)。

責(zé)任編輯:趙寧寧 來源: 寫代碼的SharkChili
相關(guān)推薦

2025-07-28 06:38:07

2021-09-11 19:00:54

Intro元素MemoryCache

2024-09-05 08:07:55

2022-10-25 18:00:00

Redis事務(wù)生產(chǎn)事故

2019-10-10 15:40:17

redisbug數(shù)據(jù)庫(kù)

2024-06-28 10:01:04

2022-06-21 11:24:05

多線程運(yùn)維

2021-06-10 06:59:34

Redis應(yīng)用API

2024-02-04 08:26:38

線程池參數(shù)內(nèi)存

2020-10-22 07:09:19

TCP網(wǎng)絡(luò)協(xié)議

2009-12-17 14:53:52

VS2008程序

2021-05-20 10:02:50

系統(tǒng)Redis技巧

2021-08-26 14:26:25

Java代碼集合

2010-01-06 10:56:47

華為交換機(jī)使用

2020-11-16 12:35:25

線程池Java代碼

2021-07-11 09:34:45

ArrayListLinkedList

2011-08-18 13:49:32

筆記本技巧

2022-04-08 08:48:16

線上事故日志訂閱者

2024-11-20 18:16:39

MyBatis批量操作數(shù)據(jù)庫(kù)

2024-08-20 21:27:04

docker部署容器
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)