【JUC系列】并发容器之CopyOnWrite(CopyOnWriteArrayList、CopyOnWriteArraySet)

CopyOnWrite

文章目录

  • CopyOnWrite
    • CopyOnWriteArrayList
      • 核心组成
        • 内部类-迭代器COWIterator
        • 成员变量
        • 构造函数
        • 核心方法
          • **boolean add(E e)**
          • **addIfAbsent(E e, Object[] snapshot)**
          • **remove(int index)**
          • **set(int index, E element)**
        • Arrays.copyOf方法
        • System.arraycopy方法
      • 示例
      • 使用场景
    • CopyOnWriteArraySet

CopyOnWriteArrayList

java.util.ArrayList 的线程是安全的,其中所有可变操作(添加、设置等)都是通过制作底层数组的新副本来实现的。

这通常成本太高,但当遍历操作的数量大大超过突变时,它可能比替代方法更有效,并且在您不能或不想同步遍历但需要排除并发线程之间的干扰时很有用。

snapshot风格的迭代器方法使用对创建迭代器时数组状态的引用。这个数组在迭代器的生命周期内永远不会改变,所以干扰是不可能的,并且迭代器保证不会抛出ConcurrentModificationException。自创建迭代器以来,迭代器不会反映对列表的添加、删除或更改。不支持迭代器本身的元素更改操作(删除、设置和添加)。这些方法抛出UnsupportedOperationException

允许所有元素,包括 null。

此类实现了List, RandomAccess, Cloneable, java.io.Serializable四个类说明。

  • 支持对列表的基本操作
  • 支持随机访问
  • 支持克隆
  • 支持可序列化

核心组成

内部类-迭代器COWIterator
    static final class COWIterator<E> implements ListIterator<E> {
        /** 作为快照的数组 */
        private final Object[] snapshot;
        /** 数组的下标 可以理解为游标 */
        private int cursor;

        /** 构造函数 传入参数为元素数组和数组游标*/
        private COWIterator(Object[] elements, int initialCursor) {
            cursor = initialCursor;
            snapshot = elements;
        }

        /** 通过比较游标与快照数组的长度比较,检查快照数组是否还有元素*/
        public boolean hasNext() {
            return cursor < snapshot.length;
        }

        /** 若游标大于0,代表游标代表的下标之前还有元素*/
        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.
         * 不支持remove
         */
        public void remove() {
            throw new UnsupportedOperationException();
        }

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

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

        /** 遍历数组支持Consumer*/
        @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;
        }
    }

主要使用构造器方式,获取当时的数组形成数组快照。

return new COWIterator<E>(getArray(), 0);
成员变量
	/** 可重入锁 */
	final transient ReentrantLock lock = new ReentrantLock();
	/** 对象数组,用于存放元素 */
    private transient volatile Object[] array;
构造函数
    /**
     * 默认构造函数
     */
	public CopyOnWriteArrayList() {
        setArray(new Object[0]);
    }

    /**
     * 参数为集合型的构造函数
     */
    public CopyOnWriteArrayList(Collection<? extends E> c) {
        Object[] elements;
        // 对CopyOnWriteArrayList类,直接获取数组
        if (c.getClass() == CopyOnWriteArrayList.class)
            elements = ((CopyOnWriteArrayList<?>)c).getArray();
        // 若类型不同
        else {
            // 将集合转化为数组赋予elements
            elements = c.toArray();
            // c.toArray might (incorrectly) not return Object[] (see 6260652)
            if (elements.getClass() != Object[].class) // elements类型不为Object[]类型
                // 将elements数组转化为Object[]类型的数组
                elements = Arrays.copyOf(elements, elements.length, Object[].class);
        }
        // 设置数组
        setArray(elements);
    }

    /**
     * 参数为数组型的构造函数
     */
    public CopyOnWriteArrayList(E[] toCopyIn) {
        // 将toCopyIn转化为Object[]类型数组,然后设置当前数组
        setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
    }
核心方法
方法名 描述
int size() 列表中元素的个数
boolean isEmpty() 列表是否为空,true为空,false为非空
boolean contains(Object o) 如果此列表包含指定元素,则返回 true。 更正式地说,当且仅当此列表包含至少一个元素 e 满足(o==null ? e==null : o.equals(e))时,才返回 true。
int indexOf(Object o) 返回此列表中指定元素第一次出现的索引,如果此列表不包含该元素,则返回 -1。 更正式地说,返回满足(o==null ? get(i)==null : o.equals(get(i)))的最低索引 i,如果没有这样的索引,则返回 -1。
int indexOf(E e, int index) 返回此列表中指定元素第一次出现的索引,从索引向前搜索,如果未找到该元素,则返回 -1。 更正式地说,返回最低索引 i 使得 (i >= index && (e==null ? get(i)==null : e.equals(get(i)))),如果不存在则返回 -1 指数。
int lastIndexOf(Object o) 返回此列表中指定元素最后一次出现的索引,如果此列表不包含该元素,则返回 -1。 更正式地说,返回满足(o==null ? get(i)==null : o.equals(get(i))) 的最高索引 i,如果没有这样的索引,则返回 -1。
int lastIndexOf(E e, int index) 返回此列表中指定元素的最后一次出现的索引,从索引向后搜索,如果未找到该元素,则返回 -1。 更正式地说,返回最高索引 i 使得(i <= index && (e==null ? get(i)==null : e.equals(get(i)))),如果不存在则返回 -1 指数。
Object clone() 返回此列表的浅拷贝。 (元素本身不会被复制。)
Object[] toArray() 以正确的顺序(从第一个元素到最后一个元素)返回包含此列表中所有元素的数组。
返回的数组将是“安全的”,因为此列表不维护对它的引用。 (换句话说,这个方法必须分配一个新数组)。 因此,调用者可以自由修改返回的数组。
此方法充当基于数组和基于集合的 API 之间的桥梁。
T[] toArray(T a[]) 以正确的顺序(从第一个元素到最后一个元素)返回一个包含此列表中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。如果列表适合指定的数组,则在其中返回。否则,将使用指定数组的运行时类型和此列表的大小分配一个新数组。
如果此列表适合指定的数组并有剩余空间(即,数组的元素比此列表多),则数组中紧随列表末尾的元素设置为 null。 (仅当调用者知道此列表不包含任何空元素时,这在确定此列表的长度时才有用。)
与 toArray() 方法一样,此方法充当基于数组的 API 和基于集合的 API 之间的桥梁。此外,此方法允许对输出数组的运行时类型进行精确控制,并且在某些情况下可用于节省分配成本。
假设 x 是一个已知仅包含字符串的列表。以下代码可用于将列表转储到新分配的 String 数组中:
String[] y = x.toArray(new String[0]);
请注意,toArray(new Object[0]) 在功能上与 toArray() 相同。
E get(int index) 返回此列表中指定位置的元素。
E set(int index, E element) 将此列表中指定位置的元素替换为指定元素。
boolean add(E e) 将指定元素附加到此列表的末尾。
void add(int index, E element) 在此列表中的指定位置插入指定元素。 将当前位于该位置的元素(如果有)和任何后续元素向右移动(将其索引加一)。
E remove(int index) 移除此列表中指定位置的元素。 将任何后续元素向左移动(从它们的索引中减去 1)。 返回从列表中删除的元素。
boolean remove(Object o) 从此列表中删除第一次出现的指定元素(如果存在)。 如果此列表不包含该元素,则它不变。 更正式地说,删除具有最低索引 i 的元素,使得(o==null ? get(i)==null : o.equals(get(i)))(如果存在这样的元素)。 如果此列表包含指定的元素(或等效地,如果此列表因调用而更改),则返回 true。
boolean addIfAbsent(E e) 追加元素(如果不存在)。
boolean containsAll(Collection c) 如果此列表包含指定集合的所有元素,则返回 true。
boolean removeAll(Collection c) 从此列表中删除包含在指定集合中的所有元素。 由于需要内部临时数组,因此在此类中这是一项特别昂贵的操作。
boolean retainAll(Collection c) 仅保留此列表中包含在指定集合中的元素。 换句话说,从这个列表中删除所有不包含在指定集合中的元素。
int addAllAbsent(Collection c) 按照指定集合的迭代器返回的顺序,将指定集合中尚未包含在此列表中的所有元素附加到此列表的末尾。
void clear() 从此列表中删除所有元素。 此调用返回后,列表将为空。
boolean addAll(Collection c) 按照指定集合的迭代器返回的顺序,将指定集合中的所有元素附加到此列表的末尾。
boolean addAll(int index, Collection c) 将指定集合中的所有元素插入此列表,从指定位置开始。 将当前位于该位置的元素(如果有)和任何后续元素向右移动(增加它们的索引)。 新元素将按照指定集合的迭代器返回的顺序出现在此列表中。
void forEach(Consumer action)
boolean removeIf(Predicate filter)
void replaceAll(UnaryOperator operator)
void sort(Comparator c)
Iterator iterator() 以正确的顺序返回此列表中元素的迭代器。
返回的迭代器提供了构造迭代器时列表状态的快照。 遍历迭代器时不需要同步。 迭代器不支持 remove 方法。
ListIterator listIterator() 返回此列表中元素的列表迭代器(以正确的顺序)。
返回的迭代器提供了构造迭代器时列表状态的快照。 遍历迭代器时不需要同步。 迭代器不支持删除、设置或添加方法。
ListIterator listIterator(int index) 返回此列表中元素的列表迭代器(以正确的顺序),从列表中的指定位置开始。 指定的索引指示初始调用 next 将返回的第一个元素。 对 previous 的初始调用将返回具有指定索引减一的元素。
返回的迭代器提供了构造迭代器时列表状态的快照。 遍历迭代器时不需要同步。 迭代器不支持删除、设置或添加方法。
Spliterator spliterator() 在此列表中的元素上返回一个 Spliterator。
Spliterator 报告 Spliterator.IMMUTABLE、Spliterator.ORDERED、Spliterator.SIZED 和 Spliterator.SUBSIZED。
拆分器在构造拆分器时提供列表状态的快照。 在分离器上操作时不需要同步。
List subList(int fromIndex, int toIndex) 返回此列表在 fromIndex(包括)和 toIndex(不包括)之间部分的视图。 返回列表由该列表支持,因此返回列表中的更改会反映在该列表中。
如果通过返回列表以外的任何方式修改后备列表(即此列表),则此方法返回的列表的语义将变为未定义。
boolean 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);
            // 新增元素e,添加到复制数组中
            newElements[len] = e;
            // 重置数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
addIfAbsent(E e, Object[] snapshot)
	/** 使用给定最近快照不包含 e 的强提示的 addIfAbsent 版本。*/
	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++)
                    // 当前数组的元素与快照的元素不相等并且e与当前元素相等
                    // 表示在snapshot与current之间修改了数组,并且设置了数组某一元素为e,已经存在
                    if (current[i] != snapshot[i] && eq(e, current[i]))
                        return false;
                // 在当前数组中找到e元素
                if (indexOf(e, current, common, len) >= 0)
                        return false;
            }
            // 复制源数组
            Object[] newElements = Arrays.copyOf(current, len + 1);
            // 对数组len索引的元素赋值为e
            newElements[len] = e;
            // 重置源数组
            setArray(newElements);
            return true;
        } finally {
            lock.unlock();
        }
    }
remove(int index)
	/** 移除此列表中指定位置的元素。 将任何后续元素向左移动(从它们的索引中减去 1)。 返回从列表中删除的元素。*/
	public E remove(int index) {
        final ReentrantLock lock = this.lock;
        // 通过锁保证线程的安全性
        lock.lock();
        try {
            Object[] elements = getArray();
            int len = elements.length;
            // 获取到下标为index的元素
            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();
        }
    }
set(int index, E element)
	/**  将此列表中指定位置的元素替换为指定元素。*/
	public E set(int index, E element) {
        final ReentrantLock lock = this.lock;
        // 通过锁保证线程的安全性
        lock.lock();
        try {
            Object[] elements = getArray();
            // 获取到下标为index的元素
            E oldValue = get(elements, index);
            // 若新元素与旧元素不同,则重置数组,将新值更新到数组。
            if (oldValue != element) {
                int len = elements.length;
                Object[] newElements = Arrays.copyOf(elements, len);
                newElements[index] = element;
                setArray(newElements);
            } else {
                // 不是完全没有操作; 确保volatile的写语义
                // Not quite a no-op; ensures volatile write semantics
                setArray(elements);
            }
            return oldValue;
        } finally {
            lock.unlock();
        }
    }
Arrays.copyOf方法

static T[] copyOf(T[] original, int newLength)

	/*
    * 复制指定的数组,截断或填充空值(如有必要),使副本具有指定的长度。 
    * 对于在原始数组和副本中都有效的所有索引,这两个数组将包含相同的值。 
    * 对于在副本中有效但在原始数组中无效的任何索引,副本将包含 null。
    * 当且仅当指定长度大于原始数组的长度时,此类索引才会存在。 结果数组属于 newType 类。
    * Params:
        original  – 被复制的数组(原始数组)
        newLength – 副本数组的长度
        newType   – 副本数组的class
    * Type parameters:
         – 返回副本数组中对象的类
         – 原始数组中对象的类
	*/
	public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) {
        @SuppressWarnings("unchecked")
        // 确定副本数组的class类型
        // (将newType转化为Object类型,将Object[].class转化为Object类型,判断两者是否相等,若相等,则生成指定长度的Object数组,否则,生成指定长度的新类型的数组)
        T[] copy = ((Object)newType == (Object)Object[].class)
            ? (T[]) new Object[newLength]
            : (T[]) Array.newInstance(newType.getComponentType(), newLength);
        // 将original数组从下标0开始,复制长度为(original.length和newLength的较小者),复制到copy数组中(也从下标0开始)
        System.arraycopy(original, 0, copy, 0,
                         Math.min(original.length, newLength));
        return copy;
    }
System.arraycopy方法
    /*
    * 将指定源数组中的数组从指定位置开始复制到目标数组的指定位置。 
    * 数组组件的子序列从 src 引用的源数组复制到 dest 引用的目标数组。 复制的组件数量等于长度参数。 
	*/
	public static native void arraycopy(Object src,  int  srcPos,	
                                        Object dest, int destPos,
                                        int length);

源数组中位置srcPossrcPos+length-1的数据项分别复制到目标数组的位置destPosdestPos+length-1

如果srcdest参数引用同一个数组对象,则执行复制,就好像首先将位置srcPossrcPos+length-1的数据项复制到具有相同长度数据项的临时数组,然后临时数组的内容是 通过目标数组的destPos+length-1复制到位置destPos

如果 dest 为 null,则抛出NullPointerException

如果 src 为 null,则抛出NullPointerException并且不修改目标数组。

否则,如果以下任何一项为真,则抛出ArrayStoreException并且不修改目标:

  • src 参数引用一个不是数组的对象。
  • dest 参数引用一个不是数组的对象。
  • src 参数和 dest 参数引用其组件类型为不同原始类型的数组。
  • src 参数是指具有原始组件类型的数组,而 dest 参数是指具有引用组件类型的数组。
  • src 参数是指具有引用组件类型的数组,而 dest 参数是指具有原始组件类型的数组。

否则,如果以下任何一项为真,则抛出IndexOutOfBoundsException并且不修改目标:

  • srcPos 参数是否定的。
  • destPos 参数是否定的。
  • 长度参数是否定的。
  • srcPos+length 大于 src.length,源数组的长度。
  • destPos+length 大于 dest.length,即目标数组的长度。

否则,如果从位置srcPossrcPos+length-1的源数组的任何实际组件不能通过赋值转换转换为目标数组的组件类型,则抛出ArrayStoreException。 在这种情况下,设k为小于length的最小非负整数,使得 src[srcPos+k] 不能转换为目标数组的组件类型; 当抛出异常时,从位置srcPossrcPos+k-1的源数组组件将已经被复制到目标数组位置destPosdestPos+k-1并且目标数组的其他位置不会被修改。 (由于已经逐条列出了限制,本段仅适用于两个数组都具有引用类型的组件类型的情况。)

示例

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Iterator;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.TimeUnit;

public class CopyOnWriteArrayListDemo {

    static class PutThread extends Thread {

        /**
         * 复制类
         */
        private final CopyOnWriteArrayList<Integer> cowal;

        /**
         * 代表毫秒数
         */
        private final long time;

        public PutThread(CopyOnWriteArrayList<Integer> cowal, long time) {
            this.cowal = cowal;
            this.time = time;
        }

        @Override
        public void run() {
            try {
                for (int i = 100; i < 200; i++) {
                    TimeUnit.MILLISECONDS.sleep(time);
                    cowal.add(i);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        CopyOnWriteArrayList<Integer> cowal = new CopyOnWriteArrayList<>();
        // 初始化数组
        for (int i = 0; i < 10; i++) {
            cowal.add(i);
        }
        // 新建一个线程,每隔50ms向cowal数组中添加一个元素
        PutThread p1 = new PutThread(cowal, 50);
        p1.start();
        // 获取cowal某个时刻的快照迭代器,遍历输出
        System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "-" + Thread.currentThread().getName() + "] " + " cowal.size = " + cowal.size() + ".");

        Iterator<Integer> iterator = cowal.iterator();
        StringBuilder cowalString1 = new StringBuilder();
        while (iterator.hasNext()) {
            cowalString1.append(iterator.next()).append(" ");
        }
        System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "-" + Thread.currentThread().getName() + "] " + " cowalString1 = " + cowalString1 + ".");
        // main线程休眠2s,让PutThread执行
        try {
            TimeUnit.MILLISECONDS.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 获取cowal某个时刻的快照迭代器,遍历输出此时的数组
        iterator = cowal.iterator();
        StringBuilder cowalString2 = new StringBuilder();
        while (iterator.hasNext()) {
            cowalString2.append(iterator.next()).append(" ");
        }
        System.out.println("[" + new SimpleDateFormat("HH:mm:ss").format(new Date()) + "-" + Thread.currentThread().getName() + "] " + " cowalString2 = " + cowalString2 + ".");

    }
}

某次执行结果

[17:56:54-main]  cowal.size = 10.
[17:56:54-main]  cowalString1 = 0 1 2 3 4 5 6 7 8 9 .
[17:56:56-main]  cowalString2 = 0 1 2 3 4 5 6 7 8 9 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 .

使用场景

由于CopyOnWriteArrayList主要是依赖复制数组来进行操作,所以存在缺点内存的使用率较高,和当数据多的时候,操作的耗时长一点,使用的是最终一致性无法保证实时性;同样对集合中数据量不可控。

比较适合读多写少的场景。


请先说说非并发集合中Fail-fast机制?

再为什么说ArrayList查询快而增删慢?

对比ArrayList说说CopyOnWriteArrayList的增删改查实现原理? COW基于拷贝

再说下弱一致性的迭代器原理是怎么样的? COWIterator

CopyOnWriteArrayList为什么并发安全且性能比Vector好? Vector对单独的add,remove等方法都是在方法上加了synchronized; 并且如果一个线程A调用size时,另一个线程B 执行了remove,然后size的值就不是最新的,然后线程A调用remove就会越界(这时就需要再加一个Synchronized)。这样就导致有了双重锁,效率大大降低,何必呢。于是vector废弃了,要用就用CopyOnWriteArrayList 吧

CopyOnWriteArrayList有何缺陷,说说其应用场景?

CopyOnWriteArraySet

CopyOnWriteArraySet使用Array实现了一个Set,保证所有元素都不重复。其内部使用CopyOnWriteArrayList实现。

你可能感兴趣的:(#,多线程与并发,JAVA,java)