什么是安全失败与快速失败?
- 在java.util包下的集合类,它们都有这样一个安全特性,在并发情况下用iterator遍历集合中的元素的时候,如果有线程A在遍历这个集合的同时,同时线程B执行了操作改变了集合的数据结构(增删改),那么程序会直接中断抛出 Concurrent Modification Exception,这种机制,就称作快速失败机制。
- 安全失败机制是集合在被遍历的时候,这个遍历的对象是这个集合的一个副本,也就是操作的不是这个集合对象本身,那么每个线程操作的都是自己获得的一份拷贝,其他线程对这个拷贝的改变自然不会影响到自身遍历的集合。
快速失败原理
- 快速失败:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变 modCount 的值。每当迭代器使用 hashNext() / next() 遍历下一个元素之前,都会检测 modCount 变量是否为 expectedmodCount 值,是的话就返回遍历;否则抛出异常,终止遍历。
- 论据
//这是abstractlist下定义的modCount
protected transient int modCount = 0;
//ArrayList中的Iterator源码
int expectedModCount = modCount;
---------------------华丽分割线--------------------------------
//来看一个测试用例
List<String> jpanMeiZi = new ArrayList<>(); //一个日本妹子集合
jpanMeiZi.add("有村架纯");
jpanMeiZi.add("新垣结衣");
jpanMeiZi.add("石原里美");
jpanMeiZi.add("斋藤飞鸟");
Iterator<String> iterator = jpanMeiZi.iterator(); //获取迭代器
while(iterator.hasNext()) {
String str = iterator.next();
if(str.equals("新垣结衣")) { //由于Gakki有男朋友了,所以当我们遇到她的时候,是时候对他放手了。把她拿掉
jpanMeiZi.remove("新垣结衣");
}
System.out.println(str);
}
//控制台结果
有村架纯
新垣结衣
Exception in thread "main" java.util.ConcurrentModificationException //这个异常就是并发修改结构的结果
at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
at java.util.ArrayList$Itr.next(ArrayList.java:851)
at com.ssp.share.test.Test02.main(Test02.java:17)
Process finished with exit code 1
现在我们从源码角度来看看它为什么会抛出异常
按照流程走:
----1、添加元素:
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 判断数组容量是否支撑再添加一个元素
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//在abstractlist中定义的变量 protected transient int modCount = 0;
//这个变量代表了集合实际被修改的次数,很重要
modCount++;
//下面就是扩容过程,不解释
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
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);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
那么,从这样一个添加元素的过程,我们可以观察到,这个modelCount代表的是这个集合实际上被修改的次数。而我们给出的这段代码中,添加了4个元素,每次添加一个元素,它都会进行一个 modelCount++ 操作,所以它在进入这个 while 循环之前,它的值变成了4。
jpanMeiZi.add("有村架纯");
jpanMeiZi.add("新垣结衣");
jpanMeiZi.add("石原里美");
jpanMeiZi.add("斋藤飞鸟");
Iterator<String> iterator = jpanMeiZi.iterator(); //获取迭代器
while(iterator.hasNext()) {
String str = iterator.next();
if(str.equals("新垣结衣")) { 候,是时候对他放手了。把她拿掉
jpanMeiZi.remove("新垣结衣");
}
System.out.println(str);
}
现在就是重头戏了
进入了 while 循环,执行了里面的语句:
第一条:
public boolean hasNext() {
//返回为true,继续执行循环体内容,size指代集合实际元素个数
//这个cursor它指代一个指向当前元素的一个游标,默认为0
return cursor != size;
}
第二条:
while(iterator.hasNext()) {
String str = iterator.next(); <------执行这条,看编号 1>
if(str.equals("新垣结衣")) {
jpanMeiZi.remove("新垣结衣");
}
System.out.println(str);
}
// 1>我们进入这个方法看看它都做了些什么
public E next() {
//每次读取元素的时候,它都要检查一个这个modelCount的数值有没有被修改。详细看编号2>
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
// 2>
final void checkForComodification() {
//这里就是比较的地方,这个expectedModCount是预期集合被修改的次数
//它被定义在Itr这个类下,int expectedModCount = modCount; 具体看编号3>
//集合如果被修改,那么这个modCount就会被++操作,自然不等于这个expectedModCount
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
// 3> 这个类实现了Iterator接口,自然也就实现了它的方法,而我们所使用的迭代器,就是
//这个类的实例,调用的方法自然就是这个类下的方法了
public Iterator<E> iterator() {
return new Itr(); // <--------------创建了一个实例
}
private class Itr implements Iterator<E> {
int cursor;
int lastRet = -1;
int expectedModCount = modCount; //<---这里,modelCount是在ArrayList父类中定义的一个变量0
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
第三条:
public E next() {
checkForComodification();
int i = cursor; //<---------这里,cursor开始为0,下面的操作就是移动这个游标和进行一个判断操作
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData; //这个ArrayList.this是指我们创建的list对象
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1; //指向下一个
return (E) elementData[lastRet = i]; //返回当前遍历出的元素
}
第四条:
while(iterator.hasNext()) {
String str = iterator.next();
if(str.equals("新垣结衣")) { //<----执行这里,看看里面做了些什么
jpanMeiZi.remove("新垣结衣");
}
System.out.println(str);
}
//做的一些判断
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
第五条:
String str = iterator.next();
if(str.equals("新垣结衣")) {
jpanMeiZi.remove("新垣结衣"); //<---------------执行这里
}
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else { //<-----------------执行这里
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index); //<-----------这才是真正执行删除的方法
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++; // 如果执行了删除进行++操作,后继便于抛出异常
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,numMoved);
elementData[--size] = null; //将后置的元素制空,让GC回收
}
第六条:
while(iterator.hasNext()) {
String str = iterator.next();
if(str.equals("新垣结衣")) {
jpanMeiZi.remove("新垣结衣");
}
System.out.println(str); //<-----------执行打印
}
第七条:
while(iterator.hasNext()) { //<-----------执行此处,后继就和前面差不多了
String str = iterator.next(); //<----------此处执行结束,因为结构上发生了改
// 变,即modCount != expectedModCount mod为5,ex为4,抛出异常,终止程序
if(str.equals("新垣结衣")) {
jpanMeiZi.remove("新垣结衣");
}
System.out.println(str);
}
至此,对快速失败的总结。一个线程在通过迭代器遍历这个集合的同时,另外一个线程在对这个集合进行结构上的改变。由于操纵的是这个集合本身,导致异常抛出。
- 额外补充:
-----> 还是这个例子,不过注意,此时的集合被我删除了一个元素,现在只有三个
List<String> jpanMeiZi = new ArrayList<>(); //一个日本妹子集合
jpanMeiZi.add("有村架纯");
jpanMeiZi.add("新垣结衣");
jpanMeiZi.add("石原里美"); // <-----这里打断点,开始Debug
Iterator<String> iterator = jpanMeiZi.iterator(); //获取迭代器
while(iterator.hasNext()) {
String str = iterator.next();
if(str.equals("新垣结衣")) {
jpanMeiZi.remove("新垣结衣");
}
System.out.println(str);
}
那么我们来看看,它会发生什么?下面方法按顺序执行:
public boolean hasNext() {
return cursor != size; //<-------这个光标需要注意,开始的时候它是0 != 3 返回true
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1; //<--------光标变成了1
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
//执行完第一个
控制台输出:有村架纯
//继续执行
public boolean hasNext() {
return cursor != size; //<------- 1 != 3 返回true
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1; //<------- cursor 变成了2
return (E) elementData[lastRet = i];
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
while(iterator.hasNext()) {
String str = iterator.next();
if(str.equals("新垣结衣")) { // <----------因为第二元素是新垣结衣,所以这里会执行删除
jpanMeiZi.remove("新垣结衣"); // <-----------执行remove方法
}
System.out.println(str);
}
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index); //<-----------真正执行删除的方法
return true;
}
}
return false;
}
private void fastRemove(int index) {
modCount++; //<------------因为是结构上的改变,所以modCount++,此时为4
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // <----- 这里需要关注这个size,删除一个元素之后,它变成了2
}
public boolean hasNext() {
return cursor != size; //<---------这个curore=2 size=2 ,
//由于删除一个元素之后,这个方法返回了false
//所以,后继它不会再去执行并发异常检测
}
//程序结束,打印移除元素之外的集合内容
对于这种情况,当且仅当你要删除的元素在倒数第二个的时候,也就是说你的cursor = size 的时候,并发异常不会抛出。这个是需要注意的地方,具体可以实操更多的例子去作证。
安全失败原理
- 通过迭代器迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发 Concurrent Modification Exception。java.util.concurrent 包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
-
CopyOnWriteArrayList 集合 是遍历时能安全删除元素的代表集合。
在了解这个集合之前,我们有必要先知道这个集合的单词核心意思,CopyOnWrite,它是指写入时复制。
写入时复制(CopyOnWrite,简称COW)思想是计算机程序设计领域中的一种优化策略。其核心思想是,如果有多个调用者(Callers)同时要求相同的资源(如内存或者是磁盘上的数据存储),他们会共同获取相同的指针指向相同的资源,直到某个调用者视图修改资源内容时,系统才会真正复制一份专用副本(private copy)给该调用者,而其他调用者所见到的最初的资源仍然保持不变。这过程对其他的调用者都是透明的(transparently)。此做法主要的优点是如果调用者没有修改资源,就不会有副本(private copy)被创建,因此多个调用者只是读取操作时可以共享同一份资源。
到这里,我们大致可以了解到这个集合的创建意图。当A线程在读取这个集合时,B线程在操作这个集合的结构,由于俩者操纵的不是同一个集合对象,B线程得到的是指向这个集合的一个副本,所以它的修改并不会影响到其他线程去读取原集合本身。
--------------华丽分割线--------------------------------------------
那么我们现在从源码角度来看看,写操作的一个执行的流程:
public boolean add(E e) {
final ReentrantLock lock = this.lock; //可重入锁,写操作需要加锁
lock.lock(); //从这里可以看出,这个线程在添加元素的时候是需要上锁的,目的避免了同时复制了N个副本
try {
Object[] elements = getArray(); //获取原来的集合
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e; //增加一个元素之前我们需要复制源数组 的内容
setArray(newElements); //在这个复制数组的基础上进行修改,然后再写入内存
return true;
} finally {
lock.unlock(); //释放锁
}
}
final Object[] getArray() {
return array;
}
读操作的流程:下面方法依次有递进关系,可以很明显的看到,读是在同一个源数组上,而不是副本,且读操作没有加上锁,这相对于与采取加锁的方式,性能提升不少
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
final Object[] getArray() {
return array;
}
private transient volatile Object[] array;
脑容量有限,本文就到这里,下一节,我们继续看看这个可重入锁底层究竟在这个集合上干了些什么。
网友评论