CopyOnWriteArrayList&CopyOnWriteArraySet
基于jdk11
由于ArrayList操作并发下非线程安全,因为当一个线程在读,另外一个线程在写这样会造成线程不安全,因此引出了CopyOnWriteArrayList 也叫写时复制集合,而为什么要把CopyOnWriteArraySet也在这里说的主要是这个Set内部大部分是基于CopyOnWriteArrayList实现的,二者基本都是一致,只是一个可以存储重复数据一个不行,下面分析也是重点分析list
CopyOnWriteArrayList 是它支持并发情况下的使用,其内部的数据结构也是使用对象数组,通过同步锁和拷贝数组来保证并发安全。
根据它名称来分析就是写的时候复制,写时复制内部原理是当一个线程对集合进行add操作时,并不会直接在原数组添加,而是通过拷贝原数组到一个新数组后再进行add操作,同时在执行这个操作的时候还会在代码级别上加上同步锁(可以防止出现复制多个对象数组出现栈溢出),当完成添加数据后再把原对象数组指向这个新数组。这样进行读操作的时候不需要进行锁也不会出现并发操作。
下面开始看源码:
-
先看CopyOnWriteArrayList的全局遍历
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { private static final long serialVersionUID = 8673264195747942595L; /** * 作为同步锁的对象 */ final transient Object lock = new Object(); /* *存储数据的对象数组,注意这里用Volatile 可以禁止指令重排序,和可以使变量在线程之间是可见的,即a线程修改了改数组,B线程里面知道后进行更新 */ private transient volatile Object[] array; }
-
CopyOnWriteArrayList的构造函数
/** * 无参构造方法,会初始化一个大小尾0的对象数组。 */ public CopyOnWriteArrayList() { setArray(new Object[0]); } /** * *带集合的构造方法 * @param c the collection of initially held elements * @throws NullPointerException if the specified collection is null */ public CopyOnWriteArrayList(Collection<? extends E> c) { Object[] es; //先判断c是否CopyOnWriteArrayList同种类型。 if (c.getClass() == CopyOnWriteArrayList.class) //获取c的数据数组并赋值给es es = ((CopyOnWriteArrayList<?>)c).getArray(); else { //如果不同类型的化直接通过toArray调用系统底层system类获取数据数组 es = c.toArray(); // 如果类型不同则调用Arrays转换类型 if (es.getClass() != Object[].class) es = Arrays.copyOf(es, es.length, Object[].class); } setArray(es); } /** * 如果是数组则直接转换类型后就赋值给array数组 */ public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); }
-
CopyOnWriteArrayList的添加数据操作
它的添加操作是先拷贝一份原数组的数据到新数组,然后在这个新数组进行添加,添加完后把当前array数组指向这个新数组,以上操作均在同步锁内进行。、
add有2个方法,一个是直接在数组末尾添加,一个是指定下标进行添加。
public boolean add(E e) { //用同步锁保证线程安全 synchronized (lock) { 把当前数据赋值给es (相当于拷贝一份) Object[] es = getArray(); //获取当前的数据长度 int len = es.length; //进行数组拷贝并进行扩容 es = Arrays.copyOf(es, len + 1); //添加元素到末尾 es[len] = e; //把CopyOnWriteArrayList中的array指向es; setArray(es); return true; } } /** * 指向index进行添加 */ public void add(int index, E element) { //加同步锁保证线程安全 synchronized (lock) { //拷贝数组 Object[] es = getArray(); int len = es.length; //判断越界问题 if (index > len || index < 0) throw new IndexOutOfBoundsException(outOfBounds(index, len)); 再创建一个新对象数组 Object[] newElements; //计算需要移动的元素个数 int numMoved = len - index; if (numMoved == 0)//如果0即尾部添加 newElements = Arrays.copyOf(es, len + 1); else { //否则创建一个比原数据数组大一的对象数组 newElements = new Object[len + 1]; //先把es中的0~index的数据通过底层数组拷贝到新数组, System.arraycopy(es, 0, newElements, 0, index); //再次通过底层把es中index~index+1之间的数据拷贝的新数组 System.arraycopy(es, index, newElements, index + 1, numMoved); } //把待添加数据插入indexx newElements[index] = element; //把指向新数组 setArray(newElements); } }
-
批量添加数据
//批量添加一个Collection子类元素序列 public boolean addAll(Collection<? extends E> c) { //判断类型是否相同,并且根据不同当情况获取c中的数据对象数组并赋值个新数组cs Object[] cs = (c.getClass() == CopyOnWriteArrayList.class) ? ((CopyOnWriteArrayList<?>)c).getArray() : c.toArray(); if (cs.length == 0) return false; //同步锁或独占锁控制线程安全 synchronized (lock) { //把CopyOnWriteArrayList中的数据数组拷贝一份给es Object[] es = getArray(); //获取es的长度并创建一个新数组,进行添加 int len = es.length; Object[] newElements; //如果原数据数据为空则直接把cs赋值给新数值 if (len == 0 && cs.getClass() == Object[].class) newElements = cs; else { //否则先拷贝es并扩容为len+cs.length的对象数组中并赋值给新数组 newElements = Arrays.copyOf(es, len + cs.length); 在把需要添加的数据通过java底层库实现拷贝 System.arraycopy(cs, 0, newElements, len, cs.length); } setArray(newElements); return true; } } /** * 从指定位置开始添加 * @see #add(int,Object) */ public boolean addAll(int index, Collection<? extends E> c) { Object[] cs = c.toArray(); synchronized (lock) { Object[] es = getArray(); int len = es.length; if (index > len || index < 0) throw new IndexOutOfBoundsException(outOfBounds(index, len)); if (cs.length == 0) return false; //以上操作和上面差不多 //确认需要移动的个数,和创建新数组来存放这些数据 int numMoved = len - index; Object[] newElements; if (numMoved == 0)//如果是尾部则直接扩容数据对象并赋值给新数组 newElements = Arrays.copyOf(es, len + cs.length); else { //否则创建两个数据数组大小的新数组 newElements = new Object[len + cs.length]; //通过java底层拷贝es中0~index的数据到新数组中 System.arraycopy(es, 0, newElements, 0, index); //再拷贝es中index~index+numMoved的数据到新数组下标为index+cs.length的位置之后 System.arraycopy(es, index, newElements, index + cs.length, numMoved); } //最后把需要添加的数据拷贝进新数组中 System.arraycopy(cs, 0, newElements, index, cs.length); //把array指向新数组 setArray(newElements); return true; } }
-
修改操作
public E set(int index, E element) { //加同步锁 synchronized (lock) { //拷贝数组 Object[] es = getArray(); //获取index的旧值 E oldValue = elementAt(es, index); //修改并重新指向新数组 if (oldValue != element) { es = es.clone(); es[index] = element; setArray(es); } return oldValue; } }
-
删除数据
删除操作大体过程,在同步锁中先拷贝一份原数据数组到新数组中,然后计算要删除元素的位置到末尾需要移动的个数,因为删除后需要往前面移动(非尾部数据),其中删除元素是通过System调用底部类库来实现数组拷贝的。
//删除index的元素 public E remove(int index) { //同上 synchronized (lock) { Object[] es = getArray(); int len = es.length; E oldValue = elementAt(es, index); //计算移动的个数,以为删除改元素后,如果非尾部需要将后面的数据往前面移动 int numMoved = len - index - 1; Object[] newElements; //末尾的情况则之间调用Arrays的方法拷贝一些数组完事 if (numMoved == 0) newElements = Arrays.copyOf(es, len - 1); //非尾部元素,需要分两段拷贝,先拷贝0~index到新数组中(不包括index) //然后再拷贝index+1~尾部的数据到新数组中,然后把数据数组对象指向该新数组就好 else { newElements = new Object[len - 1]; System.arraycopy(es, 0, newElements, 0, index); System.arraycopy(es, index + 1, newElements, index, numMoved); } setArray(newElements); return oldValue; } } /** * 根据数值移除 */ public boolean remove(Object o) { //拷贝数组 Object[] snapshot = getArray(); //调用indexOfRange获取该对象所在的下标 int index = indexOfRange(o, snapshot, 0, snapshot.length); //再调用下面的remove方法 return index >= 0 && remove(o, snapshot, index); } private static int indexOfRange(Object o, Object[] es, int from, int to) { //根据o的值判断在那个位置 if (o == null) { for (int i = from; i < to; i++) if (es[i] == null) return i; } else { for (int i = from; i < to; i++) if (o.equals(es[i])) return i; } return -1; } /** * */ private boolean remove(Object o, Object[] snapshot, int index) { //同步锁控制线程安全 synchronized (lock) { //拷贝数据到current数组 Object[] current = getArray(); //获取当前数据长度 int len = current.length; //两者不同时 if (snapshot != current) //证明在传递过程中发生了改变,重新找index。 findIndex: { //那之前确认的index与当前数据数组的长度比较找较短那一个 int prefix = Math.min(index, len); //遍历找到数值相等的位置 for (int i = 0; i < prefix; i++) { if (current[i] != snapshot[i] && Objects.equals(o, current[i])) { index = i; break findIndex; } } //判断index。 if (index >= len) return false; if (current[index] == o) break findIndex; //重新找index index = indexOfRange(o, current, index, len); if (index < 0) return false; } //这里的删除和上面第一个remove操作是一致的 Object[] newElements = new Object[len - 1]; System.arraycopy(current, 0, newElements, 0, index); System.arraycopy(current, index + 1, newElements, index, len - index - 1); setArray(newElements); return true; } }
-
获取s数组倒数第一次出现元素的位置
//根据元素值获取最后一次出现该元素的位置 public int lastIndexOf(Object o) { //拷贝数组 Object[] es = getArray(); //调用lastIndexOfRange获取位置 return lastIndexOfRange(o, es, 0, es.length); } /* *根据从index开始查找最后一次出现的e元素的位置 */ public int lastIndexOf(E e, int index) { Object[] es = getArray(); return lastIndexOfRange(e, es, 0, index + 1); } //遍历查询小便 private static int lastIndexOfRange(Object o, Object[] es, int from, int to) { if (o == null) { for (int i = to - 1; i >= from; i--) if (es[i] == null) return i; } else { for (int i = to - 1; i >= from; i--) if (o.equals(es[i])) return i; } return -1; }
-
介绍一下CopyOnWriteArraySet这个也是写时复制的容器但是是set类型即不能存储重复数据的,但其内部只是存储数据也是使用CopyOnWriteArrayList来存储只是部分方法调用需要进行判断是否存在,这些方法都在CopyOnWriteArrayList中实现的
首先是添加元素方法,如果已经存在则不添加
//先拷贝一份数据到snapshot数组 public boolean addIfAbsent(E e) { //拷贝数据的快照数组中 Object[] snapshot = getArray(); //先判断e元素是否存在如果存在直接返回失败,否则不存在在在进行调用添加方法 return indexOfRange(e, snapshot, 0, snapshot.length) < 0 && addIfAbsent(e, snapshot); } /** * 这个方法是set实际添加元素的方法,里面有进行判断元素是否存在 */ private boolean addIfAbsent(E e, Object[] snapshot) { //同步锁 synchronized (lock) { //再次拷贝当前数据数组 Object[] current = getArray(); int len = current.length; //如果前后不一致说明有线程修改了则再次判断 if (snapshot != current) { //再次判断之前快照数组中是否存在该数据 int common = Math.min(snapshot.length, len); for (int i = 0; i < common; i++) if (current[i] != snapshot[i] && Objects.equals(e, current[i])) return false; //如果快照数组中不存在则遍历当前数组中后面部分,如果不存在则添加 if (indexOfRange(e, current, common, len) >= 0) return false; } //添加元素操作 Object[] newElements = Arrays.copyOf(current, len + 1); newElements[len] = e; setArray(newElements); return true; } }
-
批量添加操作也是基于Set的
public int addAllAbsent(Collection<? extends E> c) { //获取c序列的对象数据数组 Object[] cs = c.toArray(); if (cs.length == 0) return 0; synchronized (lock) { //获取当前Set的数据对象数组 Object[] es = getArray(); int len = es.length; int added = 0;//记录添加成功的个数 //这里判断cs里面的元素是否存在,如果不存在则直接添加 for (int i = 0; i < cs.length; ++i) { Object e = cs[i]; //这里进行了两次判断,一次是判断当前set中是否存在,还有一次是判断当前元素是否在cs前面已经添加过了。如果两者都成立则添加 if (indexOfRange(e, es, 0, len) < 0 && indexOfRange(e, cs, 0, added) < 0) cs[added++] = e; } //如果有添加数据则重新拷贝到新数组并重新指向该新数组 if (added > 0) { Object[] newElements = Arrays.copyOf(es, len + added); System.arraycopy(cs, 0, newElements, len, added); setArray(newElements); } return added; } }
-
CopyOnWriteArraySet中判断一个序列是否是set中的子序列
//先进行类型判断如果不是同类型则跳到list中的containsAll进行判断是否完全存在于集合里面, //如果类型现它则调用比较方法 public boolean containsAll(Collection<?> c) { return (c instanceof Set) ? compareSets(al.getArray(), (Set<?>) c) >= 0 : al.containsAll(c); } //该方法jdk1.8是不存在的 //这里是比较当前数据对象数组中是否包含c的 //1是set是 snapshot的超集,0是两者元素相同,-1是不相同 private static int compareSets(Object[] snapshot, Set<?> set) { // Uses O(n^2) algorithm, that is only appropriate for small // final int len = snapshot.length; // 数组用来判断是否匹配用的 final boolean[] matched = new boolean[len]; // int j = 0; outer: for (Object x : set) { for (int i = j; i < len; i++) { //如果set中的数值和快照数组中的值相同则设置指定位置中元素匹配matched[i]为true if (!matched[i] && Objects.equals(x, snapshot[i])) { matched[i] = true; if (i == j) do { j++; } while (j < len && matched[j]); continue outer; } } return -1; } return (j == len) ? 0 : 1; } //CopyOnWriteArrayList中的用于判断是否包含c序列如果发现一个不存在立刻返回 public boolean containsAll(Collection<?> c) { Object[] es = getArray(); int len = es.length; for (Object e : c) { if (indexOfRange(e, es, 0, len) < 0) return false; } return true; }
其他那些方法类似就不分析了。
总结:CopyOnWriteArrayList和CopyOnWriteArrayList是juc包下的一种支持并发的容器,它的优点是读写都是线程安全的,而且读取速率快,其中通过独占锁(或同步锁)在代码级别上来控制线程安全,但因为写时复制,因此如果原理数据就很大了再拷贝一份,就很吃内存,此外,读取速率是很快读取的会出现类似于幻读/不可重读这样的情况,也就是说数据可能不是最新的,也就是说只能写时复制只是保证了最终一致性,总体感觉二者区别就在于是否可以重复存储元素,另外上面是基于jdk11分析,对比了一下jdk1.8感觉差别也不大,就添加了个别方法
HashTable源码分析
LinkedList源码分析
Vector源码分析
CopyOnWriteArrayList源码分析
SynchorincedList源码分析