目录
1. 前言
使用过 ArrayList 集合的同学应该大致都知道,ArrayList 是一个非线程安全的集合;同样,Java也为我们提供了线程安全的 List 集合,它就是我们本篇文章要说的 CopyOnWriteArrayList。因为开发过程中通常会使用其他的的方式保证线程安全,所以它使用的频率没有 ArrayList 那么频繁。
2.CpoyOnWriteArrayList 原理简单概述
CopyOnWriteArrayList 的底层采用了加锁的方式保证线程安全,并且加的是 Lock 锁而不是 Sychonized 锁。
假如现在有两个线程,一个读线程A,一个写线程B,同时想要想数组中添加元素,读线A程就会读取当前内存中 CopyOnWriteArrayList 集合,写线程B则是会将内存中的 CopyOnWriteArrayList 集合对象复制一份新的,在新复制的集合中执行添加操作,添加操作完成之后再将新的集合赋值给原来老的集合,并且这个过程中写线程B会获取唯一的 Lock 锁,其它写线程会阻塞等待,实现读写分离。那么假如说有第三个写线程C也想要执行写数据操作,就需要等待写线程B操作完成之后释放 Lock 锁自己获取到 Lock 锁之后才能去执行写入操作。
3. CopyOnWriteArrayList 源码分析
3.1 属性构造器解读
下面是我粘贴的一部分属性,get,set方法,构造方法。
(1)可以看到在 CopyOnWriteArrayList 内部它定义了一个 Lock 锁对象;
(2)底层定义了一个名为 array 的对象数组;
(3)无参构造可以看出调用无参构造会将 array 数组对象的长度设置为 0,只有在进行存储元素的时候才回去扩容;
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final long serialVersionUID = 8673264195747942595L;
/** The lock protecting all mutators */
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
/**
* Gets the array. Non-private so as to also be accessible
* from CopyOnWriteArraySet class.
*/
final Object[] getArray() {
return array;
}
/**
* Sets the array.
*/
final void setArray(Object[] a) {
array = a;
}
/**
* Creates an empty list.
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
3.2 get 方法分析
下面是 CopyOnWriteArrayList 的 get 获取元素的方法,这里它 index 和数组的长度大小都没有做判断,所以很有可能会出现索引越界异常;
get 获取元素的方法很简单,没有出现加锁的行为
public E get(int index) {
// 直接返回对象 index 位置的元素
return get(getArray(), index);
}
3.3 add 方法分析
public void add(int index, E element) {
// 获取 Lock 锁
final ReentrantLock lock = this.lock;
// 调用方法上锁
lock.lock();
try {
// 获取内存的数组对象并赋值为 elements
Object[] elements = getArray();
// 定义一个变量 len 获取数组的长度
int len = elements.length;
// 判断方法的参数 index 是否越界或合法
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
// 定义一个新数组对象 newElements
Object[] newElements;
// 定义一个变量 numMoved 接收数组长度 - index 的值
int numMoved = len - index;
// 如果 numMoved 为0,则说明要把新添加的元素放在数组的最后
if (numMoved == 0)
// 调用调用 copyOf 方法将原来数组中的数据全部复制到 newElements中,
// 并在数组末尾添加上新的元素
newElements = Arrays.copyOf(elements, len + 1);
else {
// 如果 numMoved 不为0,则说明要将该元素添加在数组中间的某个位置
// 先将新数组的长度 + 1
newElements = new Object[len + 1];
// 将老数组 0~index 之间的数据全部复制到新数组中
System.arraycopy(elements, 0, newElements, 0, index);
// 再将 index~数组最后的数据全部复制到新数组中
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
// 将要添加的元素 element 添加到新数组的 index 的位置
newElements[index] = element;
// 将老数组地址值赋值给新数组对象
setArray(newElements);
} finally {
// 操作完毕,最后释放锁
lock.unlock();
}
}
3.4 set 方法分析
public E set(int index, E element) {
// 获取 Lock 锁
final ReentrantLock lock = this.lock;
// 调用方法上锁
lock.lock();
try {
// 获取内存的数组对象并赋值给一个新的数组对象 elements
Object[] elements = getArray();
// 获取 index 处的元素
E oldValue = get(elements, index);
// 判断 oldValue 和要插入的元素是否相等
if (oldValue != element) {
// 获取数组的长度
int len = elements.length;
// 将原本的数组数据复制到新数组 newElements 中
Object[] newElements = Arrays.copyOf(elements, len);
// 将 element 放置到新数组的 index 处
newElements[index] = element;
// 将新数组覆盖原来的数组
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
// 进入 else ,说明要set的元素在数组中已经存在,直接返回原数组
setArray(elements);
}
// 返回位置 index 处的老的元素
return oldValue;
} finally {
// 操作完成,释放 lock 锁
lock.unlock();
}
}
3.5 remove 方法分析
public E remove(int index) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
// 获取数组长度
int len = elements.length;
// 获取 index 处的元素
E oldValue = get(elements, index);
// 定义 numMoved 计算出要移动的元素的数量
int numMoved = len - index - 1;
// 如果 numMoved 为0,说明要删除的元素恰好是数组的最后一个元素
if (numMoved == 0)
// 覆盖原来的数组
setArray(Arrays.copyOf(elements, len - 1));
else
// numMoved 不为0,则定义一个新数组,长度为原来的数组长度-1
Object[] newElements = new Object[len - 1];
// 将 0~index 处的元素复制到新数组中去
System.arraycopy(elements, 0, newElements, 0, index);
// 将 index+1~数组最后的元素移动到新数组中
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
// 覆盖原来的数组
setArray(newElements);
}
// 返回删除的元素值
return oldValue;
} finally {
// 操作完毕,释放 lock 锁
lock.unlock();
}
}
4. 总结概括
经过上面对 add 添加方法,get 获取方法,set 修改方法,remove 删除方法的分析,其实同学们也可以看出,无非就是在原来 ArrayList 集合的基础上添加了一把 lock 。
在做增,改,删三种操作的时候,搭配上 copy 复制数组的思想,就可以做到线程安全,这就是 CopyOnWriteArrayList 线程安全的核心设计思想,不算特别难理解。