使用 CopyOnWrite 实现并发写操作

x33g5p2x  于2022-03-19 转载在 其他  
字(3.2k)|赞(0)|评价(0)|浏览(180)

一 点睛

同步类容器是一种串行化、线程安全的容器,在特定情况下对资源加锁。因此在多线程环境中,会降低应用的吞吐量,另外,同步容器类在早期设计时没有考虑一些并发问题,因此在使用时经常会出现 ConcurrentModificationException 等并发异常。

| <br>同步类容器<br> | <br>并发类容器<br> |
| <br>HashTable<br> | <br>ConcurrentHashMap<br> |
| <br>Vector<br> | <br>CopyOnWriteArrayList<br> |
| <br>Stack<br> | <br>CopyOnWriteArraySet<br> |

二 并发读写

1 代码

package concurrent;

import java.util.Iterator;
import java.util.Vector;

public class TestCopyOnWriteArrayList {
    public static void main(String[] args) {
        Vector<String> names = new Vector<>();//1.5
        names.add("zs");
        names.add("ls");
        names.add("ww");
        Iterator<String> iter = names.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next()); // 仅仅对集合进行读操作,不会有异常
            names.add("x"); //仅仅对集合进行写操作:因为ArrayList会动态扩容(1.5倍),因此names会无限扩大,因此会报错
        }
    }
}

2 运行结果

zs

Exception in thread "main" java.util.ConcurrentModificationException

at java.util.Vector$Itr.checkForComodification(Vector.java:1210)

at java.util.Vector$Itr.next(Vector.java:1163)

at concurrent.TestCopyOnWriteArrayList.main(TestCopyOnWriteArrayList.java:24)

3 说明

如果将 Vector 修改为 ArrayList,也会报 ConcurrentModificationException 异常。

造成 ConcurrentModificationException 异常的原因有以下几点。

a ArrayList 有一个全局变量 modCount(从父类 AbstractList 继承而来),并且在 ArrayList 内部类 ITR 中有一个 expectedModCount 变量。

b 当对 ArrayList 进行迭代(iter.next)时,迭代器会先确保 modCount 和 expectedModCount 的值一致,如果不一致就会抛出 ConcurrentModificationException 异常。

c 在本案例中,在迭代的同时,又进行了写操作(name.add(...)),而写操作会改变 modCount 的值(modCount++),因此就导致 modCount != expectedModCount ,最终抛出 ConcurrentModificationException 异常。

三 源码分析

1 modCount 定义位置

public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
    protected transient int modCount = 0;
}

2 add 的时候会修改 modCount 的值

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }
    private void ensureCapacityInternal(int minCapacity) {
        ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
    }
    private void ensureExplicitCapacity(int minCapacity) {
        modCount++;
        // overflow-conscious code
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
}

3 如果 modCount 和 expectedModCount 不一致,抛出异常

private class Itr implements Iterator<E> {
    int expectedModCount = modCount;
    public E next() {
        checkForComodification();
    }
    final void checkForComodification() {
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
    }
}

四 解决方法

一种解决 ConcurrentModificationException 异常的简单方法,就是将之前使用的同步类容器,改为并发类容器。

JUC 提供了多种并发类容器来改善性能,并且也解决了上述异常。因此,如果在多线程环境下编程,建议使用并发类容器来替代传统的同步类容器。

1 代码

package concurrent;

import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;

public class TestCopyOnWriteArrayList {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> names = new CopyOnWriteArrayList<>();
        names.add("zs");
        names.add("ls");
        names.add("ww");
        Iterator<String> iter = names.iterator();
        while (iter.hasNext()) {
            System.out.println(iter.next());
            names.add("x");
        }
    }
}

2 运行结果

zs

ls

ww

五 原理说明

CopyOnWrite 容器增加元素,会经历以下两步。

1 先将当前容器复制一份,然后向新的容器(复制后的容器)里添加元素(并不会直接向当前容器增加元素)。

2 增加完元素后,再将引用指向新的容器,原容器等待被 GC 收集。

对于“读多写少”的业务,更适合使用 CopyOnWrite 容器,但如果是“写多读少”就不适合,因为容器复制比较消耗性能。

相关文章