You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Java多线程超市结账系统删除客户时持续抛出异常求助

解决Java线程版超市结账系统的异常问题

嘿,看来你在实现Java线程版超市结账系统时遇到了异常问题——大概率是并发修改非线程安全集合或者遍历集合时直接移除元素搞的鬼!我帮你拆解问题,再给一套可行的解决方案。

问题根源分析

你遇到的异常(大概率是ConcurrentModificationException)主要来自这几个点:

  • 你用的ArrayList不是线程安全集合,多个结账台线程同时读写(减等待时间、移除客户)会触发并发修改异常
  • 遍历客户队列时直接调用remove()方法,哪怕是单线程场景也容易触发这个异常,更别说多线程了
  • 分配客户到最短等待结账台时,没有处理竞态条件:多个客户同时分配时,可能会同时计算出同一个“最短等待”结账台,导致等待时间计算不准确

修复后的完整代码实现

1. 客户类(Client)

给客户的等待时间操作加同步,避免多线程同时修改:

public class Client {
    private final int id;
    private int waitTime;

    public Client(int id, int waitTime) {
        this.id = id;
        this.waitTime = waitTime;
    }

    public int getId() {
        return id;
    }

    // 同步获取等待时间
    public synchronized int getWaitTime() {
        return waitTime;
    }

    // 同步递减等待时间
    public synchronized void decreaseWaitTime() {
        if (waitTime > 0) {
            waitTime--;
        }
    }

    // 同步判断是否完成结账
    public synchronized boolean isDone() {
        return waitTime == 0;
    }
}

2. 结账台类(Checkout)

换成线程安全的队列,用迭代器安全移除客户,同时处理线程中断:

import java.util.concurrent.ConcurrentLinkedQueue;

public class Checkout extends Thread {
    private final int checkoutId;
    // 用线程安全的ConcurrentLinkedQueue替代ArrayList
    private final ConcurrentLinkedQueue<Client> clients = new ConcurrentLinkedQueue<>();

    public Checkout(int checkoutId) {
        this.checkoutId = checkoutId;
    }

    public int getCheckoutId() {
        return checkoutId;
    }

    // 计算当前结账台的总等待时间
    public int getTotalWaitTime() {
        int total = 0;
        for (Client client : clients) {
            total += client.getWaitTime();
        }
        return total;
    }

    // 添加客户到结账台
    public void addClient(Client client) {
        clients.add(client);
        System.out.printf("客户%d被分配到结账台%d%n", client.getId(), checkoutId);
    }

    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                // 每秒执行一次等待时间递减
                Thread.sleep(1000);

                // 用迭代器遍历,安全移除完成结账的客户
                var iterator = clients.iterator();
                while (iterator.hasNext()) {
                    Client client = iterator.next();
                    client.decreaseWaitTime();
                    if (client.isDone()) {
                        iterator.remove(); // 迭代器的remove方法不会触发并发修改异常
                        System.out.printf("客户%d完成结账,离开结账台%d%n", client.getId(), checkoutId);
                    }
                }
            } catch (InterruptedException e) {
                // 处理线程中断,优雅停止服务
                Thread.currentThread().interrupt();
                System.out.printf("结账台%d停止服务%n", checkoutId);
            }
        }
    }
}

3. 超市系统主类(Supermarket)

加锁处理客户分配的竞态条件,确保每次都能准确找到最短等待的结账台:

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

public class Supermarket {
    private final List<Checkout> checkouts = new ArrayList<>();
    // 用于同步客户分配操作的锁对象
    private final Object assignLock = new Object();

    public Supermarket(int checkoutCount) {
        // 初始化指定数量的结账台并启动线程
        for (int i = 1; i <= checkoutCount; i++) {
            Checkout checkout = new Checkout(i);
            checkouts.add(checkout);
            checkout.start();
        }
    }

    // 分配客户到等待时间最短的结账台
    public void assignClient(Client client) {
        synchronized (assignLock) {
            Checkout shortestCheckout = null;
            int minTotalWait = Integer.MAX_VALUE;

            for (Checkout checkout : checkouts) {
                int currentTotal = checkout.getTotalWaitTime();
                if (currentTotal < minTotalWait) {
                    minTotalWait = currentTotal;
                    shortestCheckout = checkout;
                }
            }

            if (shortestCheckout != null) {
                shortestCheckout.addClient(client);
            }
        }
    }

    // 停止所有结账台服务
    public void shutdown() {
        for (Checkout checkout : checkouts) {
            checkout.interrupt();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 初始化3个结账台
        Supermarket supermarket = new Supermarket(3);

        // 模拟10个客户陆续到来
        for (int i = 1; i <= 10; i++) {
            // 随机生成5-14秒的等待时间
            int waitTime = (int) (Math.random() * 10) + 5;
            supermarket.assignClient(new Client(i, waitTime));
            Thread.sleep(500); // 每0.5秒来一个客户
        }

        // 运行20秒后停止所有服务
        Thread.sleep(20000);
        supermarket.shutdown();
    }
}

关键修复点说明

  • 线程安全集合:用ConcurrentLinkedQueue替代ArrayList,它天生支持并发读写,避免了ConcurrentModificationException
  • 同步操作:Client类的等待时间相关方法加了synchronized,防止多个线程同时修改同一个客户的等待时间;客户分配逻辑加了锁,避免多个客户同时分配时的竞态条件
  • 安全移除元素:使用迭代器的remove()方法,而不是直接调用集合的remove(),这是遍历集合时移除元素的标准安全做法
  • 优雅线程停止:处理InterruptedException,确保结账台线程可以被安全中断,而不是强制终止

内容的提问来源于stack exchange,提问作者Siespi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 09:22:24