CopyOnWriteArrayList源码分析

https://blog.csdn.net/mayongzhan_csdn/article/details/81099883

 

容器一般分为三类:普通容器,同步容器,并发容器。 
对于容器我们每天都再用,无非就是对容器的增删改查和迭代,单个操作都没有问题,加上多线程有修改、有迭代、有查询。如果你遇到如下问题但对原理说不清楚,还是建议先看下我之前的博文Java容器迭代时修改问题及方案 
问题诸如:

迭代时没有则添加抛出异常ConcurrentModificationException 
迭代时有则删除,但是删除不干净 (比如两个紧挨着的相同的对象只能删除一个) 
再复合操作中,用了同步容器(线程安全的)如Vector,仍然抛出异常ConcurrentModificationException

今天讲这个COW(写时复制)的list,在熟悉Java锁ReentrantLock源码的情况下在读本文就是轻车熟路,非常简单。

想到CopyOnWriteArrayList脑子中应该立马出现几个关键词: 
1)线程安全的List 
2)脏读 
3)写时复制COW 
4)底层volatile Object[] 初始0长度 
5)读多写少场景 
就说明你理解的差不多了,我们还是过下源码吧!

重要属性

final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
1
2
一把独占重入锁 
一个数组Object[] volatile修饰

构造方法

public CopyOnWriteArrayList() {
    setArray(new Object[0]);
}
1
2
3
没有默认容量,ArrayList默认是10,不像ArrayBlockingQueue需要设置容量

增:add(E e)方法

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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
add方法是互斥的,没有null检查,不像ArrayBlockingQueue非null

1)lock.lokc()我们熟悉的锁 
2)拷贝数组增加一个容量 
3)给数组最后一个位置复制为当前添加的对象 
4)给数组引用复制(volatile修饰保证写能被其它线程可见)

不考虑独占重入锁锁,这段逻辑变得异常的简单。

问:为什么不像ArrayList一样能够扩容,而是每次添加都会拷贝数组? 
答:COW是写时复制 适合读多写少场景,所以写场景少,每次加1个容量,不会浪费空间。

查:indexOf(obj)方法

public int indexOf(Object o) {
    Object[] elements = getArray();
    return indexOf(o, elements, 0, elements.length);
}
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
返回找到第一个符合条件的对象的下标 
为空使用elements[i] == null判断 
不为空obj.equals(elements[i]) 
找不到返回-1

读取到的是可能脏数据。

删:remove(obj)方法 
外层remove方法

public boolean remove(Object o) {
    Object[] snapshot = getArray();
    int index = indexOf(o, snapshot, 0, snapshot.length);
    return (index < 0) ? false : remove(o, snapshot, index);
}
1
2
3
4
5
remove调用的内层的内部支撑方法remove(o,snapshot,index) 
 
remove(obj)内部调用了重载的私有支撑方法remove(obj,snapshot,index) 
在外层没有锁的情况下通过equals来比较找到下标,前面说过这个下标的值有可能已经被修改成了其它,所以真正执行remove的时候还是要加锁snapshot!=current就说明是—此时就是可能脏读,如果该另外的线程正好改掉的就是这个下标的值才是真的脏读。所以需要遍历重新判断,如果没有被修改再删除这个值,再次执行数组拷贝,再次赋值。

addIfAbsent(E e) 
我们知道List可以放入重复元素,Set放入的是不可重复的,我们看下CopyArrayList是如何执行的。

public boolean addIfAbsent(E e) {
    Object[] snapshot = getArray();
    return indexOf(e, snapshot, 0, snapshot.length) >= 0 ? false :
        addIfAbsent(e, snapshot);
}
private boolean addIfAbsent(E e, Object[] snapshot) {
        final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] current = getArray();
        int len = current.length;
        if (snapshot != current) {
            // Optimize for lost race to another addXXX operation
            int common = Math.min(snapshot.length, len);
            for (int i = 0; i < common; i++)
                if (current[i] != snapshot[i] && eq(e, current[i]))
                    return false;
            if (indexOf(e, current, common, len) >= 0)
                    return false;
        }
        Object[] newElements = Arrays.copyOf(current, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
代码依然是比较简单 
先不加锁查询没有, 
再执行真正的addIfAbsent(e,snapshot), 
真正执行的时候加锁判断是否被别人修改过, 
修改过(snapshot != current)并且current[i] != snapshot[i] && eq(e, current[i])即两个元素下标不相同,但是元素是相同的,说明已经被别人线程添加过了直接返回false,来保证没有重复的元素。

迭代器Iterator 
我们知道所有的集合都有Iterator

static final class COWIterator<E> implements ListIterator<E> {
        /** Snapshot of the array */
        private final Object[] snapshot;
        /** Index of element to be returned by subsequent call to next.  */
        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;
        }

        @SuppressWarnings("unchecked")
        public E next() {
            if (! hasNext())
                throw new NoSuchElementException();
            return (E) snapshot[cursor++];
        }

        @SuppressWarnings("unchecked")
        public E previous() {
            if (! hasPrevious())
                throw new NoSuchElementException();
            return (E) snapshot[--cursor];
        }

        public int nextIndex() {
            return cursor;
        }

        public int previousIndex() {
            return cursor-1;
        }

        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code remove}
         *         is not supported by this iterator.
         */
        public void remove() {
            throw new UnsupportedOperationException();
        }

        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code set}
         *         is not supported by this iterator.
         */
        public void set(E e) {
            throw new UnsupportedOperationException();
        }

        /**
         * Not supported. Always throws UnsupportedOperationException.
         * @throws UnsupportedOperationException always; {@code add}
         *         is not supported by this iterator.
         */
        public void add(E e) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void forEachRemaining(Consumer<? super E> action) {
            Objects.requireNonNull(action);
            Object[] elements = snapshot;
            final int size = elements.length;
            for (int i = cursor; i < size; i++) {
                @SuppressWarnings("unchecked") E e = (E) elements[i];
                action.accept(e);
            }
            cursor = size;
        }
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
一点也不意外,也是找了以前的老的引用–snapshot,所有的迭代都是在迭代老的数组,同时迭代器不支持写操作,add,set,remove会抛出异常。

所有的size,contains也都是统计的老的数组的值。toArray也是拷贝。

总结:

COW写时复制,适合读多写少场景 
读是可以并发的,读的数据可能是老的 
写时互斥的,互斥还是通过ReentrantLock来控制的。 
底层是没有容量的volatile修饰的Object[]

锁是整个并发包的基础,读锁事半功倍。
 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值