理解Java并发编程:Object的wait/notify/notifyAll方法解析


掌握Java并发编程是深入理解Java的必经之路。市面上许多高性能的开源框架中都用到了Java并发编程技术。本专栏从Java并发相关的知识点逐一切入,循序渐进,最终将Java并发相关的知识完美地呈现在读者面前。

首先要讲解的是Object类的用于线程间通信的方法。

wait/notify/notifyAll

谈到并发控制,就不得不提Object类提供的线程间通信方式。Object类提供了三个用于线程间通信的方法:wait/notify/notifyAll。所有的Java对象都自然继承这些方法。下面对这些方法一一讲解。

wait

调用对象的wait()方法会使当前线程等待,直到另一个线程调用此对象的 notify() 方法或 notifyAll() 方法。

调用wait方法时,当前线程必须先拥有此对象的监视器(monitor),否则报错。调用wait方法后,线程释放此监视器的所有权并等待,直到另一个线程通过调用notify方法或notifyAll方法唤醒在此对象监视器上等待的线程。被唤醒的线程与其它线程公平竞争该对象的锁,直到它可以重新获得对象监视器的所有权后才恢复执行。

划重点:对象的wait()方法只能被此对象监视器(monitor)的所有者的线程调用。也就是说当调用wait方法时,首先要确保调用wait方法的线程已经获得了对象的锁。

中断和虚假唤醒是可能的,因此应该向下面示例这样,始终在循环中调用wait方法,而不是使用if判断。

synchronized (obj) {
    while (<condition does not hold>)
          obj.wait();
          ... // Perform action appropriate to condition
}

wait方法有个重载的版本,即wait(long timeout)timeout为等待的最长时间(以毫秒为单位)

调用wait()方法等价于调用wait(0)方法。换句话说,wait()方法的行为和调用 wait(0) 一样。

调用wait(timeout)方法会导致当前线程等待,直到另一个线程调用此对象的notify()方法或notifyAll()方法,或者指定的时间量已经过去。和wait方法一样,调用wait(timeout)方法时当前线程必须拥有此对象的监视器。

wait(timeout)方法使当前线程(称之为T)将自己置于此对象的等待集(wait set)中,然后放弃对此对象的所有同步声明。 线程T出于线程调度目的而被禁用并处于休眠状态,直到发生以下四种情况之一:

  • 某个其它线程调用了此对象的notify方法,而线程T恰好被任意选择为要唤醒的线程。
  • 其他一些线程调用了此对象notifyAll方法。
  • 其他一些线程中断了线程T 。
  • 等待已经过了指定的时间。如果timeout为零,则不考虑等待时间,线程只是等待直到收到通知。

然后,线程T从该对象的等待集中移除,并重新启用线程调度。 然后它以正常的方式与其他线程竞争在对象上同步的权利; 一旦它获得了对象的控制权,它对对象的所有同步声明都将恢复到之前的状态。也就是说,恢复到调用wait方法时的情况。 然后线程T从wait方法的调用中返回。 因此,从wait方法返回时,对象和线程T的同步状态与调用wait方法时完全相同。

线程也可以在没有被通知、中断或超时的情况下唤醒,即所谓的虚假唤醒。 虽然这在实践中很少发生,但应用程序必须再次判断导致线程被唤醒的条件来防止虚假唤醒,如果条件不满足则继续等待。 换句话说,等待应该总是在循环中发生,就像下面的例子一样。

这里使用while循环判断而不是if单次判断,保证满足条件才会继续执行。

synchronized (obj) {
     while (<condition does not hold>)
          obj.wait(timeout);
          ... // Perform action appropriate to condition
}

如果调用对象的wait方法时当前线程没有获得对象的monitor,则会抛出异常,例如下面这个例子。

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        o.wait();
    }
}

运行时异常

Exception in thread "main" java.lang.IllegalMonitorStateException
	at java.lang.Object.wait(Native Method)
	at java.lang.Object.wait(Object.java:502)
	at com.bigbird.conc.Test1.main(Test1.java:6)

当前线程必须拥有该对象的监视器(monitor),才能调用该对象的wait()方法。

获得某个对象的同步监视器可以使用synchronized关键字,一旦进入到synchronized块内,该线程必然获取到了对象的监视器。

public class Test1 {
    public static void main(String[] args) throws InterruptedException {
        Object o = new Object();
        synchronized (o){
            o.wait();
        }
    }
}

Tip:sleep()方法和wait()方法的区别

在调用对象的wait()方法时,线程必须持有被调用对象的锁(monitor);调用wait()方法后,线程就会释放掉该对象的锁。而在调用线程的sleep()方法时,不需要获得对象的monitor,也不会释放任何该线程持有的对象的锁(monitor)。

notify

调用对象的notify()方法会唤醒一个正在等待这个对象的锁的线程。如果有多个线程都在等待这个对象的锁,则会任意选择其中一个将其唤醒。正如前文所述,一个线程可以通过调用该对象的wait()方法来等待该对象的锁。被唤醒的线程不会继续执行,而是和其它线程一样共同竞争该对象的锁。获得对象的锁之后才会继续执行。

和wait()方法一样,调用对象的notify()方法的线程也必须先持有该对象的锁(monitor)。

获取对象的锁(monitor)的方式有下列几种:

  • 调用该对象的一个用synchronized修饰的实例方法

  • 通过执行一个由synchronized修饰的代码块

  • 通过调用类的一个synchronized static 修饰的类方法来获得Class类型对象的锁

一个对象的锁(monitor)同一时刻只能被一个线程拥有。

notifyAll

调用对象的notifyAll()方法会唤醒等待此对象锁(monitor)的所有线程,即该对象等待集合(wait set)中的所有线程被唤醒。

前文已经介绍了,一个线程可以通过调用某个对象的wait()方法使其等待该对象的锁,即进入该对象的等待集合。

被唤醒的线程不会继续执行直到其获得该对象的锁。被唤醒的线程和其它线程相比,在对象锁的竞争上没有什么劣势或者优势,它们公平竞争对象的同步锁(monitor),都有可能成为该对象锁的下一个持有者。

和wait()、notify()方法一样,一个对象的notifyAll()方法只能被持有这个对象的monitor的线程所调用。

获取对象的锁(monitor)的3种方式上面已经介绍,无疑离不开synchronized关键字。

案例

结合一个案例讲解一下上述几个方法的应用。

需求描述:创建一个计数器对象,提供加一和减一方法。创建多个线程调用该对象的加一、减一方法。

并输出这样一个结果序列:10101010…

代码如下:

//计数器对象
public class MyCounter {
    private int counter;
    public synchronized void increase() throws InterruptedException {
        //此处不能用if判断,必须放到循环中判断
        while (counter != 0) {
            wait();
        }
        counter++;
        System.out.println(counter);
        notify();
    }

    public synchronized void decrease() throws InterruptedException {
        while (counter != 1) {
            wait();
        }
        counter--;
        System.out.println(counter);
        notify();
    }
}
//加1线程
public class IncreaseThread extends Thread {
    private MyCounter counter;
    public IncreaseThread(MyCounter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
                counter.increase();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
//减1线程
public class DecreaseThread extends Thread {
    private MyCounter counter;
    public DecreaseThread(MyCounter counter) {
        this.counter = counter;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
                counter.decrease();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

测试

public class MainApp {
    public static void main(String[] args) {
        MyCounter myCounter = new MyCounter();
        IncreaseThread increaseThread1 = new IncreaseThread(myCounter);
        IncreaseThread increaseThread2 = new IncreaseThread(myCounter);
        DecreaseThread decreaseThread1 = new DecreaseThread(myCounter);
        DecreaseThread decreaseThread2 = new DecreaseThread(myCounter);
        increaseThread1.start();
        increaseThread2.start();
        decreaseThread1.start();
        decreaseThread2.start();
    }
}

控制台输出1010101010....,符合预期。也可以将while循环判断改成if判断,看看结果是否满足预期。

未完待续…请关注公众号【程猿薇茑】

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程猿薇茑

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值