(一) 并发容器之 CopyOnWriteArrayList

总所周知, ArrayList 是线程不安全的, 而 Vector 的性能太弱了 (直接锁对象), 基本不会使用, JDK 1.5 在 J.U.C 包中提供了一个并发 List : CopyOnWriteArrarList

ArrayList, Vector, LinkedList 三者的区别

ArrayList : 底层是 Object 数组, 使用线性存储方式

特点: 插入慢, 查询快, 线程不安全

Vector : 同 ArrayList 一样 继承了 AbstractList 实现了 List 接口

特点: 线程安全, 使用了同步锁, 但是性能太低, 即使两个线程同时进行读操作都会产生互斥,

但是实际上, 分析源码会发现, 确实很多方法加了 synchronized 同步关键字, 从而保证所有的对外接口方法都会以 Vector 对象为锁, 即 Vector 内部, 所有的方法都不会被多线程访问.

但事实, 单个方法的原子性, 并不能保证复合操作也是原子性的

虽然源代码注释里面说这个是线程安全的,因为确实很多方法都加上了同步关键字 synchronized,但是对于符合操作而言,只是同步方法并没有解决线程安全的问题。要真正达成线程安全,还需要以 vector 对象为锁,来进行操作。所以,如果是这样的话,那么用 vector 和 ArrayList 就没有区别了,所以,不推荐使用 vector。

LinkedList: 底层由链表实现, 使用了链式存储方式

特点: 插入快, 查询慢,线程不安全

ArrayList, LinkedList 集合类都实现了懒加载的方式 (在集合容器初始化的过程中并不会为容器分配内存空间, 只在调用 add 方法的时候才分配内存)

ArrayList 扩容机制

   private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
    	// 位移操作, 相对于 / 操作速度更快
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);// 检查数组长度是否最大限制
        / /使用 Arrays 工具类拷贝数组
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

选择 1.5 倍的原因是考虑到空间和时间的情况下, 如果太小, 就会在条件元素的时候频繁的进行扩容, 影响程序运行速度, 如果太大, 对内存空间会造成一定的浪费

并且 ArrayLis t的扩容消费相对于 HashMap 更小, 所以允许 ArrayList 更频繁的扩容

使用 ArrayList, 在高并发多线程的情况下, 会抛出 java.util.ConcurrentModificationException 异常

static List<String> list = new ArrayList<>();

public static void main(String[] args) throws Exception {
    for (int i = 0; i < 20; i++) {
        new Thread(() -> test()).start();
    }
}

static void test() {
    list.add(UUID.randomUUID().toString().substring(0, 8));
    System.out.println(list); // 抛出 ConcurrentModificationException 异常
}

如何解决ArrayList在多线程的情况下不安全的问题?

  • ​使用 Vector?

    千万不要,,虽然 Vector 在方法上加了锁,保证数据的安全性,但是程序并发执行的效率明显下降, 所以在考虑要保证,数据安全却不考虑执行效率的情况下使用 Vector

  • java.util.Collections 工具类

    里面有很多内部类, 其中 synchronizedList 方法接受一个 List,然后返回线程安全的List,可以解决抛出异常的问题(里面锁的是一个 Object mutex 对象), 不止 List, Set 和 Map 同样可以使用这种方式

  • java.util.concurrent.CopyOnWriteArrayList

    这个选择才是最优的,读写分离并发容器

CopyOnWriteArrayList

public class CopyOnWriteArrayList<E>
    implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
   
    /** The lock protecting all mutators */
    final transient ReentrantLock lock = new ReentrantLock();

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

    .....

    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> k = CopyOnWriteArrayList.class;
            lockOffset = UNSAFE.objectFieldOffset
                (k.getDeclaredField("lock"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
    
}

查看 CopyOnWriteArrayList 源码,你会发现,里面用到了 CAS,ReentrantLock , 查看他的 add 方法

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();// 锁
    try {
        Object[] elements = getArray();
        int len = elements.length;
        // 数组拷贝,并非 1.5 倍扩容,而是将数组长度加一
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        // 覆盖旧的数组
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

往一个容器里添加元素的时候, 不直接往容器中添加,而是将容器复制一份,其长度会加一,可以在新的容器中进行添加元素,添加完元素后,在将原容器的引用更新为新的容器.

这样就可以做到读写分离 (读容器中的数据可以在就容器中完成, 而写操作则是在新的容器中进行)

猜你喜欢

转载自blog.csdn.net/Gp_2512212842/article/details/107689590