我想通过多线程模拟竞赛游戏,并且我希望运行者在Referee开枪后开始运行,所以我将wait()放入运行者的run方法中以便等待裁判,这是运行者(PrintRunner.class)run方法

@Override
public void run() {
    sleepTime = random.nextInt(3)+8;
    System.out.println(name+" is ready~");
    try {
        synchronized(this){
            wait();
        }
        System.out.println("Start running~");
        Thread.sleep(sleepTime*1000);
    } catch (InterruptedException e) {
        System.err.println(e.getMessage());
    }
    System.out.println(name +" win the game!!!");
}

这是Referee运行方法:
@Override
public void run() {
    TimeUnit unit = TimeUnit.SECONDS;
    try {
        unit.sleep(3);
        System.out.println(name+" : On your mark, get set~");
        unit.sleep(5);
    } catch (InterruptedException e) {
        System.err.println(e.getMessage());
    }
    System.out.println("Fire a pistol!!!");

    synchronized(PrintRunner.class){
        notifyAll();
    }
}

当裁判通知运行者时,我得到IllegalMonitorStateException,当我使用wait()notifyAll()时,我获得了PrintRunner锁。

请告诉我代码为什么会出错。

最佳答案

您的程序无法正常工作,因为您正在notifyAll()实例上调用Referee,但在与PrintRunner.class同步的块内执行了该操作。您只能在持有锁的对象上调用notify()/notifyAll(),否则会得到IllegalMonitorStateException

但是切换到PrintRunner.class.notifyAll()对您无济于事,因为此调用仅对正在等待PrintRunner.class对象通知的那些线程有效,而您没有此类线程。您的线程正在等待特定的实例,而不是在类本身上。结果,您的Referee需要遍历所有等待的PrintRunner实例,并在每个实例上调用notify():

for(PrintRunner runner: runners) {
    synchronized(runner) {
        runner.notify();
    }
}

所描述的解决方案将起作用,但是其缺点是对所有运行者都不公平。其中一些会比其他人更早得到通知。

重要说明:使用PrintRunner.class.wait()PrintRunner.class.notifyAll()可以使用,但存在相同的不公平性问题,因为每个运行者都必须重新获取单个PrintRunner.class实例的锁才能取得进展。而且他们只能顺序执行。在您的情况下(除了wait()调用,同步块(synchronized block)中什么都没有),启动之间的延迟可以忽略不计,但仍然存在。

幸运的是,Java为您的问题提供了更好的解决方案– CountDownLatch类。特别是,您需要一个值为1的CountDownLatch。所有运行者都必须等待该闩锁,直到裁判将其设置为零为止。此时,它们将立即被释放。请注意,所有对象(裁判和运行者)必须使用相同的共享闩锁。让裁判拥有它(是手枪):

Referee.java
private final CountDownLatch startLatch = new CountDownLatch(1);

public CountDownLatch getStartLatch() {
    return startLatch;
}

@Override
public void run() {
    // prepare to start, make sure all the runners are ready
    // ...

    // This will release all the waiting runners:
    startLatch.countDown();
}

PrintRunner.java
@Override
public void run() {
    try {
        // This call will wait until the referee counts the latch down to zero
        referee.getStartLatch().await();
    } catch (InterruptedException e) {
        // Handle unexpected interruption here
    }
}

为了确保所有流道线程已启动并准备就绪,可以使用另一个闩锁,其初始值等于线程数。每个运行器线程都必须在调用getStartLatch().await()之前立即将该闩锁准备就绪,将其递减计数。裁判员必须先等待该闩锁,然后才能开始倒计时。这将确保您的比赛尽可能公平-所有运行者都有时间为它做准备,并且所有他们都同时被释放。

10-08 19:38