小探CopyOnWriteArrayList

前边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];
}
  1. 得到数组
  2. 从数组中拿出指定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的最终一致性,使得数组中永远是最新值。虽然读取可能读到旧值,没关系,下次读就是新的了嘛,当然运气不好的话,就下下次,总可以读到最新的;另一方面,每写都要进行复制数组,开销相当大,也正因为此,很适合读多写少的场景,

公众号 程序员二狗

每日原创文章 一起交流学习

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值