一次錯誤使用 synchronized 同步鎖導致的問題


前幾天公司新人小A跑來問我,說他的一個 ArrayList 無法進行 add 操作了,讓我幫他看看。原來他使用一個 ArrayList 作為文件下載進度的存放隊列,再使用另一個線程不停地取隊列的對象寫到數據庫,是一個典型的生產者-消費者模型。簡化的實現代碼是這樣的:

private List<Progress> progressList = new ArrayList<>();
//生產線程
synchronized(progressList){
    progressList.add(new Progress());
}
//消費線程
while(true){
    synchronized(progressList){
        Progress progress = progressList.get(0);
        if(progress != null){
            progressList.remove(0);
            //work with data
        }
    }
}

問題顯而易見,為了保證列表的線程安全,代碼使用了 synchronized 關鍵字保證生產和消費的同步,問題出在把同步代碼塊外面加了死循環,導致這個鎖一直被消費線程持有,生產線程無法得到鎖自然無法進入操作列表的代碼塊,導致入隊一直失敗。

其實使用 List 和 synchronized 實現消費隊列是非常低效的做法,Java 並發包提供了 ArrayBlockingQueue、LinkedBlockingQueue 和 LinkedBlockingDequeue 三個線程安全的隊列非常適合用來實現生產者-消費者模型的,它們都實現了 BlockingQueue 接口,主要提供了 put 和 offer 兩個入隊方法以及 take 和 poll 兩個出隊方法,其中 offer 和 poll 可以設置阻塞超時時間,而 put 和 take 則會一直阻塞直到隊列可以進行入隊或出隊。

由於 LinkedBlockingQueue 使用鏈表實現,容量沒有限制,適合當前業務需要,可以用 LinkedBlockingQueue 將以上代碼進行重構,簡化代碼如下:

private BlockingQueue<Progress> progressQueue = new LinkedBlockingQueue<>();
//生產線程
progressQueue.put(new Progress());
//消費線程
while(true){
    Progress progress = progressQueue.take();
    //work with data
}

這樣一來既省去了自己進行線程同步的工作量,也使代碼更加精簡直觀。在實際開發中應該盡可能利用 Java 並發包提供的容器,而不是自己去實現線程同步。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM