【问题标题】:ConcurrentModificationException in Multiple Threading access多线程访问中的 ConcurrentModificationException
【发布时间】:2017-02-02 10:29:58
【问题描述】:

刚开始学习多线程,陷入了并发修改的境地。

这是我的 Java 类

package ashish.demo.threading.basic;

import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.List;

/**
 * Created by ashishratan on 2/2/17.
 */
public class ItemTask implements Runnable {

    private volatile boolean shutdown;
    private List<Item> itemList = new ArrayList<Item>();
    private volatile Item item;
    private volatile boolean addItemEvent;
    private volatile boolean removeItemEvent;

    @Override
    public void run() {
        while (!this.shutdown) {
            try {
                synchronized (this) {
                    if (this.item != null) {
                        this.item.setProductName("Created By:: " + Thread.currentThread().getName());
                    }
                    if (this.addItemEvent) {
                        this.itemList.add(this.item);
                        this.item=null;
                        this.addItemEvent = false;
                        this.statusDisplay();

                    }
                    if (this.removeItemEvent) {
                        this.itemList.add(this.item);
                        this.removeItemEvent = false;
                        this.statusDisplay();
                    }
                }
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("Shutting down...");
    }


    public void addItem(Item item) {
        this.item = item;
        this.addItemEvent = true;
    }

    public synchronized List<Item> getItemList() {
        this.statusDisplay();
        return itemList;
    }

    public void setItemList(List<Item> itemList) {
        this.itemList = itemList;
    }

    public synchronized void shutdownHook() {
        this.statusDisplay();
        this.shutdown = true;
        System.out.println(this.getItemList());
    }

    private synchronized void statusDisplay() {

        System.out.println(Thread.currentThread());
        System.out.println("Total Items In Stock are " + this.itemList.size());
    }
}

跑步者类

    package ashish.demo.threading;

    import ashish.demo.threading.basic.Item;
    import ashish.demo.threading.basic.ItemTask;

    import java.util.Scanner;

    public class Main {

        public static void main(String[] args) {

            ItemTask itemTask = new ItemTask();
            Thread thread =null;

            for (int i = 0; i < 500; i++) {
                thread=new Thread(itemTask);
                thread.setName("ItemTask-Thread-"+(i+1));
                thread.setPriority(Thread.MAX_PRIORITY);
                thread.start();
            }
            System.out.println("Please Enter Number (0) to exit");
            Scanner scanner = new Scanner(System.in);
            int i = scanner.nextInt();
            while (i>0){
                itemTask.addItem(new Item(1,12.0f,"Product "+i,(byte)12));
                System.out.println(itemTask.getItemList()); // Line #26, Exception
                System.out.println("Please Enter Number (0) to exit");
                i = scanner.nextInt();
            }
            System.out.println("EXIT");
            itemTask.shutdownHook();
        }
    }


    package ashish.demo.threading.basic;

    import java.io.Serializable;

    /**
     * Created by ashishratan on 2/2/17.
     */
    public class Item implements Serializable {

        private Integer orderId;
        private Float price;
        private String productName;
        private byte category;

        public Item(Integer orderId, Float price, String productName, byte category) {
            this.orderId = orderId;
            this.price = price;
            this.productName = productName;
            this.category = category;
        }

        public Item() {
        }

        public Integer getOrderId() {
            return orderId;
        }

        public void setOrderId(Integer orderId) {
            this.orderId = orderId;
        }

        public Float getPrice() {
            return price;
        }

        public void setPrice(Float price) {
            this.price = price;
        }

        public String getProductName() {
            return productName;
        }

        public void setProductName(String productName) {
            this.productName = productName;
        }

        public byte getCategory() {
            return category;
        }

        public void setCategory(byte category) {
            this.category = category;
        }

        @Override
        public String toString() {
            return "Item{" +
                    "orderId=" + orderId +
                    ", price=" + price +
                    ", productName='" + productName + '\'' +
                    ", category=" + category +
                    '}';
        }


        @Override
        public boolean equals(Object o) {
            if (this == o) return true;
            if (!(o instanceof Item)) return false;

            Item item = (Item) o;

            if (getCategory() != item.getCategory()) return false;
            if (getOrderId() != null ? !getOrderId().equals(item.getOrderId()) : item.getOrderId() != null) return false;
            if (getPrice() != null ? !getPrice().equals(item.getPrice()) : item.getPrice() != null) return false;
            return getProductName() != null ? getProductName().equals(item.getProductName()) : item.getProductName() == null;
        }

        @Override
        public int hashCode() {
            int result = getOrderId() != null ? getOrderId().hashCode() : 0;
            result = 31 * result + (getPrice() != null ? getPrice().hashCode() : 0);
            result = 31 * result + (getProductName() != null ? getProductName().hashCode() : 0);
            result = 31 * result + (int) getCategory();
            return result;
        }
    }

异常跟踪

    Please Enter Number (0) to exit
    3
    Thread[main,5,main]
    Total Items In Stock are 0
    []
    Please Enter Number (0) to exit
    Thread[ItemTask-Thread-455,10,main]
    Total Items In Stock are 1
    6
    Thread[main,5,main]
    Total Items In Stock are 1
    Thread[ItemTask-Thread-464,10,main]
    Total Items In Stock are 2
    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 java.util.AbstractCollection.toString(AbstractCollection.java:461)
        at java.lang.String.valueOf(String.java:2994)
        at java.io.PrintStream.println(PrintStream.java:821)
        at ashish.demo.threading.Main.main(Main.java:26)
    12

【问题讨论】:

  • "if (this.remove ItemEvent) { this.itemList.add (this.item);" ?
  • 那么同步在做什么??
  • Main.java的第26行是哪一个?
  • 在方法上使用synchronized(this)synchronized关键字使用相同的同步对象。
  • 另外,您正在泄漏关键资源。同步仅在返回对列表的引用时发生。通过该引用进行的访问将不再同步。您可以通过返回列表副本或扩展 API 来避免这种情况,这样就不必放弃对列表的直接访问权限。

标签: java multithreading collections


【解决方案1】:

Java Concurrency In Practice 中的建议是:“小心隐式迭代”。你有隐式迭代就行了:

System.out.println(itemTask.getItemList());

因为需要迭代这个列表才能将其转换为字符串。

itemTask.getItemList() 已同步的事实无关紧要 - 监视器仅在调用 itemTask.getItemList() 期间保持:一旦返回结果,监视器不再保持,这意味着监视器当您将该结果传递给 System.out.println 时,不会保留。

为了确保您在打印时拥有对项目列表的独占访问权限,请在itemTask 上显式同步:

synchronized (itemTask) {
  System.out.println(itemTask.getItemList());
}

这将在System.out.println 调用期间正确地保持监视器; itemTask 中的修改不能同时发生,因为这些修改发生在 synchronized (this) 块中,其中“this”是您在外部同步的 itemTask

对此的替代方法是从getItemList 方法返回列表的防御性副本:

public synchronized List<Item> getItemList() {
    this.statusDisplay();
    return new ArrayList<>(itemList);
}

这将消除对getItemList 的调用进行外部同步的需要,这是一种更安全的设计,因为您不依赖班级的客户来“做正确的事”。

【讨论】:

  • 您是否同意拥有一个方法getItemList 并不是一个好主意?这是对此类错误的邀请。 (假设该方法不返回副本)。
  • @Fildor 是的,我愿意。
  • 使用 ItemTask 对象同步不是性能问题
  • @AshishRatan 可能是;但是如果代码首先不正确,则不必担心代码是否具有性能。
【解决方案2】:

您正在打印项目列表,其中涉及使用迭代器迭代列表,同时在 ItemTask 线程中发生结构修改(添加项目)。迭代器认为这很糟糕,因此异常停止了世界。这被称为快速失败行为。您将不得不使用同步列表(这很糟糕)或制作每个想要修改或读取的线程都应该获取的锁/互斥锁。

顺便说一句,由于您使用了多达 500 个线程来更新单个对象的状态,因此发生了一些极端的争用。

【讨论】:

  • 但是我只在特定条件下更新我的列表,而且我也让它变得不稳定。当布尔值为真时,否则不加。
  • @AshishRatan 但是没有什么可以保证这两个操作不会同时发生。实际上,由于您进行的争吵,即使尝试几次也很有可能发生这种情况。
  • 好的,那么什么条件才是安全的呢?除了 getItemList() 方法之外,还有什么其他因素会导致该问题?
  • @AshishRatan 1. 如上所述,对该列表的访问应同步或受锁保护。这里只有两个线程正在访问列表。一个线程更新 ItemTask,另一个接收并执行用户命令,因此不需要 ReadWriteLock。每当主线程想要打印列表时,它都应该获取锁。每当 ItemTask 线程想要修改时,它也应该获取该锁。 2. 不,因为您的代码中没有任何其他迭代。
  • 从案例中我可以理解的是:一个线程更新列表,然后进入睡眠状态,但是在我的主要方法中,我正在访问此列表的地方正在立即运行并尝试访问资源.这可能是这个例外背后的原因吗?我很困惑,为什么会发生?我对引用进行了更改,这些更改保存在对象上,当线程唤醒时,应该需要一些时间才能正确应用于引用。
【解决方案3】:

如果输入打印缓慢,程序不会持续抛出并发修改异常。但如果输入速度非常快,它就会经常抛出它。

据我了解,根本原因: 虽然 ItemTask 中的 getItemList 方法是同步的,但它只是将集合返回到 Main 类。集合的迭代是在不同步的 Main 方法中完成的,同时如果其他线程将任何元素放入集合中,迭代器将验证是否对集合进行了任何修改,如果有任何修改,它会抛出异常 [从堆栈跟踪中可以看出java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)].

如果你在getItemList方法中将itemList的打印移动到Itemtask或者在Itemtask中新的同步方法来打印列表,至少不会发生错误,至少对我来说没有发生。

【讨论】:

  • java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)]。是您答案的摘要。现在该部分用于打印列表在 itemTask 内,我只是想了解 java 中同步块工作的机制。要获得好的答案,您可以使用@Andy 的答案
猜你喜欢
  • 2017-02-10
  • 2020-02-21
  • 1970-01-01
  • 2012-10-30
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-04-03
相关资源
最近更新 更多