简介
CopyOnWriteArrayList是ArrayList的线程安全版本,内部也是通过数组实现,每次对数组的修改都完全拷贝一份新的数组来修改,修改完了再替换掉老数组,这样就保证了只阻塞写操作,不阻塞读操作,实现了读写分离。
继承体系
CopyOnWriteArrayList实现了List,Cloneabele,Serializable,RandomAccess接口
CopyOnWriteArrayList实现了List接口,提供了基础的添加,删除,遍历等操作
CopyOnWriteArrayList实现了RandomAccess接口,提供了随机访问的能力。
CopyOnWriteArrayList实现了Cloneale,可以被克隆。
CopyOnWriteArrayList实现了Serializable,可以被序列化。
源码分析
属性
/**序列化号*/
private static final long serialVersionUID = 8673264195747942595L;
/** 用于写操作时加锁 */
final transient ReentrantLock lock = new ReentrantLock();
/** 真正存储元素的数组 只能通过getArray()/setArray()方法访问*/
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
构造方法
这个构造方法是创建一个空的list
/**
* 创建一个空的List
*/
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
final void setArray(Object[] a) {
array = a;
}
这个构造方法是传入一个集合,先判断传入集合是否是CopyOnWriteArrayList类型,如果是,则直接拷贝,浅拷贝。如果不是,则先将集合装换为数组,再进行数组
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
//如果C是CopyOnWriteArrayList类型的,直接把它的数组赋值给当前的list数组,这里是浅拷贝,两个集合共用同一个数组
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
//如果不是CopyOnWriteArrayList类型,则先调用toArray()方法将集合装换为数组,再把数组中的元素拷贝。
elements = c.toArray();
//toArray()方法返回的可能不是Object数组
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
这个方法是传入一个数组,这里是直接把toCopyIn的元素拷贝给当前list的数组。
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
add(E e)方法
添加一个元素到末尾
public boolean add(E e) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取数组和数组长度
Object[] elements = getArray();
int len = elements.length;
//用旧的数组拷贝一个长度+1的新的数组
Object[] newElements = Arrays.copyOf(elements, len + 1);
//把要添加的元素放在新数组的末尾
newElements[len] = e;
//更新新的数组
setArray(newElements);
return true;
} finally {
//解锁
lock.unlock();
}
}
步骤:
- 先加锁
- 获取元素的数组和元素的长度
- 用旧的数组拷贝一个长度+1的新的数组
- 把要添加的数组放在新数组的末尾,同时更新数组的新引用,覆盖掉原数组
- 解锁
add(int index,E element)方法
添加一个元素在指定的索引处
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
//先加锁
lock.lock();
try {
//获取原数组和原数组的长度
Object[] elements = getArray();
int len = elements.length;
//这里判断给定的index是否越界
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
//计算要移动数组的个数
int numMoved = len - index;
//这里表示无须移动元素,直接添加到数组的末尾 相当于一个优化,拷贝一个len+1的一个新数组
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
//新建一个长度为len+1的数组
newElements = new Object[len + 1];
//这里表示从旧数组的第0个位置拷贝index个元素到相应的新数组中
System.arraycopy(elements, 0, newElements, 0, index);
//这里表示从旧数组的index位置拷贝到新数组的index+1位置,index位置留空
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
//将新元素放在index位置
newElements[index] = element;
setArray(newElements);
} finally {
//释放锁
lock.unlock();
}
}
步骤:
- 加锁
- 获取元素的数组和数组的长度
- 判断添加元素的index是否越界,可以等于len
- 声明一个新的Object数组并计算出要移动的长度
- 如果numMoved==0的话,就是要添加到数组的末尾,这里只需要拷贝旧数组,长度+1的新数组
- 先声明一个len+1新的数组 拷贝旧数组从0位置拷贝到新的数组,拷贝index个,同时再拷贝 从旧数组的index位置拷贝到新数组的(新数组从index+1位置)拷贝numMoved个,这时index位置就空出来了
- 将新元素放到新数组的index位置,同时更新新的数组引用
- 释放锁。
addIfAbsent(E e)方法
添加一个元素如果这个元素不存在集合中
public boolean addIfAbsent(E e) {
//先获取元素数组的一个快照
Object[] snapshot = getArray();
//这里indexOf()返回值大于0表示数组中已经有这个元素了就返回false,小于0则表示没有这个元素就进入重载的方法将新元素添加到末尾
return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
addIfAbsent(e, snapshot);
}
//这个方法主要是遍历数组看数组中是否有o这个元素,有的话返回的是数组中这个元素的位置下标 没有就返回-1
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
private boolean addIfAbsent(E e, Object[] snapshot) {
final ReentrantLock lock = this.lock;
//加锁
lock.lock();
try {
//获取当前元素数组和数组长度
Object[] current = getArray();
int len = current.length;
//这里是优化与另外一个addIfAbsent竞争失败的情况
//如果快照与刚获取的数组不一致,则说明有修改
if (snapshot != current) {
//获得快照长度和数组长度的最小值
int common = Math.min(snapshot.length, len);
for (int i = 0; i < common; i++)
//遍历数组每一个位置是否和快照每一个位置相同,有不相同并且e不存在数组中则返回false
if (current[i] != snapshot[i] && eq(e, current[i]))
return false;
//这个是判断e是否在数组中,返回值大于0则表示数组中含有e这个元素,就返回false
if (indexOf(e, current, common, len) >= 0)
return false;
}
//拷贝一个长度为len+1的新数组
//将要添加的元素放入数组中len的位置,更新数组引用
Object[] newElements = Arrays.copyOf(current, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
//释放锁
lock.unlock();
}
}
get(int index)方法
获取指定索引的元素,因为底层是数组,实现了RandomAccess接口,支持随机访问,时间复杂度为O(1)
public E get(int index) {
//获取元素,读操作不需要加锁
//直接返回的是index位置的元素
//这里没有做越界检查,因为数组本身就会做越界检查
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
final Object[] getArray() {
return array;
}
remove(int index)方法
删除指定索引的元素,写操作,需要加锁保证安全
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);
//计算出要移动的元素的个数
int numMoved = len - index - 1;
//如果移除的是最后一个元素,直接拷贝长度len-1的数组,相当于就直接把最后一个元素删除了
if (numMoved == 0)
setArray(Arrays.copyOf(elements, len - 1));
else {
//如果移除的不是最后一个元素
//新建一个长度为len-1的新数组
Object[] newElements = new Object[len - 1];
//将前index位置的元素拷贝到新数组中 旧数组从0开始拷贝
System.arraycopy(elements, 0, newElements, 0, index);
//将旧数组从index+1位置拷贝到新数组 新数组从index位置接收拷贝数据,这样相当于把旧数组中index位置删除了
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
//释放锁
lock.unlock();
}
}
addAllAbsent(Collection<> c)方法
这个方法是去除集合中重复的元素,返回值为添加的元素的数量,这个方法在CopyOnWriteArraySet中使用过
public int addAllAbsent(Collection<? extends E> c) {
//将集合c转换为数组
Object[] cs = c.toArray();
if (cs.length == 0)
return 0;
final ReentrantLock lock = this.lock;
//写操作 需要加锁
lock.lock();
try {
//获取旧数组和旧数组长度
Object[] elements = getArray();
int len = elements.length;
//记录已经添加的元素数量
int added = 0;
// uniquify and compact elements in cs
for (int i = 0; i < cs.length; ++i) {
Object e = cs[i];
//这个是判断e是否在旧数组的0-len中存在 小于0表示不存在 下一个判断是判断e是否在cs
数组的0-added位置存在 两者都满足不存在的话 才会把e放入cs数组中的added位置 这
样就把cs数组中重复的元素去除掉了
if (indexOf(e, elements, 0, len) < 0 &&
indexOf(e, cs, 0, added) < 0)
cs[added++] = e;
}
//for循环执行完后 added>0表示已经添加的元素
if (added > 0) {
Object[] newElements = Arrays.copyOf(elements, len + added);
System.arraycopy(cs, 0, newElements, len, added);
setArray(newElements);
}
//返回添加元素的数量
return added;
} finally {
//释放锁
lock.unlock();
}
}
Size()方法
返回数组的长度
public int size() {
//获取长度时不需要加锁
//直接返回数组的长度
return getArray().length;
}
总结
- CopyOnWriteArrayList使用ReentrantLock可重入锁加锁,保证多线程情况下的线程安全。
- CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做写操作,修改完后再用新数组替换老数组的引用,空间复杂度为O(n)
- CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1)
- CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用读多写少的场合。
- CopyOnWriteArrayList只保证最终一致性,不保证实时一致性。