leetcode——Iterator 使用思考(仅单线程安全remove 方法、多线程下对 iterator 遍历加锁)

今天在做剑指 Offer 38. 字符串的排列时,视图利用 map 保存字符与字符个数的映射关系,然后取 map 的 key 集合(map.getSet),对 set 进行遍历时对value 为0的值直接用 map.remove(c)进行删除,结果爆出了 ConcurrentModificationException。

猜想原因应该是:遍历的对象是利用 getSet 方法获取的 Set 类型引用,这些引用指向的是 map 中的地址,而不是又新建了一个 Set 对象,因此问题相当于:遍历 Set 引用的时候,对 map 中的 entry 进行了增删,而 java这样做是不安全的,如“遍历的时候删除了后续某个要被遍历的对象,而引用不会增删,因此在遍历到 Set 中保存的某 Entry 地址的时候,找不到已经删除的 Entry 对象了”。

为什么需要 Iterator

先介绍:Iterator 中的remove(删除上一个元素,对 curSor--,保障(1)如果上个元素是最后的元素,curSor 等于新的 size,hashNext 返回 false;(2)如果上个元素不是最后元素,curSor 仍指向原来的元素,原来元素的下标已经随着前一元素的删除而减一)

后面会讲 AbstractList 对 iterator 的具体实现(保存上一个元素下标,当前元素下标 curSor,提供 remove 方法用于删除上一个遍历到的元素——将当前元素下标减一,因为上一个元素删去了,当前元素在数组中的位置当然要往前移动一个位置,并且保障下标不会超过 size 的大小,比如原数组大小为4,现在要遍历最后一个元素——下标为3的元素,这个时候将前一个元素删去了,新的数组大小为3,如果不对当前下标进行减一的话,就越界了——在 iterator 中,该异常体现在hasNext 方法:因为 hasNext 方法通过curSor==size来判断集合遍历完毕,如果当前 curSor=3,而 size=3,这时会认为数组已经遍历完了——curSor 已经越界,实际上最后一个元素还没遍历到呢!)

再分析:为什么要用 Iterator 呢——提供遍历时的安全删除方法(只对进行 it.remove 操作的当前线程安全)

前面说的 Iterator 的设计好复杂哦(而且只能删掉当前curSor指向的元素的前一个元素),有没有其他的实现方式,保证既能遍历集合,又能在遍历的时候对集合元素进行增删呢?我们想象一个利用 size=list.size(),for(int i=0;i<size;i++)方法进行对 list 的遍历,遍历到某个位置 i时,突然将i+1位置的元素删掉了,这个时候会出现什么情况呢?

(1)访问越界:会发现,我们会循环到这一步:list.get(size-1),而此时集合已经不包含这个位置了,因此我们发现由于 for 循环提前写死了遍历的次数,而无法根据 list 的 size()大小的变化实时改变要遍历的下标范围,倒置下标越界。另外,刚才举得例子中删除的元素是当前遍历位置的后面的元素,那如果删的是前面的元素又会发生什么呢?

(2)遗漏访问部分元素:如果当前遍历到 list.get(i),现在删除了 i-1,那么由于删除元素后面的元素都要向前挪动一位,原本下标为 i+1的元素此时移动到了 i 位置,而由于 for 循环写死了访问的下标,下一个要访问的元素应该是 list.get(i+1)了,也就是说,跳过了原本位于 i+1,而现在位于 i 的元素啦。。。。

因此基本的 for 循环是无法满足我们的遍历时增删的需求的,我们需要某种能够实时调整访问下标的机制,而并没有一种能够完全允许任意增删元素的方法,只有 iterator这种允许删除前一个元素的机制啦。并且 Iterator 应该还会提供其他的性能优化或者安全保障的吧

 

使用Iterator 遍历集合时修改集合报错(单线程)

对Vector、ArrayList在迭代的时候如果同时对其进行修改就会抛出java.util.ConcurrentModificationException异常。

public class Test {
    public static void main(String[] args)  {
        ArrayList<Integer> list = new ArrayList<Integer>();
        list.add(2);
        Iterator<Integer> iterator = list.iterator();
        while(iterator.hasNext()){
            Integer integer = iterator.next();
            if(integer==2)
                list.remove(integer);
        }
    }
}

异常出现的原因

  查看 ArrayList 中 iterator() 方法的具体实现,发现实现于父类AbstractList。

private class Itr implements Iterator<E> {
    int cursor = 0;
    int lastRet = -1;
    int expectedModCount = modCount;
    public boolean hasNext() {
           return cursor != size();
    }
    public E next() {
           checkForComodification();
        try {
        E next = get(cursor);
        lastRet = cursor++;
        return next;
        } catch (IndexOutOfBoundsException e) {
        checkForComodification();
        throw new NoSuchElementException();
        }
    }
    public void remove() {
        if (lastRet == -1)
        throw new IllegalStateException();
           checkForComodification();
 
        try {
        AbstractList.this.remove(lastRet);
        if (lastRet < cursor)
            cursor--;
        lastRet = -1;
        expectedModCount = modCount;
        } catch (IndexOutOfBoundsException e) {
        throw new ConcurrentModificationException();
        }
    }
 
    final void checkForComodification() {
        if (modCount != expectedModCount)
        throw new ConcurrentModificationException();
    }
}

  


多线程使用 Iterator 的 remove 方法安全么?(不安全)

//Iterator 多线程 修改集合
public class Test1 {
    static ArrayList<Integer> list = new ArrayList<Integer>();
    public static void main(String[] args)  {
        list.add(1);
        list.add(2);
        list.add(3);
        list.add(4);
        list.add(5);
        Thread thread1 = new Thread(){
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while(iterator.hasNext()){
                    Integer integer = iterator.next();
                    System.out.println(integer);
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            };
        };
        Thread thread2 = new Thread(){
            public void run() {
                Iterator<Integer> iterator = list.iterator();
                while(iterator.hasNext()){
                    Integer integer = iterator.next();
                    if(integer==2)
                        iterator.remove();
                }
            };
        };
        thread1.start();
        thread2.start();
    }
}

 源码追踪:

//main
Iterator<Integer> iterator = list.iterator();

//ArrayList.java
public Iterator<E> iterator() {
        return new Itr();
    }

//ArrayList.java
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;

        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(); //报错中说到的901行
        }
    }

分析

每个线程拥有自己的 Itr 对象,checkForComodificaion 的目的是,避免除了 Iterator 提供的 remove 方法之外的,其他会导致 modCount++的方法的调用,因为这些方法调用后可能会导致集合访问不安全(越界、遗漏等)。

代码中线程2调用了 Iterator 提供的 remove 方法,会对 curSor 指针进行修改(从而避免了线程2集合访问时的不安全),因此在调用完 ArrayList 的 remove 方法之后,还会将 exceptedModCount 置为  新的modCount,从而避免线程2后续用 next 方法遍历时调用 check 方法而报错(这个时候不会出现不安全问题,无需报错)。

但是线程2中 remove 方法提供的保护只针对线程2的访问(只能修改线程2的 iterator 的curSor), 无法对线程1的 curSor 进行修正,也就是说线程1在后续访问集合时,可能会出现越界、遗漏部分元素(无法正常访问所有元素)的情况,这个时候线程1当然应该报错以提示这个将会发生的安全问题,而checkForComodificaion正是提供这一保障的,modCount 是访问同一集合的线程共享的变量,线程1通过checkForComodificaion判断“有其他的线程对集合进行增删啦(线程1发现modCount 变了,和它自己独有的expectedModCount不相等了),我再继续照着老一套遍历集合就会出现安全问题啦”,于是爆出异常

有可能有朋友说ArrayList是非线程安全的容器,换成Vector就没问题了,实际上换成Vector还是会出现这种错误。

  原因在于,虽然Vector的方法采用了synchronized进行了同步,但是实际上通过Iterator访问的情况下,每个线程里面返回的是不同的iterator,也即是说expectedModCount是每个线程私有。假若此时有2个线程,线程1在进行遍历,线程2在进行修改,那么很有可能导致线程2修改后导致Vector中的modCount自增了,线程2的expectedModCount也自增了,但是线程1的expectedModCount没有自增,此时线程1遍历时就会出现expectedModCount不等于modCount的情况了。

  因此一般有2种解决办法:

  1)在使用iterator迭代的时候使用synchronized或者Lock进行同步;(就是说在线程2迭代、迭代时修改集合时,线程1无法对集合进行迭代访问,当然就不存在迭代访问时的越界、遗漏等不安全问题啦)

  2)使用并发容器CopyOnWriteArrayList代替ArrayList和Vector,实际上这也不行👋🏻。(读写分离,进行写操作时,是对副本进行修改的,读的对象是原始的对象,在读的过程中,不会出现下标越界等问题)

补充:为什么 Vector 线程安全,如果没有synchronized关键字,会发生什么?

当synchronized对方法加锁时,不同线程不能对同一对象同时进入方法(同时进入,指的是一个线程1还没执行完这个方法时,cpu 将运行时间片分给了另一个线程2,这个时候如果线程2也运行到要进入该方法了,如果线程2也能进入该方法,就指两个线程同时进入该方法;如果该方法加了synchronized关键字,线程2就会挂起,把时间片让出去;如果是加了 CAS 锁(为什么 CAS 叫乐观锁呢(待补充)——系统设计者小明如果认为该系统发生锁竞争的可能性较小,由于synchronized导致的线程挂起会引起上下文切换的消耗,他认为)的话,就会时间片空转,而不会调用内核的线程挂起操作,就不会引起用户态内核态的上下文切换。),但是可以对不同对象同时进入该方法。

Vector的 removeElementAt 

如果没有synchronized关键字的话

线程1进入了该方法,它想删掉 v1 末尾元素,它先判断 index 是否>=elemenCount,如果不是的话就认为该元素存在,可以放心的继续操作啦。

突然,这个时候线程1的时间片没有啦,轮到线程2了,因为没有synchronized关键字修饰,线程2也能进入该方法,它也要删掉v1末尾元素,他判断可以继续操作,然后将最后的元素给删去了,并将 elementCount--。

突然,时间片又轮给线程1了,线程1因为已经执行完 if(index>=elementCount)这一步骤了,不会再重新判断,虽然当前elementCount已经发生了改变(elementCount==elementCount了),但是线程1不知道呀,所以它还是继续了后面的删除操作,它最终进行到了 elementData[elementCount]这一步。。。这时发生了数组越界异常。。。。

public synchronized void removeElementAt(int index) {
        modCount++;
        if (index >= elementCount) {
            throw new ArrayIndexOutOfBoundsException(index + " >= " +
                                                     elementCount);
        }
        else if (index < 0) {
            throw new ArrayIndexOutOfBoundsException(index);
        }
        int j = elementCount - index - 1;
        if (j > 0) {
            System.arraycopy(elementData, index + 1, elementData, index, j);
        }
        elementCount--;
        elementData[elementCount] = null; /* to let gc do its work */
    }

那么下面的 Vector 的 get 方法,为啥也要加锁呢。。。(synchronized对方法加锁时,不同线程不能对同一对象调用该get方法。。。有教程说是为了保障 get 方法能尽量读到最新更新的值???不懂不懂)

public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);

        return elementData(index);
    }

 


在Java中如何遍历Map对象

How to Iterate Over a Map in Java

在java中遍历Map有不少的方法。我们看一下最常用的方法及其优缺点。

既然java中的所有map都实现了Map接口,以下方法适用于任何map实现(HashMap, TreeMap, LinkedHashMap, Hashtable, 等等)

方法一 在for-each循环中使用entries来遍历

这是最常见的并且在大多数情况下也是最可取的遍历方式。在键值都需要时使用。

Map<Integer, Integer> map = new HashMap<Integer, Integer>();
 
for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
 
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
 
}

注意:for-each循环在java 5中被引入所以该方法只能应用于java 5或更高的版本中。如果你遍历的是一个空的map对象,for-each循环将抛出NullPointerException,因此在遍历前你总是应该检查空引用

方法二 在for-each循环中遍历keys或values。

如果只需要map中的键或者值,你可以通过keySet或values来实现遍历,而不是用entrySet。

Map<Integer, Integer> map = new HashMap<Integer, Integer>();
 
//遍历map中的键
 
for (Integer key : map.keySet()) {
 
    System.out.println("Key = " + key);
 
}
 
//遍历map中的值
 
for (Integer value : map.values()) {
 
    System.out.println("Value = " + value);
 
}

该方法比entrySet遍历在性能上稍好(快了10%),而且代码更加干净。

方法三使用Iterator遍历

使用泛型:

Map<Integer, Integer> map = new HashMap<Integer, Integer>();
 
Iterator<Map.Entry<Integer, Integer>> entries = map.entrySet().iterator();
 
while (entries.hasNext()) {
 
    Map.Entry<Integer, Integer> entry = entries.next();
 
    System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());
 
}

不使用泛型:

Map map = new HashMap();
 
Iterator entries = map.entrySet().iterator();
 
while (entries.hasNext()) {
 
    Map.Entry entry = (Map.Entry) entries.next();
 
    Integer key = (Integer)entry.getKey();
 
    Integer value = (Integer)entry.getValue();
 
    System.out.println("Key = " + key + ", Value = " + value);
 
}

你也可以在keySet和values上应用同样的方法。

该种方式看起来冗余却有其优点所在。首先,在老版本java中这是惟一遍历map的方式。另一个好处是,你可以在遍历时调用iterator.remove()来删除entries,另两个方法则不能。根据javadoc的说明,如果在for-each遍历中尝试使用此方法,结果是不可预测的。

从性能方面看,该方法类同于for-each遍历(即方法二)的性能。

方法四、通过键找值遍历(效率低)

Map<Integer, Integer> map = new HashMap<Integer, Integer>();
 
for (Integer key : map.keySet()) {
 
    Integer value = map.get(key);
 
    System.out.println("Key = " + key + ", Value = " + value);
 
}

作为方法一的替代,这个代码看上去更加干净;但实际上它相当慢且无效率。因为从键取值是耗时的操作(与方法一相比,在不同的Map实现中该方法慢了20%~200%)。如果你安装了FindBugs,它会做出检查并警告你关于哪些是低效率的遍历。所以尽量避免使用。

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值