首页 > 解决方案 > 仅使用 wait() 和 notify() 在两个线程之间交替

问题描述

我正在做一个关于java线程的练习,我需要做以下事情:假设你有两个线程,比如说player1和player2,它们都共享一个Ball对象。目标是让他们中的一个人拿到球 1 秒钟,然后放下球,让第二个球员做同样的事情,几次。更具体地说,我有两个这样的类(Player2 是相同的,除了名称):

class Player1 extends Thread{

    private Ball ball;

    public Player1 (Ball ball) {
        this.ball=ball;
        this.setName("Player 1");
    }

    @Override
    public void run() {
        for (int i=0;i<25;i++){
            try{
                ball.takeBall(); //Player takes the ball
                sleep(1000);
                ball.dropBall(); //Player drops the ball
            }catch (Exception e){

            }
        }
    }
}

目标是编写方法takeBall(),并dropBall()以使两个线程交替的方式。

这是我的尝试。


public class Ball{

    boolean ballInUse = false;

    public synchronized void takeBall() throws InterruptedException {
        System.out.println("Ball taken by -> "+Thread.currentThread().getName());

        while (ballInUse){
           wait();
       }

       ballInUse = true;
    }


    public synchronized void dropBall() throws InterruptedException {
        System.out.println("Ball dropped by -> "+Thread.currentThread().getName());
        ballInUse = false;
        notify();
    }

}

//Edited to show the main class

class MyMainClass
    public static void main(String args[]) {

        Ball ball = new Ball();

        Player1 player1 = new Player1(ball);
        Player2 player2 = new Player2(ball);

        player1.start();
        player2.start();
        
    }// main

}

在我的脑海中有点道理,因为变量ballInUse只有在球被另一个线程丢下后才变为假,所以两个线程应该在这两种方法之间交替。显然这个想法有问题,因为这就是我得到的:

Ball taken by -> Player 1
Ball taken by -> Player 2
Ball dropped by -> Player 1
Ball taken by -> Player 1
Ball dropped by -> Player 1
Ball taken by -> Player 1
Ball dropped by -> Player 1
Ball taken by -> Player 1

似乎第二个线程永远不会从第二种方法得到通知。我肯定在这里错过了重要的一点,所以任何提示都将不胜感激。

标签: javamultithreadingwait

解决方案


是的。但是,“唤醒线程”并不意味着“它会向前飞并立即开始运行这一时刻,而您的线程必须执行极其缓慢的动作……循环一个 for 循环”。

它可以(这是线程的问题之一:事情是任意的,任意是不好的,因为它是不可测试的。除了不使用需要同步的线程之外,没有解决办法,例如通过事务和一个数据库)......但它通常不会。

你的 dropBall 运行到它的自然结束,然后它释放锁(因为 dropBall 方法是同步的)。只有现在,其他线程的 takeBall 才能从它的等待中继续前进(等待不只是等待通知 - 它还释放锁并且不能继续,直到它都被通知并且它可以重新获取锁,这最初是它根据定义不能,因为你不能在不持有锁的情况下通知)。

实际发生的是这样的:

  • Player2 的线程正在注意球的锁定是否可用;目前不是。
  • Player1 的线程释放锁,跳回循环 25 次的 for 循环,执行 takeBall 方法并重新获取锁。
  • 过了一会儿,Player2抬起头说:哦,等等,什么?哦,疯了!我错过了!

不能保证锁是“公平的”。因为公平锁需要时间,并且 JVM 旨在尽可能快地实现所有保证(因此,不会给你未承诺的免费赠品),因此 JVM 锁几乎永远不会公平,除非JVM impl 的极其奇怪的组合,操作系统和硬件。

在放下和重新拍摄之间睡一觉,你可能会看到你所期待的东西。现在虚拟机/操作系统/硬件要么 [A] 实际上让 player2 的线程抓住锁并实际拿到球,要么 [B] 旋转它的拇指。

请注意,Thread.sleep您将要编写的内容不得在您持有锁的任何地方。鉴于整个 takeBall 方法是同步的,不要在那里做;在您的 for 循环中执行此操作。(作为旁注,捕获异常并且什么都不做?这非常糟糕,永远不要这样做1 - 我也为你解决了这个问题):

try {
  ball.takeBall();
  sleep(1000);
  ball.dropBall();
  sleep(100);
} catch (Exception e) {
  throw new RuntimeException("Unhandled", e);
}

[1] 除非你真的打算让代码默默地忽略错误并像什么都没发生一样继续。有时这是正确的举动,但极为罕见,新手不应该干预。太容易误解某事,然后完全丢失,因为关键信息(即错误发生、发生位置、原因以及周围的细节)被丢弃了。


推荐阅读