【JUC】COW容器浅析

文章目录

    • 1、什么是COW
    • 2、Java中的Cow容器
    • 3、CopyOnWriteArrayList源码分析
    • 4、COW容器优缺点及适用场景

1、什么是COW

维基百科定义:
  写入时复制(英语:Copy-on-write,简称COW)是一种计算机程序设计领域的优化策略。其核心思想是,如果有多个调用者(callers)同时请求相同资源(如内存或磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者试图修改资源的内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。
  此作法主要的优点是如果调用者没有修改该资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
大白话:
  通俗的讲就是,cow基于“写时复制”的思想,当并发请求读取相同的数据资源时,读取的是同一份数据,当某个请求尝试去修改资源时,就会通过复制出一个副本进而去修改副本的数据,其他请求读取的数据还是最初不变的,最后将指向源数据的引用指向此线程修改后的副本数据。

2、Java中的Cow容器

  Java中的Cow容器有两个,在Jdk1.5版本中开始出现的,分别是CopyOnWriteArrayList及CopyOnWriteArraySet。
  CopyOnWriteArrayList与CopyOnWriteArraySet基本一致,主要区别是在add方法,CopyOnWriteArraySet,有set的特性,即存储元素的是不重复的,因此CopyOnWriteArraySet的add方法中使用的是addIfAbsent(E e),即只有当元素不存在的时候,才会将元素添加到集合的尾部。

3、CopyOnWriteArrayList源码分析

  从源码中,可以看出CopyOnWriteArrayList内部持有一个ReentrantLock锁,最重要属性array是一个Object类型的数组并且有volatile关键字修饰,而且这个array只能通过getArray()和setArray()来进行访问。

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

    /** The array, accessed only via getArray/setArray. */
    private transient volatile Object[] array;
    /**
     * 获取源数组
     */
    final Object[] getArray() {
        return array;
    }

    /**
     *将源数组引用指向新数组
     */
    final void setArray(Object[] a) {
        array = a;
    }

add()方法:

    public boolean add(E e) {
        final ReentrantLock lock = this.lock;
        // 1、加锁
        lock.lock();
        try {
        	// 2、获取源数组引用
            Object[] elements = getArray();
            int len = elements.length;
        	// 3、拷贝出一个新数组,新数组长度=源数组长度 + 1
            Object[] newElements = Arrays.copyOf(elements, len + 1);
            // 4、将元素添加到新数组的尾部
            newElements[len] = e;
            // 5、将源数组引用指向新数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }

add()方法主要工作流程如下:

  • 先进行加锁操作
  • 通过getArray()方法获取源数组
  • 拷贝出一个新数组,并且新数组的长度加1
  • 将要添加的元素追加到新数组的尾部
  • 通过setArray()方法将源数组引用指向新数组
  • 最后释放锁

remove()方法

public boolean remove(Object o) {
	// 1、获取数组快照
    Object[] snapshot = getArray();
    // 2、获取要移除元素的索引下标
    int index = indexOf(o, snapshot, 0, snapshot.length);
    // 3、若未找到,则直接返回false,否则调用remove方法进行移除
    return (index < 0) ? false : remove(o, snapshot, index);
}
private static int indexOf(Object o, Object[] elements,
                           int index, int fence) {
    // 若要移除的元素为null,则直接遍历数组,找到第一个值为null的数组下标返回                       
    if (o == null) {
        for (int i = index; i < fence; i++)
            if (elements[i] == null)
                return i;
    } else {
    // 遍历数组,通过equals方法找到要移除的元素的下标返回
        for (int i = index; i < fence; i++)
            if (o.equals(elements[i]))
                return i;
    }
    // 未找到要删除的元素,默认返回-1
    return -1;
}
private boolean remove(Object o, Object[] snapshot, int index) {
    final ReentrantLock lock = this.lock;
    // 1、加锁
    lock.lock();
    try {
    	// 2、获取源数组
        Object[] current = getArray();
        int len = current.length;
        // 3、若快照与当前数组不等,则说明并发情况下源数组已经被改变
        if (snapshot != current) findIndex: {
        	// 取较小数组长度
            int prefix = Math.min(index, len);
            // 遍历判断是否能能找到要删除的元素的下标,若找到则跳出if语句
            for (int i = 0; i < prefix; i++) {
                if (current[i] != snapshot[i] && eq(o, current[i])) {
                    index = i;
                    break findIndex;
                }
            }
            // 若之前定位的数组下标大于当前源数组长度,则直接返回false
            if (index >= len)
                return false;
            // 若源数组中索引下标位置的元素与要删除的元素相等,则跳出if语句
            if (current[index] == o)
                break findIndex;
            // 遍历获取要删除的元素下标
            index = indexOf(o, current, index, len);
            if (index < 0)
                return false;
        }
        // 创建新数组
        Object[] newElements = new Object[len - 1];
        // 将当前数组中索引下标位置之前的元素先拷贝到新数组中
        System.arraycopy(current, 0, newElements, 0, index);
        // 将当前数组中索引下标位置止呕的元素再拷贝到新数组中
        System.arraycopy(current, index + 1,
                         newElements, index,
                         len - index - 1);
        // 通过setArray()方法将源数组引用指向新数组
        setArray(newElements);
        return true;
    } finally {
    	// 解锁
        lock.unlock();
    }
}

remove()方法的工作流程其实也不复杂,顺着源码往下看就能理顺,主要流程如下:

  • 先在无锁的情况下,在当前数组中寻找要移除的元素下标,若未找到,则直接返回,否则调用重写的remove()方法
  • 调用重写的remove()方法,会先进行加锁操作
  • 然后看当前数组在加锁前是否已经发生变化(和未加锁时获取的源数组进行比较),因为并发情况下,源数组可能已经被其他修改操作修改而发生变化。
  • 若当前数组发生变化,则尝试在获取要删除的元素的下标
  • 找到要移除的元素下标,则生成新数组,并进行数组元素拷贝;否则直接返回
  • 最后将源数组引用指向新数组
  • 最后再释放锁

4、COW容器优缺点及适用场景

  从源码中我们就能体会到,cow容器提供了在修改操作时,采用复制新数组的方式,并在修改操作(添加或删除)中加锁,读取操作在并发情况下并不能保证读取的元素是最新的,但是修改操作会保证数据的最终一致性。
优点:

  • 在多线程并发场景中,以牺牲空间来换取数据被并发修改的最终一致性,尤其适合读多写少的场景
  • 并发修改操作(添加或删除)不会出现并发修改异常(ConcurrentModificationException)

缺点:

  • 不适合写操作比较多的场景,写操作由于加锁,会影响性能
  • 修改操作,会额外占用一倍的内存空间

适用场景:

  • 多线程并发情况下,且读多写少的场景,容忍牺牲一部分空间来换取多线程环境下的数据的最终一致性

你可能感兴趣的:(Java多线程与并发专栏,多线程,并发编程,cow,java)