• java 多线程操作List,已经做了同步synchronized,还会有ConcurrentModificationException,知道为什么吗?


    如题,最近项目里有个模块我做了异步处理方面的事情,在code过程中发现一个颠覆我对synchronized这个关键字和用法的地方,请问各位java开发者们是否对此有一个合理的解释,不多说,我直接贴出问题代码:

    (事实证明这是一个坑,各位读者,如果有兴趣,可以先不看答案,自己看看能不能发现这个坑)

    复制代码
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.concurrent.CopyOnWriteArrayList;
    
    public class ConcurrentList {
        //private static List<String> TEST_LIST = new CopyOnWriteArrayList<String>();
        private static List<String> TEST_LIST = Collections.synchronizedList(new ArrayList<String>());
    
        public static void main(String[] args) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (TEST_LIST) {
                            TEST_LIST.add("11");
                        }
                        System.out.println("Thread1 running");
                    }
                }
            }).start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        synchronized (TEST_LIST) {
                            for (String at : TEST_LIST) {
                                TEST_LIST.add("22");
                            }
                        }
                        System.out.println("Thread2 running");
                    }
                }
            }).start();
        }
    }
    复制代码

    输出结果是:

    复制代码
    Thread1 running
    Exception in thread "Thread-1" java.util.ConcurrentModificationException
        at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
        at java.util.AbstractList$Itr.next(AbstractList.java:343)
        at com.free4lab.lol.ConcurrentList$2.run(ConcurrentList.java:40)
        at java.lang.Thread.run(Thread.java:619)
    Thread1 running
    Thread1 running
    Thread1 running
    Thread1 running
    Thread1 running
    Thread1 running
    Thread1 running
    Thread1 running
    复制代码

    -----------------------------------分隔线,以下是解释--------------------------------

    问题明了了:

    以上问题不是并发的问题,是ArrayList的问题,是个坑!且看如下代码,以及运行结果:

    复制代码
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.concurrent.CopyOnWriteArrayList;
    
    public class ConcurrentList {
        //private static List<String> TEST_LIST = new CopyOnWriteArrayList<String>();
        private static List<String> TEST_LIST = Collections.synchronizedList(new ArrayList<String>());
    
        public static void main(String[] args) {
            TEST_LIST.add("111");
            TEST_LIST.add("222");
            for (String at : TEST_LIST) {
                System.out.println(at);
                TEST_LIST.add("333");
                System.out.println("add over");
            }
        }
    }
    复制代码

    结果是:

    111
    add over
    Exception in thread "main" java.util.ConcurrentModificationException
        at java.util.AbstractList$Itr.checkForComodification(AbstractList.java:372)
        at java.util.AbstractList$Itr.next(AbstractList.java:343)
        at com.free4lab.lol.ConcurrentList.main(ConcurrentList.java:15)

    分析:我们发现迭代了一次之后就抛出所谓的并发修改异常,不过这里没有多线程,看下源代码就知道了

    list.add的时候执行了,修改了modCount,循环外面一次add到第一次迭代不会有问题,因为初始化的时候在AbstractList中int expectedModCount = modCount;,

    复制代码
    /**
         * Appends the specified element to the end of this list.
         *
         * @param e element to be appended to this list
         * @return <tt>true</tt> (as specified by {@link Collection#add})
         */
        public boolean add(E e) {
        ensureCapacity(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
        }
    
    public void ensureCapacity(int minCapacity) {
        modCount++;
        int oldCapacity = elementData.length;
        if (minCapacity > oldCapacity) {
            Object oldData[] = elementData;
            int newCapacity = (oldCapacity * 3)/2 + 1;
                if (newCapacity < minCapacity)
            newCapacity = minCapacity;
                // minCapacity is usually close to size, so this is a win:
                elementData = Arrays.copyOf(elementData, newCapacity);
        }
        }

    复制代码
    复制代码
    public E next() {
                checkForComodification();
            try {
            E next = get(cursor);
            lastRet = cursor++;
            return next;
            } catch (IndexOutOfBoundsException e) {
            checkForComodification();
            throw new NoSuchElementException();
            }
        }
    复制代码

    这样迭代器next()第一次 checkForComodification() 是不会抛出异常的,第二次才会抛出异常,因为在checkForComodification()里检查了

    final void checkForComodification() {
            if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        }
    }

    这样,在循环迭代中,进行了一次add操作,修改了modcount变量,再次迭代的时候,异常就throw出来了!

    如果非要进行这样的操作,那么声明list为CopyOnWriteArrayList,就ok!因为用了copyonwrite技术

    复制代码
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.List;
    import java.util.concurrent.CopyOnWriteArrayList;
    
    public class ConcurrentList {
        private static List<String> TEST_LIST = new CopyOnWriteArrayList<String>();
        //private static List<String> TEST_LIST = Collections.synchronizedList(new ArrayList<String>());
    
        public static void main(String[] args) {
            TEST_LIST.add("111");
            TEST_LIST.add("222");
            for (String at : TEST_LIST) {
                System.out.println(at);
                TEST_LIST.add("333");
                System.out.println("add over");
            }
        }
    }
    复制代码

    输出是正确的:

    111
    add over
    222
    add over

    额外再说一点,也可以用iterator迭代,不过同样也无法调用next()方法(我注释掉了),这样程序就是死循环了,不断的加,不断的迭代。所以我感觉如果需要在迭代中增加元素,真正有用的还是CopyOnWriteArrayList,不过实际中,如果CopyOnWriteArrayList代价太高,可能我们可以申请一个临时list存放,在迭代后合并到主list中!

    复制代码
    import java.util.ArrayList;
    import java.util.Collections;
    import java.util.Iterator;
    import java.util.List;
    import java.util.concurrent.CopyOnWriteArrayList;
    
    public class ConcurrentList {
        //private static List<String> TEST_LIST = new CopyOnWriteArrayList<String>();
        private static List<String> TEST_LIST = Collections.synchronizedList(new ArrayList<String>());
    
        public static void main(String[] args) {
            TEST_LIST.add("111");
            TEST_LIST.add("222");
            Iterator iterator  = TEST_LIST.iterator();  
            while(iterator.hasNext()){
                //System.out.println(iterator.next());
                TEST_LIST.add("333");
                System.out.println("add over");
            }  
        }
    }
    复制代码

    萌萌的IT人,IT人的乐园

    E-mail: huahuiyang@gmail.com https://cn.linkedin.com/pub/huahui-yang/91/13a/105
     
     
  • 相关阅读:
    TransactionScope和Enterprise Libray 3.0 Data Access Application Block
    C#之父 Anders Hejlsberg
    openSUSE Linux 10.2 多语言版
    承蒙各位朋友支持与厚爱,荣获asp.net MVP荣誉
    如何在ASP.NET 2.0中定制Expression Builders
    检查Python对象
    IronPython中使用Cecil类库指南
    MSDN Magazine 4月份asp.net文章
    Reflector 插件
    WSS v3的Form身份认证
  • 原文地址:https://www.cnblogs.com/xiaorenwu702/p/4916747.html
Copyright © 2020-2023  润新知