java 的 CopyOnWriteArrayList類

初識CopyOnWriteArrayList

第一次見到CopyOnWriteArrayList,是在研究JDBC的時候,每一個數據庫的Driver都是維護在一個CopyOnWriteArrayList中的,為了證明這一點,貼兩段代碼,第一段在com.mysql.jdbc.Driver下,也就是我們寫Class.forName(“…”)中的內容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Driver extends NonRegisteringDriver
??implements java.sql.Driver
{
??public Driver()
????throws SQLException
??{
??}
??static
??{
????try
????{
??????DriverManager.registerDriver(new Driver());
????} catch (SQLException E) {
??????throw new RuntimeException("Can't register driver!");
????}
??}
}

看到com.mysql.jdbc.Driver調用了DriverManager的registerDriver方法,這個類在java.sql.DriverManager下:

1
2
3
4
5
6
7
8
9
10
11
12
public class DriverManager
{
????private static final CopyOnWriteArrayList<DriverInfo>
????registeredDrivers = new CopyOnWriteArrayList();
????private static volatile int loginTimeout = 0;
????private static volatile PrintWriter logWriter = null;
????private static volatile PrintStream logStream = null;
????private static final Object logSync = new Object();
????static final SQLPermission SET_LOG_PERMISSION = new
????SQLPermission("setLog");
????...
}

看到所有的DriverInfo都在CopyOnWriteArrayList中。既然看到了CopyOnWriteArrayList,我自然免不了要研究一番為什么JDK使用的是這個List。

首先提兩點:

1、CopyOnWriteArrayList位于java.util.concurrent包下,可想而知,這個類是為并發而設計的

2、CopyOnWriteArrayList,顧名思義,Write的時候總是要Copy,也就是說對于CopyOnWriteArrayList,任何可變的操作(add、set、remove等等)都是伴隨復制這個動作的,后面會解讀CopyOnWriteArrayList的底層實現機制

四個關注點在CopyOnWriteArrayList上的答案

如何向CopyOnWriteArrayList中添加元素

對于CopyOnWriteArrayList來說,增加、刪除、修改、插入的原理都是一樣的,所以用增加元素來分析一下CopyOnWriteArrayList的底層實現機制就可以了。先看一段代碼:

1
2
3
4
5
6
public static void main(String[] args)
{
?????List<Integer> list = new CopyOnWriteArrayList<Integer>();
?????list.add(1);
?????list.add(2);
}

看一下這段代碼做了什么,先是第3行的實例化一個新的CopyOnWriteArrayList:

1
2
3
4
5
6
7
8
9
10
11
public class CopyOnWriteArrayList<E>
????implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
????private static final long serialVersionUID = 8673264195747942595L;
????/** The lock protecting all mutators */
????transient final ReentrantLock lock = new ReentrantLock();
????/** The array, accessed only via getArray/setArray. */
????private volatile transient Object[] array;
????...
}
1
2
3
public CopyOnWriteArrayList() {
????setArray(new Object[0]);
}
1
2
3
final void setArray(Object[] a) {
????array = a;
}

看到,對于CopyOnWriteArrayList來說,底層就是一個Object[] array,然后實例化一個CopyOnWriteArrayList,用圖來表示非常簡單:

就是這樣,Object array指向一個數組大小為0的數組。接著看一下,第4行的add一個整數1做了什么,add的源代碼是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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大致經歷了幾個步驟:

1、加鎖

2、拿到原數組,得到新數組的大小(原數組大小+1),實例化出一個新的數組來

3、把原數組的元素復制到新數組中去

4、新數組最后一個位置設置為待添加的元素(因為新數組的大小是按照原數組大小+1來的)

5、把Object array引用指向新數組

6、解鎖

整個過程看起來比較像ArrayList的擴容。有了這個基礎,我們再來看一下第5行的add了一個整數2做了什么,這應該非常簡單了,還是畫一張圖來表示:

和前面差不多,就不解釋了。

另外,插入、刪除、修改操作也都是一樣,每一次的操作都是以對Object[] array進行一次復制為基礎的,如果上面的流程看懂了,那么研究插入、刪除、修改的源代碼應該不難。

普通List的缺陷

常用的List有ArrayList、LinkedList、Vector,其中前兩個是線程非安全的,最后一個是線程安全的。我有一種場景,兩個線程操作了同一個List,分別對同一個List進行迭代和刪除,就如同下面的代碼:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static class T1 extends Thread
{
????private List<Integer> list;
????public T1(List<Integer> list)
????{
????????this.list = list;
????}
????public void run()
????{
????????for (Integer i : list)
????????{
????????}
????}
}
public static class T2 extends Thread
{
????private List<Integer> list;
????public T2(List<Integer> list)
????{
????????this.list = list;
????}
????public void run()
????{
????????for (int i = 0; i < list.size(); i++)
????????{
????????????list.remove(i);
????????}
????}
}

首先我在這兩個線程中放入ArrayList并啟動這兩個線程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args)
{
????List<Integer> list = new ArrayList<Integer>();
????for (int i = 0; i < 10000; i++)
????{
????????list.add(i);
????}
????T1 t1 = new T1(list);
????T2 t2 = new T2(list);
????t1.start();
????t2.start();
}

運行結果為:

1
2
3
4
Exception in thread "Thread-0" java.util.ConcurrentModificationException
????at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
????at java.util.AbstractList$Itr.next(AbstractList.java:343)
????at com.xrq.test60.TestMain$T1.run(TestMain.java:19)

把ArrayList換成LinkedList,main函數的代碼就不貼了,運行結果為:

1
2
3
4
Exception in thread "Thread-0" java.util.ConcurrentModificationException
????at java.util.LinkedList$ListItr.checkForComodification(LinkedList.java:761)
????at java.util.LinkedList$ListItr.next(LinkedList.java:696)
????at com.xrq.test60.TestMain$T1.run(TestMain.java:19)

可能有人覺得,這兩個線程都是線程非安全的類,所以不行。其實這個問題和線程安不安全沒有關系,換成Vector看一下運行結果:

1
2
3
4
Exception in thread "Thread-0" java.util.ConcurrentModificationException
????at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
????at java.util.AbstractList$Itr.next(AbstractList.java:343)
????at com.xrq.test60.TestMain$T1.run(TestMain.java:19)

Vector雖然是線程安全的,但是只是一種相對的線程安全而不是絕對的線程安全,它只能夠保證增、刪、改、查的單個操作一定是原子的,不會被打斷,但是如果組合起來用,并不能保證線程安全性。比如就像上面的線程1在遍歷一個Vector中的元素、線程2在刪除一個Vector中的元素一樣,勢必產生并發修改異常,也就是fail-fast。

CopyOnWriteArrayList的作用

把上面的代碼修改一下,用CopyOnWriteArrayList:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args)
{
????List<Integer> list = new CopyOnWriteArrayList<Integer>();
????for (int i = 0; i < 10; i++)
????{
????????list.add(i);
????}
????T1 t1 = new T1(list);
????T2 t2 = new T2(list);
????t1.start();
????t2.start();
}

可以運行一下這段代碼,是沒有任何問題的。

看到我把元素數量改小了一點,因為我們從上面的分析中應該可以看出,CopyOnWriteArrayList的缺點,就是修改代價十分昂貴,每次修改都伴隨著一次的數組復制;但同時優點也十分明顯,就是在并發下不會產生任何的線程安全問題,也就是絕對的線程安全,這也是為什么我們要使用CopyOnWriteArrayList的原因。

另外,有兩點必須講一下。我認為CopyOnWriteArrayList這個并發組件,其實反映的是兩個十分重要的分布式理念:

(1)讀寫分離

我們讀取CopyOnWriteArrayList的時候讀取的是CopyOnWriteArrayList中的Object[] array,但是修改的時候,操作的是一個新的Object[] array,讀和寫操作的不是同一個對象,這就是讀寫分離。這種技術數據庫用的非常多,在高并發下為了緩解數據庫的壓力,即使做了緩存也要對數據庫做讀寫分離,讀的時候使用讀庫,寫的時候使用寫庫,然后讀庫、寫庫之間進行一定的同步,這樣就避免同一個庫上讀、寫的IO操作太多

(2)最終一致

對CopyOnWriteArrayList來說,線程1讀取集合里面的數據,未必是最新的數據。因為線程2、線程3、線程4四個線程都修改了CopyOnWriteArrayList里面的數據,但是線程1拿到的還是最老的那個Object[] array,新添加進去的數據并沒有,所以線程1讀取的內容未必準確。不過這些數據雖然對于線程1是不一致的,但是對于之后的線程一定是一致的,它們拿到的Object[] array一定是三個線程都操作完畢之后的Object array[],這就是最終一致。最終一致對于分布式系統也非常重要,它通過容忍一定時間的數據不一致,提升整個分布式系統的可用性與分區容錯性。當然,最終一致并不是任何場景都適用的,像火車站售票這種系統用戶對于數據的實時性要求非常非常高,就必須做成強一致性的。

?

最后總結一點,隨著CopyOnWriteArrayList中元素的增加,CopyOnWriteArrayList的修改代價將越來越昂貴,因此,CopyOnWriteArrayList適用于讀操作遠多于修改操作的并發場景中

?

?

本文轉載于:?http://www.importnew.com/25034.html

?

?

?

結尾貼上兩個我測試的Demo 示例:

測試一:

package com.zslin.list.demo;import java.util.ArrayList;
import java.util.List;/*** * @author WQ<br>* @version 創建時間:2017年6月18日 下午4:15:54<br>*/
public class Resource3 {public static void main(String[] args) throws InterruptedException {List<String> a = new ArrayList<String>();a.add("a");a.add("b");a.add("c");final ArrayList<String> list = new ArrayList<String>(a);Thread t = new Thread(new Runnable() {int count = -1;@Overridepublic void run() {while (true) {list.add(count++ + "");}}});t.setDaemon(true);t.start();Thread.currentThread().sleep(3);for (String s : list) {System.out.println(s);}}
}

運行結果:

Exception in thread "main" java.util.ConcurrentModificationExceptionat java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)at java.util.ArrayList$Itr.next(ArrayList.java:851)at com.zslin.list.demo.Resource3.main(Resource3.java:31)

?

?

測試二:

package com.zslin.list.demo;import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;/*** * @author WQ<br>* @version 創建時間:2017年6月18日 下午4:17:48<br>*/
public class Resource4 {public static void main(String[] args) throws InterruptedException {List<String> a = new ArrayList<String>();a.add("a");a.add("b");a.add("c");final CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<String>(a);Thread t = new Thread(new Runnable() {int count = -1;@Overridepublic void run() {while (true) {list.add(count++ + "");}}});t.setDaemon(true);t.start();Thread.currentThread().sleep(3);for (String s : list) {System.out.println(list.hashCode());System.out.println(s);}}
}

運行結果:

159947112
a
1157371761
b
-478062346
c
-998300255
-1
-1122793921
0
1172437517
1
1152826799
2
-1744105465。。。。。。。。。//省略部分運行結果

?

轉載于:https://www.cnblogs.com/mr-wuxiansheng/p/7044571.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/252486.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/252486.shtml
英文地址,請注明出處:http://en.pswp.cn/news/252486.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

科技的趨勢!AI將進軍了37%的企業

2019獨角獸企業重金招聘Python工程師標準>>> 市場研究機構Gartner調查了全球89個國家的逾3,000名信息長&#xff08;CIO&#xff09;&#xff0c;顯示有37%的企業已經或打算于近期內部署人工智能&#xff08;AI&#xff09;&#xff0c;在4年內成長270%。Gartner研究…

CMakeLists.txt編寫規則

在PROJECT_SOURCE_DIR下新建了src, include, lib, bin四個子文件夾。 src文件夾用來存放所有的.cpp文件&#xff0c;include文件夾用來存儲所有的.h文件&#xff0c; lib中存放生成的自己編寫的共享庫&#xff0c; bin中存放所有的可執行文件 用SET來設置.exe可執行文件和共享…

nginx.conf配置詳解

######Nginx配置文件nginx.conf中文詳解######定義Nginx運行的用戶和用戶組 user www www;#nginx進程數&#xff0c;建議設置為等于CPU總核心數。 worker_processes 8;#全局錯誤日志定義類型&#xff0c;[ debug | info | notice | warn | error | crit ] error_log /usr/local…

更新 hadoop eclipse 插件

卸載hadoop 1.1.2插件。并安裝新版hadoop 2.2.0插件。 假設直接刪除eclipse plugin文件夾下的hadoop 1.1.2插件&#xff0c;會導致hadoop 1.1.2插件殘留在eclipse中&#xff0c;在eclipse perspective視圖中有Map/Reduce視圖&#xff0c;可是沒有圖標&#xff0c;新建項目也不會…

【K8S學習筆記】Part1:使用端口轉發訪問集群內的應用

本文介紹如何使用kubectl port-forward命令連接K8S集群中運行的Redis服務。這種連接方式有助于數據庫的調試工作。 注意&#xff1a;本文針對K8S的版本號為v1.9&#xff0c;其他版本可能會有少許不同。 0x00 準備工作 在進行該操作之前&#xff0c;需要滿足以下條件&#xff1a…

Ubuntu 16.04 桌面菜單欄 任務欄 標題欄消失的解決辦法

將home目錄下的.cache刪除掉就可以了 & cd & sudo rm -r ./.cache

瓜子二手車發12月二手車價格:漢蘭達奧德賽CR-V保值率居首

中新網1月22日電 日前&#xff0c;基于海量個人對個人的二手車成交數據&#xff0c;瓜子二手車公布了12月全國及多個核心城市的二手車交易“瓜子價”數據。數據顯示&#xff0c;2018年12月全國瓜子二手車嚴選直賣簽約均價為87934元&#xff0c;環比上漲0.16%&#xff0c;同比上…

概率分布之間的距離度量以及python實現(三)

概率分布之間的距離&#xff0c;顧名思義&#xff0c;度量兩組樣本分布之間的距離 。 1、卡方檢驗 統計學上的χ2統計量&#xff0c;由于它最初是由英國統計學家Karl Pearson在1900年首次提出的&#xff0c;因此也稱之為Pearson χ2&#xff0c;其計算公式為 (i1&#xff0c;2&…

Windows vs Linux:\r\n 與 \r

Linux 下文本文件的換行符為 \nWindows 下文本文件的換行符為 \r\n&#xff0c;占兩個字節&#xff1a; \r&#xff1a;歸位鍵&#xff08;CR&#xff09;&#xff0c;ascii 碼為 13\n&#xff1a;換行鍵&#xff08;LF&#xff09;&#xff0c;ascii 碼位 10也即單行無換行文本…

C++求職題

文章大部分內容轉載https://www.cnblogs.com/lanxuezaipiao/p/4127904.html 1.冒泡排序法&#xff1a; 如果有N個數字需要排序&#xff0c;那么需要進行(N-1)趟循環&#xff0c;第i趟循環需要對比的次數為(N-i)。所以可以用雙重循環&#xff0c;外層循環用于控制循環的趟數&a…

Python-爬蟲-requests

簡介 #介紹&#xff1a;使用requests可以模擬瀏覽器的請求&#xff0c;比起之前用到的urllib&#xff0c;requests模塊的api更加便捷&#xff08;本質就是封裝了urllib3&#xff09;#注意&#xff1a;requests庫發送請求將網頁內容下載下來以后&#xff0c;并不會執行js代碼&am…

JS如何監聽動畫結束

場景描述 在使用JS控制動畫時一般需要在動畫結束后執行回調去進行DOM的相關操作&#xff0c;所以需要監聽動畫結束進行回調。JS提供了以下事件用于監聽動畫的結束&#xff0c;簡單總結學習下。 CSS3動畫監聽事件 transitionEnd事件 transitionEnd事件會在CSS transition動畫結束…

封裝一個ViewPager真正的實現圖片無限循環滾動帶導航點

效果圖&#xff1a; 大家在寫項目的過程中常常會碰到須要實現Viewpager里面載入幾張圖片來循環自己主動輪播的效果&#xff0c;假設不封裝一下的話代碼分散在activity里面會顯得非常亂。并且也不利于我們下次復用&#xff0c;所以這里我把viewpager的相關代碼抽取出來放在了一個…

畢業論文頁眉頁腳頁碼插入

用word這么多年&#xff0c;第一次完整的操作了一遍頁眉頁腳頁碼的插入過程&#xff0c;其實三者都要要求奇偶頁不同 1.頁面布局-》右下角箭頭-》版式-》奇偶頁不同 因為文章不同的部分需要插入不同的頁眉頁腳頁碼&#xff0c;所以要在不同的部分插入分解符斷開它們的連接 2、…

巴黎市中心降下2019年第一場雪

當地時間1月22日&#xff0c;法國巴黎市中心降下2019年第一場雪&#xff0c;氣溫也隨之下降&#xff0c;街上的行人和車輛均有所減少。中新社記者 李洋 攝一對情侶在埃菲爾鐵塔前合影留念。無家可歸者在長椅上睡覺。游客在盧浮宮前拍照。

Echarts實現隱藏x軸,y軸,刻度線,網格

"yAxis": [{//就是一月份這個顯示為一個線段&#xff0c;而不是數軸那種一個點點"show" : true,"boundaryGap": true,"type": "category","name": "時間","data": ["1月", "2…

Ubuntu16.04 + Matlab2018+ desktop creation

https://blog.csdn.net/m0_37601622/article/details/82731879 https://blog.csdn.net/l18092482025/article/details/78906436 The second blog teaches you how to download a matlab.png when there is no matlab.png in /usr/share/applications/.

Atom插件主題推薦

注意事項 主題和插件這方面,比 Sublime Text 人性化多了..一些比較用心的作者增加了二度設置功能。 何為二度設置,就是不用手寫代碼修改配置文件&#xff0c;點點鼠標&#xff0c;填填輸入框就能生效&#xff0c;主題以 isotope-ui 這個做例子介紹,看圖&#xff1a; 進入二度設…

印尼發生洪災和山體滑坡 致多人死亡數千人撤離

當地時間1月23日&#xff0c;印尼南蘇拉威西省望加錫居民受洪水影像&#xff0c;用竹筏運送摩托車。近日&#xff0c;印尼南蘇拉威西省暴雨連連&#xff0c;造成洪災和山體滑坡。目前&#xff0c;暴雨引發的洪災和山體滑坡至少已造成8人死亡&#xff0c;數千人被迫撤離家園。。…

Django學習筆記第三篇--關于響應返回

一、返回簡單類型&#xff1a; 1 #1、返回簡單字符串 2 #from django.http import HttpResponse 3 return HttpResponse("return string") 4 #2、返回json 5 return HttpResponse(json.dumps(response_data),content_type"application/json") 二、返回文…