JUC 源码分析 CopyOnWriteList

143 阅读2分钟

依据JDK8源码

内存一致性效果:一个线程往List中插入(更新也算插入)元素及其之前的动作 happens-before 其他线程后续访问或者移除该元素的动作(人话是:我只要插入元素,其他线程就能访问到以及删除。)

多线程共享的资源:数组的引用array

    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;

get操作的弱一致性

    /**
     * {@inheritDoc}
     *
     * @throws IndexOutOfBoundsException {@inheritDoc}
     */
    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];
    }

get操作没有进行同步,导致可能get到被删除或者更新掉的一个元素。

获取元素的操作分2步,第一步获取数组array。这一步因为是volatile变量建立了同步,因此其他线程在此期间对array的写入是可见的。可以读取到array的最新值。

第二步是对该数组取索引。如果在第一步之后第二步之前其他线程删除了这个元素(即修改了array)。那么是不可见的。因为取的是老array。因此读取了一个被删除的一个元素。

这就是get操作下的弱一致性问题

添加元素

   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();
        }
    }

添加元素 拷贝原来的数组到新的数组上。通过锁来同步保证了原子性与可见性

修改元素

    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 {
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);                                  //1
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }

注意这里的语句1部分,如果老值与新值一样,那么可以不操作直接返回。需要利用setArray(elements)的写入volatile来与后续的访问建立一个Happens before关系。这里可以去掉这个吗???

更新:JDK 11已经去掉了元素相同时候的setArray()。所以是可以去掉的。

删除元素

    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();
        }
    }

总结

  1. 获取List中元素为了保证性能,没有加锁。单纯利用volatile来同步,导致存在弱一致性问题(如果get的那个元素被删除或者更新了,读线程是不知道的)
  2. 修改List例如增加元素,删除元素,修改元素。因为我们采用写时复制的策略,那么修改List分为获取array引用,拷贝原数组到新数组,将新数组引用写入到array。为了保障原子性。我们必须加锁。

Ref

  1. stackoverflow.com/questions/2…