Skip to content

Commit b142e7a

Browse files
committed
[docs add]CopyOnWriteArrayList 源码分析
1 parent aabba40 commit b142e7a

File tree

7 files changed

+343
-70
lines changed

7 files changed

+343
-70
lines changed

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,11 @@
6868

6969
**源码分析**
7070

71-
- [ArrayList 源码+扩容机制分析](./docs/java/collection/arraylist-source-code.md)
72-
- [LinkedList 源码分析](./docs/java/collection/linkedlist-source-code.md)
73-
- [HashMap(JDK1.8)源码+底层数据结构分析](./docs/java/collection/hashmap-source-code.md)
74-
- [ConcurrentHashMap 源码+底层数据结构分析](./docs/java/collection/concurrent-hash-map-source-code.md)
71+
- [ArrayList 核心源码+扩容机制分析](./docs/java/collection/arraylist-source-code.md)
72+
- [LinkedList 核心源码分析](./docs/java/collection/linkedlist-source-code.md)
73+
- [HashMap 核心源码+底层数据结构分析](./docs/java/collection/hashmap-source-code.md)
74+
- [ConcurrentHashMap 核心源码+底层数据结构分析](./docs/java/collection/concurrent-hash-map-source-code.md)
75+
- [CopyOnWriteArrayList 核心源码分析](./docs/java/collection/copyonwritearraylist-source-code.md)
7576

7677
### IO
7778

docs/.vuepress/sidebar/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export default sidebar({
8888
"linkedlist-source-code",
8989
"hashmap-source-code",
9090
"concurrent-hash-map-source-code",
91+
"copyonwritearraylist-source-code",
9192
],
9293
},
9394
],

docs/home.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,10 +60,11 @@ title: JavaGuide(Java学习&&面试指南)
6060

6161
**源码分析**
6262

63-
- [ArrayList 源码+扩容机制分析](./java/collection/arraylist-source-code.md)
64-
- [LinkedList 源码分析](./java/collection/linkedlist-source-code.md)
65-
- [HashMap(JDK1.8)源码+底层数据结构分析](./java/collection/hashmap-source-code.md)
66-
- [ConcurrentHashMap 源码+底层数据结构分析](./java/collection/concurrent-hash-map-source-code.md)
63+
- [ArrayList 核心源码+扩容机制分析](./java/collection/arraylist-source-code.md)
64+
- [LinkedList 核心源码分析](./java/collection/linkedlist-source-code.md)
65+
- [HashMap 核心源码+底层数据结构分析](./java/collection/hashmap-source-code.md)
66+
- [ConcurrentHashMap 核心源码+底层数据结构分析](./java/collection/concurrent-hash-map-source-code.md)
67+
- [CopyOnWriteArrayList 核心源码分析](./java/collection/copyonwritearraylist-source-code.md)
6768

6869
### IO
6970

docs/java/collection/arraylist-source-code.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ public class ArrayList<E> extends AbstractList<E>
2424
- `Cloneable` :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
2525
- `Serializable` : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
2626

27+
![ArrayList 类图](https://oss.javaguide.cn/github/javaguide/java/collection/arraylist-class-diagram.png)
28+
2729
### ArrayList 和 Vector 的区别?(了解即可)
2830

2931
- `ArrayList``List` 的主要实现类,底层使用 `Object[]`存储,适用于频繁的查找工作,线程不安全 。
Lines changed: 316 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,316 @@
1+
---
2+
title: CopyOnWriteArrayList 源码分析
3+
category: Java
4+
tag:
5+
- Java集合
6+
---
7+
8+
## CopyOnWriteArrayList 简介
9+
10+
在 JDK1.5 之前,如果想要使用并发安全的 `List` 只能选择 `Vector`。而 `Vector` 是一种老旧的集合,已经被淘汰。`Vector` 对于增删改查等方法基本都加了 `synchronized`,这种方式虽然能够保证同步,但这相当于对整个 `Vector` 加上了一把大锁,使得每个方法执行的时候都要去获得锁,导致性能非常低下。
11+
12+
JDK1.5 引入了 `Java.util.concurrent`(JUC)包,其中提供了很多线程安全且并发性能良好的容器,其中唯一的线程安全 `List` 实现就是 `CopyOnWriteArrayList` 。关于`java.util.concurrent` 包下常见并发容器的总结,可以看我写的这篇文章:[Java 常见并发容器总结](https://javaguide.cn/java/concurrent/java-concurrent-collections.html)
13+
14+
### CopyOnWriteArrayList 到底有什么厉害之处?
15+
16+
对于大部分业务场景来说,读取操作往往是远大于写入操作的。由于读取操作不会对原有数据进行修改,因此,对于每次读取都进行加锁其实是一种资源浪费。相比之下,我们应该允许多个线程同时访问 `List` 的内部数据,毕竟对于读取操作来说是安全的。
17+
18+
这种思路与 `ReentrantReadWriteLock` 读写锁的设计思想非常类似,即读读不互斥、读写互斥、写写互斥(只有读读不互斥)。`CopyOnWriteArrayList` 更进一步地实现了这一思想。为了将读操作性能发挥到极致,`CopyOnWriteArrayList` 中的读取操作是完全无需加锁的。更加厉害的是,写入操作也不会阻塞读取操作,只有写写才会互斥。这样一来,读操作的性能就可以大幅度提升。
19+
20+
`CopyOnWriteArrayList` 线程安全的核心在于其采用了 **写时复制(Copy-On-Write)** 的策略,从 `CopyOnWriteArrayList` 的名字就能看出了。
21+
22+
### Copy-On-Write 的思想是什么?
23+
24+
`CopyOnWriteArrayList`名字中的“Copy-On-Write”即写时复制,简称 COW。
25+
26+
下面是维基百科对 Copy-On-Write 的介绍,介绍的挺不错:
27+
28+
> 写入时复制(英语:Copy-on-write,简称 COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的。此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
29+
30+
这里再以 `CopyOnWriteArrayList`为例介绍:当需要修改( `add``set``remove` 等操作) `CopyOnWriteArrayList` 的内容时,不会直接修改原数组,而是会先创建底层数组的副本,对副本数组进行修改,修改完之后再将修改后的数组赋值回去,这样就可以保证写操作不会影响读操作了。
31+
32+
可以看出,写时复制机制非常适合读多写少的并发场景,能够极大地提高系统的并发性能。
33+
34+
不过,写时复制机制并不是银弹,其依然存在一些缺点,下面列举几点:
35+
36+
1. 内存占用:每次写操作都需要复制一份原始数据,会占用额外的内存空间,在数据量比较大的情况下,可能会导致内存资源不足。
37+
2. 写操作开销:每一次写操作都需要复制一份原始数据,然后再进行修改和替换,所以写操作的开销相对较大,在写入比较频繁的场景下,性能可能会受到影响。
38+
3. 数据一致性问题:修改操作不会立即反映到最终结果中,还需要等待复制完成,这可能会导致一定的数据一致性问题。
39+
4. ......
40+
41+
## CopyOnWriteArrayList 源码分析
42+
43+
这里以 JDK1.8 为例,分析一下 `CopyOnWriteArrayList` 的底层核心源码。
44+
45+
`CopyOnWriteArrayList` 的类定义如下:
46+
47+
```java
48+
public class CopyOnWriteArrayList<E>
49+
extends Object
50+
implements List<E>, RandomAccess, Cloneable, Serializable
51+
{
52+
//...
53+
}
54+
```
55+
56+
`CopyOnWriteArrayList` 实现了以下接口:
57+
58+
- `List` : 表明它是一个列表,支持添加、删除、查找等操作,并且可以通过下标进行访问。
59+
- `RandomAccess` :这是一个标志接口,表明实现这个接口的 `List` 集合是支持 **快速随机访问** 的。
60+
- `Cloneable` :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
61+
- `Serializable` : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
62+
63+
![CopyOnWriteArrayList 类图](https://oss.javaguide.cn/github/javaguide/java/collection/copyonwritearraylist-class-diagram.png)
64+
65+
### 初始化
66+
67+
`CopyOnWriteArrayList` 中有一个无参构造函数和两个有参构造函数。
68+
69+
```java
70+
// 创建一个空的 CopyOnWriteArrayList
71+
public CopyOnWriteArrayList() {
72+
setArray(new Object[0]);
73+
}
74+
75+
// 按照集合的迭代器返回的顺序创建一个包含指定集合元素的 CopyOnWriteArrayList
76+
public CopyOnWriteArrayList(Collection<? extends E> c) {
77+
Object[] elements;
78+
if (c.getClass() == CopyOnWriteArrayList.class)
79+
elements = ((CopyOnWriteArrayList<?>)c).getArray();
80+
else {
81+
elements = c.toArray();
82+
// c.toArray might (incorrectly) not return Object[] (see 6260652)
83+
if (elements.getClass() != Object[].class)
84+
elements = Arrays.copyOf(elements, elements.length, Object[].class);
85+
}
86+
setArray(elements);
87+
}
88+
89+
// 创建一个包含指定数组的副本的列表
90+
public CopyOnWriteArrayList(E[] toCopyIn) {
91+
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
92+
}
93+
```
94+
95+
### 插入元素
96+
97+
`CopyOnWriteArrayList``add()`方法有三个版本:
98+
99+
- `add(E e)`:在 `CopyOnWriteArrayList` 的尾部插入元素。
100+
- `add(int index, E element)`:在 `CopyOnWriteArrayList` 的指定位置插入元素。
101+
- `addIfAbsent(E e)`:如果指定元素不存在,那么添加该元素。如果成功添加元素则返回 true。
102+
103+
这里以`add(E e)`为例进行介绍:
104+
105+
```java
106+
// 插入元素到 CopyOnWriteArrayList 的尾部
107+
public boolean add(E e) {
108+
final ReentrantLock lock = this.lock;
109+
// 加锁
110+
lock.lock();
111+
try {
112+
// 获取原来的数组
113+
Object[] elements = getArray();
114+
// 原来数组的长度
115+
int len = elements.length;
116+
// 创建一个长度+1的新数组,并将原来数组的元素复制给新数组
117+
Object[] newElements = Arrays.copyOf(elements, len + 1);
118+
// 元素放在新数组末尾
119+
newElements[len] = e;
120+
// array指向新数组
121+
setArray(newElements);
122+
return true;
123+
} finally {
124+
// 解锁
125+
lock.unlock();
126+
}
127+
}
128+
```
129+
130+
从上面的源码可以看出:
131+
132+
- `add`方法内部用到了 `ReentrantLock` 加锁,保证了同步,避免了多线程写的时候会复制出多个副本出来。
133+
- `CopyOnWriteArrayList` 通过复制底层数组的方式实现写操作,即先创建一个新的数组来容纳新添加的元素,然后在新数组中进行写操作,最后将新数组赋值给底层数组的引用,替换掉旧的数组。这也就证明了我们前面说的:`CopyOnWriteArrayList` 线程安全的核心在于其采用了 **写时复制(Copy-On-Write)** 的策略。
134+
- 每次写操作都需要通过 `Arrays.copyOf` 复制底层数组,时间复杂度是 O(n) 的,且会占用额外的内存空间。因此,`CopyOnWriteArrayList` 适用于读多写少的场景,在写操作不频繁且内存资源充足的情况下,可以提升系统的性能表现。
135+
- `CopyOnWriteArrayList` 中并没有类似于 `ArrayList``grow()` 方法扩容的操作。
136+
137+
> `Arrays.copyOf` 方法的时间复杂度是 O(n),其中 n 表示需要复制的数组长度。因为这个方法的实现原理是先创建一个新的数组,然后将源数组中的数据复制到新数组中,最后返回新数组。这个方法会复制整个数组,因此其时间复杂度与数组长度成正比,即 O(n)。值得注意的是,由于底层调用了系统级别的拷贝指令,因此在实际应用中这个方法的性能表现比较优秀,但是也需要注意控制复制的数据量,避免出现内存占用过高的情况。
138+
139+
### 读取元素
140+
141+
`CopyOnWriteArrayList` 的读取操作是基于内部数组 `array` 并没有发生实际的修改,因此在读取操作时不需要进行同步控制和锁操作,可以保证数据的安全性。这种机制下,多个线程可以同时读取列表中的元素。
142+
143+
```java
144+
// 底层数组,只能通过getArray和setArray方法访问
145+
private transient volatile Object[] array;
146+
147+
public E get(int index) {
148+
return get(getArray(), index);
149+
}
150+
151+
final Object[] getArray() {
152+
return array;
153+
}
154+
155+
private E get(Object[] a, int index) {
156+
return (E) a[index];
157+
}
158+
```
159+
160+
不过,`get`方法是弱一致性的,在某些情况下可能读到旧的元素值。
161+
162+
`get(int index)`方法是分两步进行的:
163+
164+
1. 通过`getArray()`获取当前数组的引用;
165+
2. 直接从数组中获取下标为 index 的元素。
166+
167+
这个过程并没有加锁,所以在并发环境下可能出现如下情况:
168+
169+
1. 线程 1 调用`get(int index)`方法获取值,内部通过`getArray()`方法获取到了 array 属性值;
170+
2. 线程 2 调用`CopyOnWriteArrayList``add``set``remove` 等修改方法时,内部通过`setArray`方法修改了`array`属性的值;
171+
3. 线程 1 还是从旧的 `array` 数组中取值。
172+
173+
### 获取列表中元素的个数
174+
175+
```java
176+
public int size() {
177+
return getArray().length;
178+
}
179+
```
180+
181+
`CopyOnWriteArrayList`中的`array`数组每次复制都刚好能够容纳下所有元素,并不像`ArrayList`那样会预留一定的空间。因此,`CopyOnWriteArrayList`中并没有`size`属性`CopyOnWriteArrayList`的底层数组的长度就是元素个数,因此`size()`方法只要返回数组长度就可以了。
182+
183+
### 删除元素
184+
185+
`CopyOnWriteArrayList`删除元素相关的方法一共有 4 个:
186+
187+
1. `remove(int index)`:移除此列表中指定位置上的元素。将任何后续元素向左移动(从它们的索引中减去 1)。
188+
2. `boolean remove(Object o)`:删除此列表中首次出现的指定元素,如果不存在该元素则返回 false。
189+
3. `boolean removeAll(Collection<?> c)`:从此列表中删除指定集合中包含的所有元素。
190+
4. `void clear()`:移除此列表中的所有元素。
191+
192+
这里以`remove(int index)`为例进行介绍:
193+
194+
```java
195+
public E remove(int index) {
196+
// 获取可重入锁
197+
final ReentrantLock lock = this.lock;
198+
// 加锁
199+
lock.lock();
200+
try {
201+
//获取当前array数组
202+
Object[] elements = getArray();
203+
// 获取当前array长度
204+
int len = elements.length;
205+
//获取指定索引的元素(旧值)
206+
E oldValue = get(elements, index);
207+
int numMoved = len - index - 1;
208+
// 判断删除的是否是最后一个元素
209+
if (numMoved == 0)
210+
// 如果删除的是最后一个元素,直接复制该元素前的所有元素到新的数组
211+
setArray(Arrays.copyOf(elements, len - 1));
212+
else {
213+
// 分段复制,将index前的元素和index+1后的元素复制到新数组
214+
// 新数组长度为旧数组长度-1
215+
Object[] newElements = new Object[len - 1];
216+
System.arraycopy(elements, 0, newElements, 0, index);
217+
System.arraycopy(elements, index + 1, newElements, index,
218+
numMoved);
219+
//将新数组赋值给array引用
220+
setArray(newElements);
221+
}
222+
return oldValue;
223+
} finally {
224+
// 解锁
225+
lock.unlock();
226+
}
227+
}
228+
```
229+
230+
### 判断元素是否存在
231+
232+
`CopyOnWriteArrayList`提供了两个用于判断指定元素是否在列表中的方法:
233+
234+
- `contains(Object o)`:判断是否包含指定元素。
235+
- `containsAll(Collection<?> c)`:判断是否保证指定集合的全部元素。
236+
237+
```java
238+
// 判断是否包含指定元素
239+
public boolean contains(Object o) {
240+
//获取当前array数组
241+
Object[] elements = getArray();
242+
//调用index尝试查找指定元素,如果返回值大于等于0,则返回true,否则返回false
243+
return indexOf(o, elements, 0, elements.length) >= 0;
244+
}
245+
246+
// 判断是否保证指定集合的全部元素
247+
public boolean containsAll(Collection<?> c) {
248+
//获取当前array数组
249+
Object[] elements = getArray();
250+
//获取数组长度
251+
int len = elements.length;
252+
//遍历指定集合
253+
for (Object e : c) {
254+
//循环调用indexOf方法判断,只要有一个没有包含就直接返回false
255+
if (indexOf(e, elements, 0, len) < 0)
256+
return false;
257+
}
258+
//最后表示全部包含或者制定集合为空集合,那么返回true
259+
return true;
260+
}
261+
```
262+
263+
## CopyOnWriteArrayList 常用方法测试
264+
265+
代码:
266+
267+
```java
268+
// 创建一个 CopyOnWriteArrayList 对象
269+
CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
270+
271+
// 向列表中添加元素
272+
list.add("Java");
273+
list.add("Python");
274+
list.add("C++");
275+
System.out.println("初始列表:" + list);
276+
277+
// 使用 get 方法获取指定位置的元素
278+
System.out.println("列表第二个元素为:" + list.get(1));
279+
280+
// 使用 remove 方法删除指定元素
281+
boolean result = list.remove("C++");
282+
System.out.println("删除结果:" + result);
283+
System.out.println("列表删除元素后为:" + list);
284+
285+
// 使用 set 方法更新指定位置的元素
286+
list.set(1, "Golang");
287+
System.out.println("列表更新后为:" + list);
288+
289+
// 使用 add 方法在指定位置插入元素
290+
list.add(0, "PHP");
291+
System.out.println("列表插入元素后为:" + list);
292+
293+
// 使用 size 方法获取列表大小
294+
System.out.println("列表大小为:" + list.size());
295+
296+
// 使用 removeAll 方法删除指定集合中所有出现的元素
297+
result = list.removeAll(List.of("Java", "Golang"));
298+
System.out.println("批量删除结果:" + result);
299+
System.out.println("列表批量删除元素后为:" + list);
300+
301+
// 使用 clear 方法清空列表中所有元素
302+
list.clear();
303+
System.out.println("列表清空后为:" + list);
304+
```
305+
306+
输出:
307+
308+
```
309+
列表更新后为:[Java, Golang]
310+
列表插入元素后为:[PHP, Java, Golang]
311+
列表大小为:3
312+
批量删除结果:true
313+
列表批量删除元素后为:[PHP]
314+
列表清空后为:[]
315+
```
316+

docs/java/collection/linkedlist-source-code.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ public class LinkedList<E>
5353
- `Cloneable` :表明它具有拷贝能力,可以进行深拷贝或浅拷贝操作。
5454
- `Serializable` : 表明它可以进行序列化操作,也就是可以将对象转换为字节流进行持久化存储或网络传输,非常方便。
5555

56+
![LinkedList 类图](https://oss.javaguide.cn/github/javaguide/java/collection/linkedlist--class-diagram.png)
57+
5658
`LinkedList` 中的元素是通过 `Node` 定义的:
5759

5860
```java
@@ -221,12 +223,13 @@ Node<E> node(int index) {
221223

222224
### 删除元素
223225

224-
`LinkedList`删除元素相关的方法一共有 4 个:
226+
`LinkedList`删除元素相关的方法一共有 5 个:
225227

226228
1. `removeFirst()`:删除并返回链表的第一个元素。
227229
2. `removeLast()`:删除并返回链表的最后一个元素。
228230
3. `remove(E e)`:删除链表中首次出现的指定元素,如果不存在该元素则返回 false。
229-
4. `remove(int index)`:删除指定索引处的元素,并返回该元素的值
231+
4. `remove(int index)`:删除指定索引处的元素,并返回该元素的值。
232+
5. `void clear()`:移除此链表中的所有元素。
230233

231234
```java
232235
// 删除并返回链表的第一个元素

0 commit comments

Comments
 (0)