并发包中的 并发 List 只有 CopyOnWriteArrayList,它是个线程安全的 ArrayList,对其进行的修改都是在底层的一个复制的数组(快照📸【关于指定数据集合的一个完全可用拷贝】)上进行的,也就是使用了 写时复制 策略。
文章目录
一、CopyOnWriteArrayList 类图
♥每个 CopyOnWriteArrayList 类对象里有一个 array 数组对象用来存放具体元素,ReentrantLock 独占锁对象用来保证同时只有一个线程对 array 进行修改。
二、CopyOnWriteArrayList 主要方法源码分析
1、 初始化
- 无参构造:
/**
* Creates an empty list.
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
- 有参构造:
-
- 入参为集合,将集合里面的元素复制到 list
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
-
- 创建一个 list,其内部元素是入参 toCopyIn 的副本
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
2、添加元素
public boolean add(E e) {
// (1)获取独占锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// (2)获取 array
Object[] elements = getArray();
// 复制 array 到新数组,添加新元素到新数组
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
// (3)使用新数组替换添加前数组
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
可以看到,(1)处获取独占锁,如果多个线程都调用 add 方法,则只有一个线程会获取到该锁,其他线程会被阻塞挂起直到锁释放。而且 获取 array 后,复制 array 到新数组,新数组的大小是原来数组大小增加 1 ,所以 CopyOnWriteArrayList 是 无界 list。
(2):
final Object[] getArray() {
return array;
}
(3):
final void setArray(Object[] a) {
array = a;
}
✨ 而且可以看出,添加元素时,首先复制了一个快照,然后在快照上进行添加,而不是直接在原来数组上进行。✨
3、删除元素
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];
// 分两次复制,index 之前 和 index 之后的
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,numMoved);
// 使用新数组代替老数组
setArray(newElements);
}
return oldValue;
} finally {
// 释放锁
lock.unlock();
}
}
可以看出,删除元素,首先获取独占锁,以保证 删除数据期间 其他线程不能对 array 进行修改,然后获取数组中要删除的元素,并把剩余的元素复制到新数组,之后使用新数组替换原来的数组,最后在返回要删除元素前,释放锁。
4、获取指定位置元素
public E get(int index) {
// (1)
return get(getArray(), index);
}
(1):
final Object[] getArray() {
return array;
}
当线程调用 get 方法获取指定位置的元素时,分两步走,① 首先获取 array 数组;② 通过下标访问指定位置的元素,但是整个过程并没有加锁同步。
- 写时复制产生的弱一致性问题
如下所示,当线程A 先获取到了 array {1,2,3},然后线程 B 拿到CPU,执行 remove ,删除掉1 ,删除的过程是:先拿到 ReetrantLock ,确保只有这个线程会删除元素,然后拷贝到 newArray,删除 “1” 后再将 array 指向 {2,3},而线程 A 仍是指向的 {1,2,3}。因此 线程A get(0) 取的元素仍是 1, 虽然 “1” 在线程 B 中已经被删除了。这就是 写时复制策略产生的 弱一致性 问题。
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
5、修改指定元素
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;
// 拷贝原数组,在新数组上修改值,并设置新数组到 array
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
}
// 如果指定位置的元素和新值一样,为了保证 volatile 语义,还是需要重新设置 array,虽然 array 的内容并没有改变
else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
- 弱一致性的迭代器
例👀 先看看迭代器的使用:
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class IteratorTest {
public static void main(String[] args) {
CopyOnWriteArrayList<String> arrayList=new CopyOnWriteArrayList<>();
arrayList.add("hello");
arrayList.add("jia");
Iterator<String> itr=arrayList.iterator();
while (itr.hasNext()){
System.out.println(itr.next());
}
}
}
运行结果:
hasNext() 方法用于判断列表中是否还有元素,next 方法则返回具体元素。 CopyOnWriteArrayList 中 迭代器的弱一致性 是指,返回迭代器后,其他线程对 list 的增 删 改 对迭代器是不可见的(因为增删改和迭代器操作的是两个数组)。
- iterator() 源码:
public Iterator<E> iterator() {
//(1)
return new COWIterator<E>(getArray(), 0);
}
(1):
static final class COWIterator<E> implements ListIterator<E> {
// array 的快照版本
private final Object[] snapshot;
// 随后会被调用的要返回的数组下标
/** Index of element to be returned by subsequent call to next. */
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
- hasnext() 源码:
// 是否遍历结束
public boolean hasNext() {
return cursor < snapshot.length;
}
- next() 源码:
@SuppressWarnings("unchecked")
// 获取元素
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
可以看到,当调用 iterator 方法获取迭代器时,实际上会返回一个 COWIterator 对象,COWIterator 对象的 snapshot 变量保存了当前 list 的内容,cursor 是遍历 list 时数据的下标。如果在该线程使用返回的 迭代器 遍历元素的过程中,其他线程没有对 list 进行增 删 改,那么 snapshot 本身就是 list 的 array,但是,如果在遍历期间,其他线程对该 list 进行了增删改,那么 snapshot 就是快照了,因为增删改后 list 里的数组被新数组替换啦,而 snapshot 还引用着老数组。这就是 迭代器弱一致性的体现。
例👀 多线程下迭代器的弱一致性的效果:
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class IteratorTest2 {
private static volatile CopyOnWriteArrayList<String> arrayList=new CopyOnWriteArrayList<>();
package DataStractures.ArrayListPack;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
public class IteratorTest2 {
private static volatile CopyOnWriteArrayList<String> arrayList=new CopyOnWriteArrayList<>();
public static void main(String[] args) throws InterruptedException {
arrayList.add("hello");
arrayList.add("jia");
arrayList.add("welcome");
arrayList.add("to");
arrayList.add("ohh");
Thread threadOne=new Thread(new Runnable() {
@Override
public void run() {
// 修改 list 中下标为1 的元素为 bin
arrayList.set(1,"bin");
// 删除元素
arrayList.remove(2);
arrayList.remove(3);
}
});
// 保证在 修改线程 启动前 获取迭代器
Iterator<String> itr=arrayList.iterator();
// 启动 修改线程
threadOne.start();
// 等待子线程执行完毕
threadOne.join();
// 迭代元素
while (itr.hasNext()){
System.out.println(itr.next());
}
}
}
运行结果:
从运行结果看到,获取迭代器,是在子线程运行之前执行的,而在子线程里的操作,一个都没有生效,这就是迭代器弱一致性的表现。
🎭 总结 :
CopyOnWriteArrayList 使用 写时复制 的策略,来保证 list 的一致性,而 获取 - 修改 - 写入,这三步操作不是原子性的,所以,在 增 删 改 的过程中都使用了 独占锁,来保证在某个时刻,只有一个线程能对 list 进行操作,是线程安全的,但是 写时复制 会导致 弱一致性问题。另外,CopyOnWriteArrayList 提供了弱一致性的迭代器,从而保证在获取迭代器后,其他线程对 list 的修改是不可见的,迭代器遍历的数组是一个快照。而且,CopyOnWriteArraySet 的底层是 使用 CopyOnWriteArrayList 实现的。