為什麼有了併發安全的集合還需要讀寫鎖?

来源:https://www.cnblogs.com/zzyang/archive/2022/05/27/16319500.html
-Advertisement-
Play Games

大家好,我是三友,這篇文章想來跟大家來探討一下,在Java中已經提供了併發安全的集合,為什麼有的場景還需要使用讀寫鎖,直接用併發安全的集合難道不行麽? 在java中,併發安全的集合有很多,這裡我就選用常見的CopyOnWriteArrayList為例,來說明一下讀寫鎖的價值到底提現在哪。 CopyO ...


大家好,我是三友,這篇文章想來跟大家來探討一下,在Java中已經提供了併發安全的集合,為什麼有的場景還需要使用讀寫鎖,直接用併發安全的集合難道不行麽?

在java中,併發安全的集合有很多,這裡我就選用常見的CopyOnWriteArrayList為例,來說明一下讀寫鎖的價值到底提現在哪。

CopyOnWriteArrayList核心源碼分析

接下來我們分析一下CopyOnWriteArrayList核心的增刪改查的方法

成員變數

//獨占鎖
final transient ReentrantLock lock = new ReentrantLock();
//底層用來存放元素的數組
private transient volatile Object[] array;

add方法:往集合中添加某個元素

public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            newElements[len] = e;
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

add操作先通過lock加鎖,保證同一時刻最多只有一個線程可以操作。加鎖成功獲取到成員變數的數據,然後拷貝成員變數數組的元素到新的數組,再基於新的數據來添加元素,最後將新拷貝的數組通過setArray來替換舊的成員變數的數組。

remove方法:移除集合中的某個元素

public E remove(int index) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            E oldValue = get(elements, index);
            int numMoved = len - index - 1;
            if (numMoved == 0)
                setArray(Arrays.copyOf(elements, len - 1));
            else {
                Object[] newElements = new Object[len - 1];
                System.arraycopy(elements, 0, newElements, 0, index);
                System.arraycopy(elements, index + 1, newElements, index,
                                 numMoved);
                setArray(newElements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

remove操作也要先獲取到鎖。它先是取出對應數組下標的舊元素,然後新建了一個原數組長度減1的新數組,將除了被移除的元素之外,剩餘的元素拷貝到新的數組,最後再通過setArray替換舊的成員變數的數組。

set方法:將集合中指定位置的元素替換成新的元素

public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            Object[] elements = getArray();
            E oldValue = get(elements, index);

            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

set方法跟add,remove操作一樣得先獲取到鎖才能繼續執行。將原數組的原有元素拷貝到新的數組上,在新的數組完成數據的替換,最後也是通過setArray替換舊的成員變數的數組。


size方法:獲取集合中元素的個數

public int size() {
        return getArray().length;
}

size方法操作很簡單,就是簡單地返回一下當前數組的長度。

迭代器的構造

public Iterator<E> iterator() {
        return new COWIterator<E>(getArray(), 0);
}

構造COWIterator的時候傳入當前數組的對象,然後基於當前數組來遍歷,也不需要加鎖。

講完CopyOnWriteArrayList源碼,我們可以看出CopyOnWriteArrayList的核心原理就是在對數組進行增刪改的時候全部都是先加獨占鎖,然後對原有的數組進行拷貝,然後基於新複製的數組進行操作,最後將這個新的數組替換成員變數的數組;而對於讀的操作來說,都是不加鎖的,是基於當前成員變數的數組的這一時刻的快照來讀的。其實CopyOnWriteArrayList是基於一種寫時複製的思想,寫的時候基於新拷貝的數組來操作,之後再賦值給成員變數,讀的時候是原有的數組,這樣讀寫其實就是不是同一個數組,這樣就避免了讀寫衝突的情況,這其實也體現了一種讀寫分離的思想,讀寫操作的是不同的數組。

CopyOnWriteArrayList適用場景

接下來我們來思考一下,CopyOnWriteArrayList適合使用在什麼樣的場景中。通過上面源碼的分析,我們可以看出,所有的寫操作,包括增刪改都需要加同一把獨占鎖,所以同時只允許一個線程對數組進行拷貝賦值的操作,多線程併發情況下所有的操作都是串列執行的,勢必會導致併發能力降低,同時每次操作都涉及到了數組的拷貝,性能也不太好;而所有的讀操作都不需要加鎖,所以同一時間可以允許大量的線程同時讀,併發性能高。所以綜上我們可以得出一個結論,那就是CopyOnWriteArrayList適合讀多寫少的場景。

CopyOnWriteArrayList的局限性

說完CopyOnWriteArrayList,我們來想一想它有沒有什麼缺點。看起來CopyOnWriteArrayList除了寫的併發性能差點,好像沒有什麼缺點了。的確,單從性能來看,確實是這種情況,但是,從數據一致性的角度來看,CopyOnWriteArrayList的數據一致性能力較弱,屬於數據弱一致性。所謂的弱一致性,你可以這麼理解,在某一個時刻,讀到的數據並不是當前這一時刻最新的數據。

就拿CopyOnWriteArrayList舉例來說,當有個線程A正在調用add方法來添加元素,此時已經完成了數組的拷貝,並且也將元素添加到數組中,但是還沒有將新的數組賦值給成員變數,此時,另一個線程B來調用CopyOnWriteArrayList的size方法,來讀取集合中元素的個數,那麼此時讀到的元素個數其實是不包括線程A要添加的元素,因為線程A並沒有將新的數組賦值給成員變數,這就導致了線程B讀到的數據不是最新的數據,也就是跟實際的數據不一致。

所以,從上面我們可以看出,CopyOnWriteArrayList對於數據一致性的保證,還是比較弱的。其實不光是CopyOnWriteArrayList,其實Java中的很多集合,隊列的實現對於數據一致性的保證都比較弱。

如何來保證數據的強一致性

那麼有什麼好的辦法可以保證數據的強一致性麽?當然,保證併發安全,加鎖就可以完成,但是加什麼鎖可以保證數據讀寫安全和數據一致性,其實最簡單粗暴的方法就是對所有的讀寫都加上同一把獨占鎖,這樣保證所有的讀寫操作都是串列執行,那麼讀的時候,其他線程一定不能寫,那麼讀的一定是最新的數據。

如果真的這麼去加獨占鎖,的確能夠保證讀寫安全,但是性能卻會很差,這也是為什麼CopyOnWriteArrayList的讀不加鎖的原因,其實CopyOnWriteArrayList在設計的時候,就是降低數據一致性來換取讀的性能。

那有沒有什麼折中的方法,既能保證讀的性能不差,又能保證數據強一致性呢。這時就可以用讀寫鎖來實現。所謂的讀寫鎖,就是寫的時候,其他線程不能寫也不能讀,讀的時候,其他線程能讀,但是不能寫。也就是寫寫、讀寫互斥,但是讀讀不互斥。基於這種方式,就能保證讀的時候,一定沒有人在寫,這樣讀到的數據就一定是最新的,同時也能保證其他線程也能讀,不會出現上面舉例的那種情況了,也就能保證數據的強一致性。讀寫鎖相比獨占鎖而言,大大提高了讀的併發能力,但是寫的時候不能讀,相比於CopyOnWriteArrayList而言,讀的併發能力有所降低,這可能就是魚(併發性能)和熊掌(數據一致性)不可兼得吧。

Java中也提供了讀寫鎖的實現,ReentrantReadWriteLock,底層是基於AQS來實現的。有興趣的小伙伴可以翻一下源碼,看看是如何實現的,這裡就不再剖析源碼了。

總結

好了,通過這篇文章,想必大家知道為什麼有併發安全的集合之後,還需要讀寫鎖的原因,因為很多併發安全的集合對於數據一致性的保證是比較弱的,一旦遇到對於數據一致性要求比較高的場景,一些併發安全的集合就不適用了;同時為了避免獨占鎖帶來的性能問題,可以選擇讀寫鎖來保證讀的併發能力。小伙伴們在實際應用中需要根據應用場景來靈活地選擇使用併發安全的集合、讀寫鎖或者是獨占鎖,其實永遠沒有最好的選擇,只有更好的選擇。

以上就是本篇文章的全部內容,如果你有什麼不懂或者想要交流的地方,歡迎關註我的個人的微信公眾號 三友的java日記 ,我們下篇文章再見。

如果覺得這篇文章對你有所幫助,還請幫忙點贊、在看、轉發一下,碼字不易,非常感謝!

 

最近花了一個月的時間,整理了這套併發編程系列的知識點。涵蓋了 volitile、synchronized、CAS、AQS、鎖優化策略、同步組件、數據結構、線程池、Thread、ThreadLocal,幾乎覆蓋了所有的學習和麵試場景,如圖。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

文檔獲取方式:掃描二維碼或者搜一搜關註微信公眾號 三友的java日記 ,回覆 併發  就能獲取了。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1 Hadoop介紹 Hadoop是Apache旗下的一個用java語言實現開源軟體框架,是一個開發和運行處理大規模數據的軟體平臺。允許使用簡單的編程模型在大量電腦集群上對大型數據集進行分散式處理。狹義上說,Hadoop指Apache這款開源框架,它的核心組件有: HDFS(分散式文件系統):解決 ...
  • 最近在數據處理中用到了窗函數, 把使用方法記錄一下, 暫時只有分組排序和滑動時間視窗的例子, 以後再逐步添加. 在SQL查詢時, 會遇到有兩類需要分組統計的場景, 在之前的SQL語法中是不方便實現的. 使用窗函數直接SQL中使用窗函數就能解決這些問題, 否則需要使用臨時表, 函數或存儲過程進行處理.... ...
  • 本文介紹如何使用 CREATE DATABASE 語句創建資料庫、 CREATE TABLE 語句創建表、ALTER TABLE 語句更新表、DROP TABLE 語句刪除表。 一、表的創建 本節要點 表通過 CREATE TABLE 語句創建而成。 表和列的命名要使用有意義的文字。 指定列的數據類 ...
  • 5月24日,由華為開發者聯盟主辦的HUAWEI Developer Day(華為開發者日,簡稱HDD)線上沙龍·創新開發專場在華為開發者學堂及各大直播平臺與廣大開發者見面。直播內容主要聚焦HarmonyOS和HMS生態應用開發,帶來關於HarmonyOS服務卡片、HMS Core開放能力、應用高效開 ...
  • 二維碼和條形碼從發明到發展已經過去了幾十年,因其能快捷方便讀取信息的特點,在數字經濟時代被廣泛應用。掃描二維碼可以識別健康狀況,識別身份信息、訪問網站鏈接、完成金融支付等等,已經成為生活中不可或缺的實用技術,所以很多App都搭載了“掃一掃”功能。 然而,在日常掃碼過程中,我們也經常會遇到掃碼環境暗、 ...
  • 1.字元集/字元編碼是什麼? 字元集或者說字元編碼就是給字元定義了數值編號以及數值編號存儲格式。 嚴格來說字元集和字元編碼是兩個概念: charset 是 character set 的簡寫,即字元集。 encoding 是 charset encoding 的簡寫,即字元集編碼,簡稱編碼。 字元集 ...
  • vant 的表單校驗 個人理解: 將rules當成一個對象去理解,傳參時可以是整個對象或者對象的某一屬性 常用兩種校驗方式 1, 正則表達式 1.1自定義校驗規則(校驗規格也可傳入多條): 表單: :rules="[{ pattern:ageRules, message: '請填寫密碼' }]" d ...
  • 深居內陸的人們,大概每個人都有過大海之夢吧。夏日傍晚在沙灘漫步奔跑;或是在海上衝浪游泳;或是在海島游玩探險;亦或靜待日出日落……本文使用 React + Three.js 技術棧,實現 3D 海洋和島嶼,主要包含知識點包括:Tone Mapping、Water 類、Sky 類、Shader 著色、S... ...
一周排行
    -Advertisement-
    Play Games
  • 前言 在我們開發過程中基本上不可或缺的用到一些敏感機密數據,比如SQL伺服器的連接串或者是OAuth2的Secret等,這些敏感數據在代碼中是不太安全的,我們不應該在源代碼中存儲密碼和其他的敏感數據,一種推薦的方式是通過Asp.Net Core的機密管理器。 機密管理器 在 ASP.NET Core ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 順序棧的介面程式 目錄順序棧的介面程式頭文件創建順序棧入棧出棧利用棧將10進位轉16進位數驗證 頭文件 #include <stdio.h> #include <stdbool.h> #include <stdlib.h> 創建順序棧 // 指的是順序棧中的元素的數據類型,用戶可以根據需要進行修改 ...
  • 前言 整理這個官方翻譯的系列,原因是網上大部分的 tomcat 版本比較舊,此版本為 v11 最新的版本。 開源項目 從零手寫實現 tomcat minicat 別稱【嗅虎】心有猛虎,輕嗅薔薇。 系列文章 web server apache tomcat11-01-官方文檔入門介紹 web serv ...
  • C總結與剖析:關鍵字篇 -- <<C語言深度解剖>> 目錄C總結與剖析:關鍵字篇 -- <<C語言深度解剖>>程式的本質:二進位文件變數1.變數:記憶體上的某個位置開闢的空間2.變數的初始化3.為什麼要有變數4.局部變數與全局變數5.變數的大小由類型決定6.任何一個變數,記憶體賦值都是從低地址開始往高地 ...
  • 如果讓你來做一個有狀態流式應用的故障恢復,你會如何來做呢? 單機和多機會遇到什麼不同的問題? Flink Checkpoint 是做什麼用的?原理是什麼? ...
  • C++ 多級繼承 多級繼承是一種面向對象編程(OOP)特性,允許一個類從多個基類繼承屬性和方法。它使代碼更易於組織和維護,並促進代碼重用。 多級繼承的語法 在 C++ 中,使用 : 符號來指定繼承關係。多級繼承的語法如下: class DerivedClass : public BaseClass1 ...
  • 前言 什麼是SpringCloud? Spring Cloud 是一系列框架的有序集合,它利用 Spring Boot 的開發便利性簡化了分散式系統的開發,比如服務註冊、服務發現、網關、路由、鏈路追蹤等。Spring Cloud 並不是重覆造輪子,而是將市面上開發得比較好的模塊集成進去,進行封裝,從 ...
  • class_template 類模板和函數模板的定義和使用類似,我們已經進行了介紹。有時,有兩個或多個類,其功能是相同的,僅僅是數據類型不同。類模板用於實現類所需數據的類型參數化 template<class NameType, class AgeType> class Person { publi ...
  • 目錄system v IPC簡介共用記憶體需要用到的函數介面shmget函數--獲取對象IDshmat函數--獲得映射空間shmctl函數--釋放資源共用記憶體實現思路註意 system v IPC簡介 消息隊列、共用記憶體和信號量統稱為system v IPC(進程間通信機制),V是羅馬數字5,是UNI ...