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