基于jdk11
由于ArrayList操作并发下非线程安全,因为当一个线程在读,另外一个线程在写这样会造成线程不安全,因此引出了CopyOnWriteArrayList 也叫写时复制集合,而为什么要把CopyOnWriteArraySet也在这里说的主要是这个Set内部大部分是基于CopyOnWriteArrayList实现的,二者基本都是一致,只是一个可以存储重复数据一个不行,下面分析也是重点分析list
CopyOnWriteArrayList 是它支持并发情况下的使用,其内部的数据结构也是使用对象数组,通过同步锁和拷贝数组来保证并发安全。
根据它名称来分析就是写的时候复制,写时复制内部原理是当一个线程对集合进行add操作时,并不会直接在原数组添加,而是通过拷贝原数组到一个新数组后再进行add操作,同时在执行这个操作的时候还会在代码级别上加上同步锁(可以防止出现复制多个对象数组出现栈溢出),当完成添加数据后再把原对象数组指向这个新数组。这样进行读操作的时候不需要进行锁也不会出现并发操作。
下面开始看源码:
先看CopyOnWriteArrayList的全局遍历
public class CopyOnWriteArrayList
implements List, 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 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 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 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 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源码分析