介绍:
CopyOnWriteArrayList 是一个线程安全的 ArrayList,它在每次修改(add/set/remove)时创建数组的新副本,然后将修改应用到新数组上。这是它名字的由来:"CopyOnWrite"。
这种设计使得它在多线程环境下能提供更好的并发性能。当一个线程修改列表时,其他线程不能访问旧数组,因此不会受到数据不一致的影响。然而,写操作的代价是创建新数组并复制所有元素,这可能在大量写操作的情况下导致性能下降。
需要注意的是,由于 CopyOnWriteArrayList 在修改操作时复制整个数组,所以它不适用于处理大量写操作和/或大数组的情况,因为这种情况下,复制操作可能会导致显著的性能下降。在这些情况下,你可能需要考虑其他并发控制策略,例如使用锁或者使用并发集合的其他实现。
属性:
/** The lock protecting all mutators */ 锁对象 用于解决线程竞争 final transient ReentrantLock lock = new ReentrantLock(); /** The array, accessed only via getArray/setArray. */ 存放数据 private transient volatile Object[] array;
构造方法:
无参构造 :初始化array
java
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
有参构造: 传入一个集合
java
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class) 如果是当前类 则直接获取array属性的值
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
// c.toArray might (incorrectly) not return Object[] (see 6260652)
//这里的是JDK以前的以一个bug 有时候虽然是toArray方法返回的虽然是Object[]数组 但实际的类型仍然是原本的类型,这种情况放入object对象会报错 ,所以重新复制了一个新的Object数组对象
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
有参构造:传入数组
java
//复制的原因和上一个构造方法一样
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
常用方法:
get:
分为两步 :
1.第一步通过getArray()获取的array属性
2.取出元素
需要注意的是该方法并未加锁,也就是说,在第一步执行的完的时候,如果有其他线程将array的值修改了,此时get的获取的array属性还是旧的引用,是无法感知到新的的变化的,也就是弱一致性。
java
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
add:
核心正如类名一样 先copy再write,同时对get的弱一致性有所了解了把。
java
public boolean add(E e) {
//获取锁对象
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取array属性
Object[] elements = getArray();
//获取array的长度
int len = elements.length;
//将array的值复制成一个新的数组 并且长度+1 填充null
Object[] newElements = Arrays.copyOf(elements, len + 1);
//将对应的下表复制
newElements[len] = e;
//将新数组赋给array属性
setArray(newElements);
return true;
} finally {
//解锁
lock.unlock();
}
}
重载方法
java
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//判断下表是否越界
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
//如果该下表和数组的长度一致
if (numMoved == 0)
//复制数组并len加1
newElements = Arrays.copyOf(elements, len + 1);
else {
//创建一个长度为len+1的数组
newElements = new Object[len + 1];
//将旧数组的数据从0开始 复制到新数组的下标 也从0开始 长度为index个
System.arraycopy(elements, 0, newElements, 0, index);
//将旧数组从index开始复制到新数组 从index+1为值开始的数据复制 复制长度为numMoved个
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
//将对应下标的值替换 并替换旧数组
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
set:
set方法中有意思的地方是,旧值和新值一样的情况下,还会重新赋值一次,jdk的注释是为了保证volatile语义。
volatile保证了不同线程之间对共享变量操作时的可见性,也就是当一个线程修改volatile修饰的变量,另一个线程会立即看到该结果
个人理解:是不同线程操作该对象的set方法或其他方法,set的结果应对后另一个线程可见,若不进行赋值,没有volatile写,其他线程的cpu缓存的array属性不会失效,不符合volatile语义。这块争议比较大,在jdk11曾经移除过,后来又加了回来,若有不同理解,可以一起讨论。
java
public E set(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取array属性
Object[] elements = getArray();
//获取对应的下标的值
E oldValue = get(elements, index);
//如果旧和新的值不一样
if (oldValue != element) {
//复制数组并在新数组中替换对应下表的值,再赋给array
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
//注释为:为了保住volatile语义 有了旧的赋值动作
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
remove:
和新增元素的代码类似,首先获取独占锁以保证删除数据期间其他线程 不能对 array 进行修改,然后获取数组中要被删除的元素,并把剩余的元素复制到新数组, 之后使用新数组替换原来的数组,最后在返回前释放锁
java
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)
//等于0就是末尾的位置 直接复制新数组 长度为旧的减一并赋值
setArray(Arrays.copyOf(elements, len - 1));
else {
//创建新数组 长度为旧的-1
Object[] newElements = new Object[len - 1];
//将旧的数组复制到新数组 从下标0开始 长度为 index (左闭右开)
System.arraycopy(elements, 0, newElements, 0, index);
//将旧的数组复制到新数组 从旧下标index+1开始 在新的index开始 长度为numMoved
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
//替换旧数组
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
重载方法:删除集合中第一个元素
java
public boolean remove(Object o) {
//获取数组
Object[] snapshot = getArray();
//获取第一次出现的下标
int index = indexOf(o, snapshot, 0, snapshot.length);
return (index < 0) ? false : remove(o, snapshot, index);
}
private boolean remove(Object o, Object[] snapshot, int index) {
//加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//获取当前array属性
Object[] current = getArray();
int len = current.length;
//如果传入的数组和当前的不一致
if (snapshot != current) findIndex: {
//防止数组越界
int prefix = Math.min(index, len);
findIndex类似一种方法体,后面break的实际跳出的是当前方法
for (int i = 0; i < prefix; i++) {
//找到两个数组内下标一样内容不一致的位置 并且o和当前数组位置的内容的相等
if (current[i] != snapshot[i] && eq(o, current[i])) {
//获取在当前数组内的下标 跳出方法体
index = i;
break findIndex;
}
}
//执行到此处说明没在循环内找到
//如果该位置大于数组长度 返回false
if (index >= len)
return false;
//如果当前位置元素==0 跳出findIndex方法体 不再执行后面逻辑
if (current[index] == o)
break findIndex;
//获取o在current中的位置 如果没找到 返回false
index = indexOf(o, current, index, len);
if (index < 0)
return false;
}
//将旧数组复制到新数组 并替换原本的
Object[] newElements = new Object[len - 1];
System.arraycopy(current, 0, newElements, 0, index);
System.arraycopy(current, index + 1,
newElements, index,
len - index - 1);
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
iterator:
返回的是内部类的对象 此类实现了ListIterator接口,该接口继承自Iterator。需要注意的是getArray()方法返回的是当前一刻的array,若之后array被替换成新的,那么在迭代过程中的修改 是不会影响到新数组的,也就是通常说的弱一致性迭代。
java
public Iterator<E> iterator() {
return new COWIterator<E>(getArray(), 0);
}
总结:
CopyOnWriteArrayList 使用写时复制的策略来保证 list的一 致性,而获取-修改-写入三步操作并不是原子性的,所以在增删改的过程中都使用了独占锁,来保证在某个时间只有1个线程能对 list 数组进行修改,另外 copyOnWriteArrayList 供了弱一致性的法代 从而保证在获取迭代器后,其他线程对 list 修改是不可见的,迭代器遍历的数组是 1个快照。另外CopyOnWriteArraySet 的底层就是使用它实现的,在保证不会新增同样的元素时调用的是addIfAbsent方法(不存在则增加),有兴趣的可以翻阅源码。