您的位置:首頁 > 軟件教程 > 教程 > Doug Lea大師的佳作CopyOnWriteArrayList,用不好能坑死你!

Doug Lea大師的佳作CopyOnWriteArrayList,用不好能坑死你!

來源:好特整理 | 時(shí)間:2024-05-19 18:55:35 | 閱讀:165 |  標(biāo)簽: T wr COP 大師 Ri S C AR EA ug   | 分享到:

一、寫在開頭 我們在學(xué)習(xí)集合或者說容器的時(shí)候了解到,很多集合并非線程安全的,在并發(fā)場景下,為了保障數(shù)據(jù)的安全性,誕生了并發(fā)容器,廣為人知的有ConcurrentHashMap、ConcurrentLinkedQueue、BlockingQueue等,那你們知道ArrayList也有自己對應(yīng)的并發(fā)容器

一、寫在開頭

我們在學(xué)習(xí)集合或者說容器的時(shí)候了解到,很多集合并非線程安全的,在并發(fā)場景下,為了保障數(shù)據(jù)的安全性,誕生了并發(fā)容器,廣為人知的有ConcurrentHashMap、ConcurrentLinkedQueue、BlockingQueue等,那你們知道ArrayList也有自己對應(yīng)的并發(fā)容器嘛?

作為使用頻率最高的集合類之一,ArrayList線程不安全,我們在并發(fā)環(huán)境下使用,一般要輔以手動上鎖、或者通過Collections.synchronizedList()轉(zhuǎn)一手,為了解決這一問題,Doug Lea(道格.利)大師為我們提供了它的并發(fā)類—— CopyOnWriteArrayList

二、認(rèn)知CopyOnWriteArrayList

Doug Lea大師的佳作CopyOnWriteArrayList,用不好能坑死你!

CopyOnWriteArrayList 是java.util.concurrent的并發(fā)類,線程安全,遵循寫時(shí)復(fù)制的原則(CopyOnWrite),什么意思呢?就是我們在對列表進(jìn)行增刪改時(shí),會先創(chuàng)建一個列表的副本,在副本中完成增刪改操作后,再將副本替換原列表,整個過程舊的列表并沒有鎖定,因此原來的讀取操作仍可繼續(xù)。

看到這里細(xì)心的同學(xué)應(yīng)該已經(jīng)發(fā)現(xiàn)了它的“弊端”了,先賦值副本,寫完再替換,這是有時(shí)間差的,沒錯,這就是CopyOnWrite的延時(shí)更新策略,我們在發(fā)生寫的同時(shí),不阻塞讀,但讀取的只是舊列表中的數(shù)據(jù),直到引用替換完成,可以保證 數(shù)據(jù)的最終一致性 ,無法保證實(shí)時(shí)性。

三、實(shí)現(xiàn)原理(源碼)

我們來看一下CopyOnWriteArrayList底層的源碼實(shí)現(xiàn),首先在內(nèi)部維護(hù)了一個數(shù)組,用volatile關(guān)鍵字修飾,保證了數(shù)據(jù)的內(nèi)存可見性

/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;

讀。篻et()方法

public E get(int index) {
    return get(getArray(), index);
}
/**
 * Gets the array.  Non-private so as to also be accessible
 * from CopyOnWriteArraySet class.
 */
final Object[] getArray() {
    return array;
}
private E get(Object[] a, int index) {
    return (E) a[index];
}

這段源碼沒什么,很好理解,就是普通的讀取數(shù)組的操作,這也能看出CopyOnWriteArrayList的讀是不阻塞的。

新增:add()方法

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
	  //1. 使用Lock,保證寫線程在同一時(shí)刻只有一個
    lock.lock();
    try {
		//2. 獲取舊數(shù)組引用
        Object[] elements = getArray();
        int len = elements.length;
		//3. 創(chuàng)建新的數(shù)組,并將舊數(shù)組的數(shù)據(jù)復(fù)制到新數(shù)組中
        Object[] newElements = Arrays.copyOf(elements, len + 1);
		//4. 往新數(shù)組中添加新的數(shù)據(jù)
		newElements[len] = e;
		//5. 將舊數(shù)組引用指向新的數(shù)組
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
final void setArray(Object[] a) {
    array = a;
}

通過這段源碼,我們就能夠感知到前面描述的實(shí)現(xiàn)原理了, 首先 ,新增元素時(shí),內(nèi)部通過可重入鎖進(jìn)行鎖定,說明寫時(shí)會獨(dú)占, 然后 ,再將原數(shù)組賦值到一個新數(shù)組中, 最后 ,將舊數(shù)組的引用指向新數(shù)組。

四、使用注意事項(xiàng),用不好坑死你

對于CopyOnWriteArrayList的日常使用,和ArrayList幾乎一模一樣,在這里就不用過多介紹了,但它的使用還是需要注意的,雖然可以保證線程安全,但因其特性所致,僅適應(yīng)于讀多寫少的并發(fā)環(huán)境,對于頻繁寫入或者寫入的對象較大,一定不要使用CopyOnWriteArrayList容器,不然會坑死你的!

【舉個例子】

之前在這篇文章中:[EasyExcel導(dǎo)入導(dǎo)出百萬數(shù)據(jù)量]
采用了CopyOnWriteArrayList,以此來保證在多線程寫入數(shù)據(jù)庫時(shí)的線程安全,由于寫入的excel文件中有100萬的數(shù)據(jù)量,再導(dǎo)入的時(shí)候非常之慢,用了514秒!

Doug Lea大師的佳作CopyOnWriteArrayList,用不好能坑死你!

核心實(shí)現(xiàn)代碼如下,具體內(nèi)容實(shí)現(xiàn)可去看那篇文章哈

@Slf4j
@Service
public class EasyExcelImportHandler implements ReadListener {
    /*成功數(shù)據(jù)*/
    private final CopyOnWriteArrayList successList = new CopyOnWriteArrayList<>();
    /*單次處理?xiàng)l數(shù)*/
    private final static int BATCH_COUNT = 20000;
    @Resource
    private ThreadPoolExecutor threadPoolExecutor;
    @Resource
    private UserMapper userMapper;



    @Override
    public void invoke(User user, AnalysisContext analysisContext) {
        if(StringUtils.isNotBlank(user.getName())){
            successList.add(user);
            return;
        }
        if(successList.size() >= BATCH_COUNT){
            log.info("讀取數(shù)據(jù):{}", successList.size());
            saveData();
        }

    }

    /**
     * 采用多線程讀取數(shù)據(jù)
     */
    private void saveData() {
        List> lists = ListUtil.split(successList, 20000);
        CountDownLatch countDownLatch = new CountDownLatch(lists.size());
        for (List list : lists) {
            threadPoolExecutor.execute(()->{
                try {
                    userMapper.insertSelective(list.stream().map(o -> {
                        User user = new User();
                        user.setName(o.getName());
                        user.setId(o.getId());
                        user.setPhoneNum(o.getPhoneNum());
                        user.setAddress(o.getAddress());
                        return user;
                    }).collect(Collectors.toList()));
                } catch (Exception e) {
                    log.error("啟動線程失敗,e:{}", e.getMessage(), e);
                } finally {
                    //執(zhí)行完一個線程減1,直到執(zhí)行完
                    countDownLatch.countDown();
                }
            });
        }
        // 等待所有線程執(zhí)行完
        try {
            countDownLatch.await();
        } catch (Exception e) {
            log.error("等待所有線程執(zhí)行完異常,e:{}", e.getMessage(), e);
        }
        // 提前將不再使用的集合清空,釋放資源
        successList.clear();
        lists.clear();
    }

    /**
     * 所有數(shù)據(jù)讀取完成之后調(diào)用
     * @param analysisContext
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext analysisContext) {
        //讀取剩余數(shù)據(jù)
        if(CollectionUtils.isNotEmpty(successList)){
            log.info("讀取數(shù)據(jù):{}條",successList.size());
            saveData();
        }
    }
}

而將這段代碼中的CopyOnWriteArrayList換為ArrayList。

/*成功數(shù)據(jù)*/
// private final CopyOnWriteArrayList successList = new CopyOnWriteArrayList<>();
private final List successList =  new ArrayList<>();

導(dǎo)入100萬數(shù)據(jù)量的耗時(shí),直接從分鐘降為秒級,由此可見CopyOnWriteArrayList在寫入大對象時(shí)的性能非常之差!

Doug Lea大師的佳作CopyOnWriteArrayList,用不好能坑死你!

五、總結(jié)

通過以上的學(xué)習(xí),我們進(jìn)行總結(jié):CopyOnWriteArrayList的優(yōu)勢在于可以保證線程安全的同時(shí),不阻塞讀操作,但是這僅限于讀多寫少的情況;

在寫多讀少的情況下,或者寫入的對象占用內(nèi)容較大時(shí),不建議使用CopyOnWriteArrayList;CopyOnWrite 容器只能保證數(shù)據(jù)的最終一致性,不能保證數(shù)據(jù)的實(shí)時(shí)一致性。所以如果你希望寫入的的數(shù)據(jù),馬上能讀到,請不要使用 CopyOnWrite 容器,最好通過 ReentrantReadWriteLock 自定義一個的列表。

六、結(jié)尾彩蛋

如果本篇博客對您有一定的幫助,大家記得 留言+點(diǎn)贊+收藏 呀。原創(chuàng)不易,轉(zhuǎn)載請聯(lián)系Build哥!

小編推薦閱讀

好特網(wǎng)發(fā)布此文僅為傳遞信息,不代表好特網(wǎng)認(rèn)同期限觀點(diǎn)或證實(shí)其描述。

大師 2.0.2.2
大師 2.0.2.2
類型:動作冒險(xiǎn)  運(yùn)營狀態(tài):正式運(yùn)營  語言:中文   

游戲攻略

游戲禮包

游戲視頻

游戲下載

游戲活動

《大師》是游戲商MagicBirdsUSInfo開發(fā)的一款非常好玩的動作冒險(xiǎn)游戲。進(jìn)入游戲就可以獲得到一套神級裝備
大師漢化版 2.0.2
大師漢化版 2.0.2
類型:動作冒險(xiǎn)  運(yùn)營狀態(tài):正式運(yùn)營  語言:中文   

游戲攻略

游戲禮包

游戲視頻

游戲下載

游戲活動

2018,3d真戰(zhàn)斗手游“大師”的新年大片!獨(dú)立動作武俠手機(jī)游戲!下載即可得到一套神級裝備!旗艦級團(tuán)隊(duì)的

相關(guān)視頻攻略

更多

掃二維碼進(jìn)入好特網(wǎng)手機(jī)版本!

掃二維碼進(jìn)入好特網(wǎng)微信公眾號!

本站所有軟件,都由網(wǎng)友上傳,如有侵犯你的版權(quán),請發(fā)郵件[email protected]

湘ICP備2022002427號-10 湘公網(wǎng)安備:43070202000427號© 2013~2025 haote.com 好特網(wǎng)