关于CopyOnWriteArrayList集合的删除元素操作

  在做坦克大战游戏过程中用到了CopyOnWriteArrayList,关于对它的元素进行遍历删除的操作困扰了我很久,先介绍一下它吧。

CopyOnWrite容器

  CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。

CopyOnWriteArrayList

  CopyOnWriteArrayList读取时不加锁,只是写入、删除、修改时加锁,所以一个线程X读取的时候另一个线程Y可能执行remove操作。remove操作首先要获取独占锁,然后进行写时复制操作,就是复制一份当前的array数组,然后在复制的新数组里面删除线程X通过get访问的元素,比如:删除完成后让array指向这个新的数组。
  在线程x执行get操作的时候并不是直接通过全局array访问数组元素而是通过方法的形参a访问的,a指向的地址和array指向的地址在调用get方法的那一刻是一样的,都指向了堆内存的数组对象。之后改变array指向的地址并不影响get的访问,因为在调用get方法的那一刻形参a指向的内存地址就已经确定了,不会改变。所以读的仍然是旧数组。

CopyOnWrite的缺点

  CopyOnWrite容器有很多优点,但是同时也存在两个问题,即内存占用问题和数据一致性问题。所以在开发的时候需要注意一下。

  内存占用问题。因为CopyOnWrite的写时复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存,旧的对象和新写入的对象(注意:在复制的时候只是复制容器里的引用,只是在写的时候会创建新对象添加到新容器里,而旧容器的对象还在使用,所以有两份对象内存)。如果这些对象占用的内存比较大,比如说200M左右,那么再写入100M数据进去,内存就会占用300M,那么这个时候很有可能造成频繁的Yong GC和Full GC。之前我们系统中使用了一个服务由于每晚使用CopyOnWrite机制更新大对象,造成了每晚15秒的Full GC,应用响应时间也随之变长。

  针对内存占用问题,可以通过压缩容器中的元素的方法来减少大对象的内存消耗,比如,如果元素全是10进制的数字,可以考虑把它压缩成36进制或64进制。或者不使用CopyOnWrite容器,而使用其他的并发容器,如ConcurrentHashMap。

  数据一致性问题。CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。【当执行add或remove操作没完成时,get获取的仍然是旧数组的元素】

转自:https://blog.csdn.net/u010002184/article/details/90452918

正确操作方法

1. foreach循环遍历删除

//己方子弹库
CopyOnWriteArrayList<Shot> myTankShots = myTank.getShots();
for (Shot shot : myTankShots) {
     if (shot.isLive()) {
          g.setColor(Color.cyan);
          g.fillOval(shot.getX(), shot.getY(), 5, 5);
     } else {
          myTankShots.remove(shot);
     }
}

**解释:**由于迭代时是对原集合的拷贝进行遍历,所以在遍历过程中对原集合所作的修改并不能被迭代器检测到,所以不会触发ConcurrentModificationException异常。但同样地,迭代器并不能访问到修改后的内容,即:迭代器遍历的是开始遍历那一刻拿到的集合拷贝,在遍历期间原集合发生的修改迭代器是不知道的。

java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。

2. 普通for循环遍历删除

for (int i = 0; i < myTankShots.size(); i++) {
    Shot shot = myTankShots.get(i);
    if (shot.isLive()) {
        g.setColor(Color.cyan);
        g.fillOval(shot.getX(), shot.getY(), 5, 5);
    } else {
        myTankShots.remove(i);
        //对索引进行递减操作 i--,以确保在下一次循环时能正确访问到下一个元素
        i--; 
    }
}

总结

其实集合更推荐用迭代器来遍历进行删除操作

Iterator<Shot> iterator = myTankShots.iterator();
while (iterator.hasNext()) {
    Shot shot = iterator.next();
    if (shot.isLive()) {
        g.setColor(Color.cyan);
        g.fillOval(shot.getX(), shot.getY(), 5, 5);
    } else {
        iterator.remove();
    }
}

但由于CopyOnWriteArrayList的迭代器不支持add、set、remove操作,会抛出UnsupportedOperationException异常。

你可能感兴趣的:(编程错误,后端,java)