文章目录
- JAVA并发集合
- 1_实现原理
- 2_什么是CopyOnWrite?
- 3_CopyOnWriteArrayList的原理
- 4_CopyOnWriteArraySet
- 5_使用场景
- 6_总结
JAVA并发集合
从Java5开始,Java在java.util.concurrent
包下提供了大量支持高效并发访问的集合类,它们既能包装良好的访问性能,有能包装线程安全。这些集合类可以分为两部分,它们的特征如下:
- 以Concurrent开头的集合类:
以Concurrent开头的集合类代表了支持并发访问的集合,它们可以支持多个线程并发写入访问,
这些写入线程的所有操作都是线程安全的,但读取操作不必锁定。以Concurrent开头的集合类采
用了更复杂的算法来保证永远不会锁住整个集合,因此在并发写入时有较好的性能。 - 以CopyOnWrite开头的集合类:
以CopyOnWrite开头的集合类采用复制底层数组的方式来实现写操作。当线程对此类集合执行读
取操作时,线程将会直接读取集合本身,无须加锁与阻塞。当线程对此类集合执行写入操作时,集
合会在底层复制一份新的数组,接下来对新的数组执行写入操作。由于对集合的写入操作都是对数
组的副本执行操作,因此它是线程安全的。 - 扩展阅读
java.util.concurrent包下线程安全的集合类的体系结构:
本文将主要讲解以CopyOnWrite
开头的集合类:
1_实现原理
在Java中,CopyOnWrite
系列的集合(如CopyOnWriteArrayList
和CopyOnWriteArraySet
)是线程安全的集合类,适用于读操作频繁且写操作相对较少的场景。它们通过一种名为 “写时复制”(Copy-On-Write,简称COW)的策略来实现线程安全。
2_什么是CopyOnWrite?
概括为"写时复制",通俗的讲是写数据的时候弄出一个新的数组,然后讲旧的数据拷贝过去,更新后再将引用指向新数组。这样在添加删除元素时就不会影响旧数组的读取了,确保高并发时读的效率,但是存在延时。
下面以CopyOnWriteArrayList
和CopyOnWriteArraySet
为例对CopyOnWrite
系列的集合进一步讲解。
3_CopyOnWriteArrayList的原理
CopyOnWriteArrayList
是一个线程安全的可变数组实现,内部通过复制底层数组来处理并发写操作。其主要特性是:
- 读操作不需要锁:因为读操作不会修改数组,因此可以并发进行。
- 写操作通过复制实现:每次写操作(如添加、删除、更新)都会创建底层数组的一个新副本,修改副本后再将其设置为新的底层数组。
内部实现机制:
以下是CopyOnWriteArrayList
的核心实现机制:
-
底层数据结构
CopyOnWriteArrayList
内部使用一个volatile
修饰的数组来存储元素,确保多线程环境下对数组的可见性。private transient volatile Object[] array;
-
读操作
读操作直接访问底层数组,无需加锁。
public E get(int index) { return get(array, index); } final Object[] getArray() { return array; } private E get(Object[] a, int index) { return (E) a[index]; }
-
写操作
写操作在进行修改时,会首先复制底层数组,然后在新数组上进行修改,最后将新数组设置为底层数组。
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(); // 释放锁 } } final void setArray(Object[] a) { array = a; }
在这个示例中,add
方法首先获取锁以确保写操作的线程安全。然后,它复制现有的数组,增加一个新元素,并将新数组设置为底层数组。
添加操作会复制新的数组并将原元素长度加一,那么移除一个元素呢?
- 移除操作:
public boolean remove(Object o) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; // 寻找要移除的元素的位置 int i = 0; for (; i < len; i++) { if (o.equals(elements[i])) { break; } } // 如果没有找到元素,直接返回false if (i == len) { return false; } // 创建新数组,长度比当前数组少一个 Object[] newElements = new Object[len - 1]; // 复制前面的元素 System.arraycopy(elements, 0, newElements, 0, i); // 复制后面的元素 System.arraycopy(elements, i + 1, newElements, i, len - i - 1); // 设置新数组 setArray(newElements); return true; } finally { lock.unlock(); } }
在CopyOnWriteArrayList
中,移除元素的操作也是通过复制数组并在新数组上进行操作来实现的。虽然每次移除操作都会创建一个新数组,存在一定的性能开销,但这种设计能够确保线程安全,适合读多写少的场景。
既然remove、add已经了解了,那么 set
也就不难猜测了,public E set(int index, E element)
操作时也是拷贝原数组然后进行操作,只不过长度相对原数组既没有增加也没有减少。
4_CopyOnWriteArraySet
CopyOnWriteArraySet
是基于CopyOnWriteArrayList
实现的线程安全的集合。它利用CopyOnWriteArrayList
来存储元素,并确保集合中的元素不重复。
5_使用场景
CopyOnWrite
集合适用于以下场景:
- 读多写少:由于每次写操作都会复制整个数组,因此写操作的开销较大。适用于读操作频繁、写操作较少的场景。
- 遍历操作:在遍历过程中,不会受到并发修改的影响,因为任何写操作都会创建一个新的数组副本,不会修改正在遍历的数组。
优缺点
优点
- 线程安全:通过写时复制机制实现线程安全,读操作无锁,性能高。
- 适用于读多写少的场景:在读操作远多于写操作的情况下,性能表现优秀。
- 不需要手动同步:用户无需手动添加同步代码,简化了并发编程。
缺点
- 内存开销大:每次写操作都会创建数组副本,占用额外的内存。
- 写操作性能差:写操作需要复制数组,性能较低。
示例代码
以下是CopyOnWriteArrayList
的简单示例代码:
import java.util.concurrent.CopyOnWriteArrayList;
public class CopyOnWriteExample {
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
// 添加元素
list.add(1);
list.add(2);
list.add(3);
// 读取元素
System.out.println("Element at index 0: " + list.get(0));
// 遍历元素
for (Integer element : list) {
System.out.println("Element: " + element);
}
// 删除元素
list.remove(Integer.valueOf(2));
System.out.println("After removal: " + list);
}
}
6_总结
CopyOnWrite
系列集合通过写时复制机制实现线程安全,适用于读操作频繁且写操作较少的场景。虽然写操作的开销较大,但在读操作占多数的应用中,CopyOnWrite
集合可以提供高效且线程安全的性能。
在CopyOnWriteArrayList
中,移除元素的操作与添加元素类似,通过复制数组并在新数组上进行操作来实现线程安全。移除元素时,需要创建一个新的数组,长度比当前数组少一个,然后复制所有不需要移除的元素到新数组中。