前边jdk源码系列中提到了hashmap和ArrayList都不是线程安全的容器,那么接下来,在本文中就来看一下jdk是怎么实现支持并发线程安全的容器吧
并发List之CopyOnWriteArrayList
顾名思义,写时复制的一个list,既然说是list,底层不用说肯定是数组了。来看下吧
属性
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
}
- 可以看到实现接口跟ArrayList一样的,前面讲过不再细说。
- 使用全局独占锁
- 底层是数组,注意是volatile修饰的哦
构造方法
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
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);
}
其实也没啥,跟ArrayList基本一样,就是给数组赋值。
题外话,不过可以学习下第二种的写法,之前看过一个面试题就是让写equals,其实想着很简单,写起来未必有如jdk源码那般精简。
添加元素
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();
}
}
- 首先获得全局锁,保证其他线程在此添加期间不会修改数组
- 把数组复制到一个新的size+1的新数组中
- 设置新数组替换原数组
- return之前释放锁
上边的流程我们就能发现,所谓的新添加一个元素其实是在新的数组上进行的,这也是所谓的写时复制的意思
修改指定位置元素
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;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
setArray(newElements);
} else {
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
- 获取锁,得到数组,获取指定位置元素
- 如果指定位置元素等于指定替换新元素,为保证volatile语义,旧数组重新设置一遍
- 如果不等的话,把就数组元素复制到新数组,并在index处进行替换
- 释放锁,返回旧的值
获取指定位置元素
public E get(int index) {
return get(getArray(), index);
}
final Object[] getArray() {
return array;
}
private E get(Object[] a, int index) {
return (E) a[index];
}
- 得到数组
- 从数组中拿出指定index处元素
注意一下,get操作并没加锁,所以第一步完成后,其他线程是可以修改数组的,如果正好就修改了index处元素,回头看get操作,第一步其实相当于是获取了前一个时刻的一个快照,那么这个修改操作他是不知道的,第二部还是会返回修改前的值,实际上数组中原来值已被修改。 这就是写时复制产生的一个弱一致性问题
删除元素
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];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index + 1, newElements, index,
numMoved);
setArray(newElements);
}
return oldValue;
} finally {
lock.unlock();
}
}
- 获得锁,获得数组
- 把剩下元素怒迁移到新数组
- 设置新数组,释放锁,返回移除值
迭代
public Iterator<E> iterator() {
// 通过数组快照构造迭代器
return new COWIterator<E>(getArray(), 0);
}
static final class COWIterator<E> implements ListIterator<E> {
private final Object[] snapshot;
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public boolean hasPrevious() {
return cursor > 0;
}
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];
}
}
可以看到通过获取数组构造迭代器,后续迭代遍历都是对这个数组进行的,跟上边的get存在一样的问题,别的线程可能会在这个过程中更新了数组,此时迭代器中的数组就是更新前数组的快照,而对当前数组增删改不可见,因为已经是两个数组了(旧数组和新数组),即存在弱一致性问题。
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch=new CountDownLatch(1);
CopyOnWriteArrayList<String> cow=new CopyOnWriteArrayList();
cow.add("111");
cow.add("222");
cow.add("333");
cow.add("444");
cow.add("555");
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {
cow.remove(1);
cow.remove(3);
countDownLatch.countDown();
}
});
// 在删除元素前获取快照
Iterator beforeDeleteIterator=cow.iterator();
thread1.start();
countDownLatch.await();
Iterator afterDeleteIterator=cow.iterator();
while (beforeDeleteIterator.hasNext()){
System.out.println(beforeDeleteIterator.next());
}
while (afterDeleteIterator.hasNext()){
System.out.println(afterDeleteIterator.next());
}
}
##########beforeDeleteIterator##
111
222
333
444
555
#########afterDeleteIterator####
111
333
444
CopyOnWriteArrayList总结
CopyOnWriteArrayList中的写操作不是原子性的,所以使用独占锁来保证线程安全。同时,因为array是volatile的,所以采用写时复制可以有效保证list的最终一致性,使得数组中永远是最新值。虽然读取可能读到旧值,没关系,下次读就是新的了嘛,当然运气不好的话,就下下次,总可以读到最新的;另一方面,每写都要进行复制数组,开销相当大,也正因为此,很适合读多写少的场景,
公众号 程序员二狗
每日原创文章 一起交流学习