一、ArrayList集合迭代时修改元素
1.普通ArrayList集合进行操作时:
@Test
public void test(){
ArrayList<Integer> list = new ArrayList<Integer>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
//在添加完元素5后进行第二次迭代会报错,大概意思是现在不能修改 java.util.ConcurrentModificationException
Integer next = iterator.next();//报错行,根据此方法进去查看详情
System.out.println(next);
list.add(5);
}
}
//异常查看解析步骤
//1.这是 ArrayList获取迭代器的方法,显然获取的是内部实现iterator接口的内部类
public Iterator<E> iterator() {
return new Itr();
}
//2.内部类--迭代器
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;//主要是这个属性,迭代时修改异常主要由它控制;
//3.modCount属性不是ArrayList中的变量,其是ArrayList父类java.util.AbstractList<E>中的一个变量主要记录对容器中元素的增加减少所做的操作;
protected transient int modCount = 0;//初始0
//如ArrayList的添加操作
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!自增modCount值
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) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
//4.迭代器在每次获取下一个元素时需要判断当前集合是否发生变化,就是根据modCount为依据
@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];
}
//检验方法
final void checkForComodification() {
//expectedModCount在创建迭代器时记录modCount的值
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
根据以上代码可判断出ArrayList在迭代遍历元素时无法修改容器的根本原因了:
1.ArrayList的父类AbstractList有一个记录容器数据增加、减少变化的操作次数modCount
2.在ArrayList对象获取迭代器时会把modCount值赋给迭代器内部一个属性,用于校验
3.迭代器在遍历属性时判断modCount值是否发生过变化,如发生变化则表示当前容器被修改或我应终止遍历;
4.此时添加一个元素,或移除一个元素,迭代器在下一次获取元素时校验不通过,抛出异常;
下面就来介绍一个解决此问题的容器:CopyOnWriteArrayList
二、CopyOnWriteArrayList
1.测试效果
public static void main(String[] args) {
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
Iterator<Integer> iterator = list.iterator();
while(iterator.hasNext()){
Integer next = iterator.next();
System.out.print(next+",");
list.add(5);
}
System.out.println("第二次循环------------"+list.size());
// 0,1,2,3,4,5,6,7,8,9,第二次循环------------20
iterator = list.iterator();
while(iterator.hasNext()){
Integer next = iterator.next();
System.out.print(next+",");
}
//0,1,2,3,4,5,6,7,8,9,5,5,5,5,5,5,5,5,5,5,
}
程序运行正常
2.解析java.util.concurrent.CopyOnWriteArrayList 实现方式
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
/** The lock protecting all mutators 数据变更时用于上锁*/
final transient ReentrantLock lock = new ReentrantLock();
/** The array, accessed only via getArray/setArray. 封装数据*/
private transient volatile Object[] array;
先查看迭代器遍历效果
//1.获取迭代器
public Iterator<E> iterator() {
//创建一个迭代器对象,getArray()当前数组
return new COWIterator<E>(getArray(), 0);
}
//2.迭代器对象,部分函数
static final class COWIterator<E> implements ListIterator<E> {
//用于迭代遍历的元素集合,主要区别就在于我遍历的是自己内部不可修改的数组,所以当集合中的数组发生变化与我内部需要遍历的数组不一样
private final Object[] snapshot;
/** Index of element to be returned by subsequent call to next. */
//遍历开始坐标
private int cursor;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
snapshot = elements;
}
public boolean hasNext() {
return cursor < snapshot.length;
}
public E next() {
if (! hasNext())
throw new NoSuchElementException();
return (E) snapshot[cursor++];//返回内部数组元素
}
}
结果:由此可以看出CopyOnWriteArrayList 集合为什么迭代遍历元素时可以对容器进行修改
1.获取迭代器时,创建一个内部迭代器并把当前数组,迭代起始位置传入;
2.迭代器遍历元素时操作的是内部赋值过来的数组
3.当集合中添加元素时会把当前数组中的元素复制到一个新的数组中再添加元素,指向新创建的数组,此时与迭代器所指向的数组不是同一个地址;
3.CopyOnWriteArrayList 主要特点-复制并写入
以add新增元素函数为例
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);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
//其他操作如修改,移除元素操作都会上锁,保证自己操作期间其他线程无法访问
4.CopyOnWriteArrayList 同一时间只能一个线程访问示例
由于加锁操作在源码上无法进行操作,而在线程上添加睡眠效果时此线程又没有获取到锁,所以自己做个演示代码
简单CopyOnWirterArrayList封装
package com.cjy.juc;
import java.util.Arrays;
import java.util.concurrent.locks.ReentrantLock;
public class CopyOnWirterArrayListDemo<T> {
/** * 1.一把锁,只能同一个线程在同一时间对容器进行修改,新增,移除操作, * 涉及到的这些操作需要先复制一个数组在添加,最后替换旧的数组 * 2.数组用于数据封装,数据可见性及时刷新到主存中 */
private final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
public CopyOnWirterArrayListDemo(){
setArray(new Object[0]);
}
final void setArray(Object[] o){
array=o;
}
final Object[] getArray(){
return array;
}
public T getIndex(int index){
if(index < 0 || index >= array.length)
throw new IllegalArgumentException("坐标不合法!!!");
return (T) array[index];
}
public boolean add(T t){
final ReentrantLock lock = this.lock;
lock.lock();
try{
Object[] old = getArray();
int length = old.length;
Object[] copyOf = Arrays.copyOf(old, length+1);
copyOf[length]=t;
// array=copyOf;
setArray(copyOf);
return true;
}catch(Exception e){
}finally{
lock.unlock();
}
return false;
}
/** * 根据坐标移除元素,有两种情况待移除元素是末尾;2待移除元素在中间 * @param index * @return */
public T remove(int index){
final ReentrantLock lock = this.lock;
lock.lock();
try {
Thread.sleep(5000);//移除元素睡眠5秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
try{
T t = getIndex(index);
Object[] old = getArray();
int len = old.length;
int delC = len-index-1;
if(delC==0){
setArray(Arrays.copyOf(old, len-1));
}else{
Object[] ele = new Object[len-1];
System.arraycopy(old, 0, ele, 0, index);
System.arraycopy(old, index+1, ele, index, delC);
setArray(ele);
}
return t;
}finally{
lock.unlock();
}
}
public void fori(){
Object[] ar = getArray();
for (int a = 0; a < ar.length; a++) {
System.out.println(ar[a]);
// if(ar.length-1 != a)
// System.out.print(",");
}
}
}
测试代码
package com.cjy.juc;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.locks.ReentrantLock;
import org.junit.Test;
public class CopyOnWirterArrayList {
public static void main(String[] args) {
/* CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>(); for (int i = 0; i < 10; i++) { list.add(i); } Iterator<Integer> iterator = list.iterator(); while(iterator.hasNext()){ Integer next = iterator.next(); System.out.print(next+","); list.add(5); } System.out.println("第二次循环------------"+list.size()); iterator = list.iterator(); while(iterator.hasNext()){ Integer next = iterator.next(); System.out.print(next+","); }*/
CopyOnWirterArrayListDemo<Integer> list = new CopyOnWirterArrayListDemo<Integer>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
CopyOnWirteDemo1 demo1 = new CopyOnWirteDemo1(list);
CopyOnWirteDemo2 demo2 = new CopyOnWirteDemo2(list);
new Thread(demo2).start();//执行移除坐标3的元素时线程阻塞一会
new Thread(demo1).start();//新增元素线程由于发现此时有线程获取锁,进入等待中
/**由于遍历方法没有加锁,所以数据打印没有顺序,但是可以看出元素4被移除了 移除元素4 新增元素true 1 2 1 2 3 5 9 3 5 9 */
}
class CopyOnWirteDemo1 implements Runnable{
CopyOnWirterArrayListDemo<Integer> list=null;
public CopyOnWirteDemo1(CopyOnWirterArrayListDemo<Integer> list){
this.list = list;
}
@Override
public void run() {
System.out.println("新增元素"+list.add(9));
list.fori();
}
}
class CopyOnWirteDemo2 implements Runnable{
CopyOnWirterArrayListDemo<Integer> list=null;
public CopyOnWirteDemo2(CopyOnWirterArrayListDemo<Integer> list){
this.list = list;
}
@Override
public void run() {
System.out.println("移除元素"+list.remove(3));
list.fori();
}
}
注意:添加、删除、修改操作多时,效率低,因为每次添加时都会进行复制,开销非常的大。并发迭代操作多时可以选择。