Java并发之:wait和notify/notifyAll

Thread和Runnable

  1. 联系:Thread类也实现了Runnable接口

  2. Java中创建线程的两种方式:

  • 继承自Thread类,然后重写其run方法

  • 声明一个类实现Runnable接口并实现抽象方法run,然后在创建Thread类实例时传入该声明类实例

    那么这两种方式哪种更好呢?答案是第2种:实现Runnable接口的这种方式。理由如下:

  • 耦合性更低,将线程和任务本身分离开来

  • 由于Java单继承的特性,继承了Thread类,那么意味着不能再继承其他类了。

那么这两种方式又有什么联系呢?我们来看Thread类的源码:

public void run() {
    if (target != null) {
        target.run();
    }
}

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
}

在这里插入图片描述
在这里插入图片描述

显然这个target就是我们在构造Thread对象传入的Runnable方法,当Thread类的run方法被JVM调用时,如果target不为空,则调用Runnable的run方法;而继承Thread类的方式,是直接重写了Thread类的run方法逻辑,从而调用子类的run方法。

  1. 如何启动线程:调用Thread.start()方法

wait()

首先明确一点,wait()方法是定义在Object类中的final实例方法。当然wait()还有两个重载方法,但本质上还是调用native的wait()方法。我们来看一下其文档注释(注释很长,我们将划分为几段逐步理解):

Causes the current thread to wait until either another thread invokes the notify() method or the notifyAll() method for this object, or a specified amount of time has elapsed.
The current thread must own this object’s monitor.

这一段我们可以总结为三点:

  • 使得当前线程进入等待
  • 被唤醒的条件:其他线程调用notify()/notifyAll(),或者是指定的等待时间已到
  • 当前线程必须持有对象的锁

This method causes the current thread (call it T) to place itself in the wait set for this object and then to relinquish any and all synchronization claims on this object. Thread T becomes disabled for thread scheduling purposes and lies dormant until one of four things happens:
Some other thread invokes the notify method for this object and thread T happens to be arbitrarily chosen as the thread to be awakened.
Some other thread invokes the notifyAll method for this object.
Some other thread interrupts thread T.
The specified amount of real time has elapsed, more or less. If timeout is zero, however, then real time is not taken into consideration and the thread simply waits until notified.

这一段可以理解为对上一段的补充说明:调用wait()会使得当前线程(T)将自身放置到这个对象(调用wait方法的那个)的WaitSet(等待集)中,然后会放弃任何在这个对象上的同步声明(可以理解为释放掉该对象的锁)。T线程此时会不能被调度,一直处于休眠状态,直到下面四种情况的发生:

  • 其他线程调用这个对象的notify方法,并且恰好唤醒线程T
  • 其他线程调用这个对象notifyAll方法
  • 其他线程中断了线程T
  • 等待的时间已到。如果超时时间为0,那么就忽略该时间,然后线程就仅仅等待被唤醒

The thread T is then removed from the wait set for this object and re-enabled for thread scheduling. It then competes in the usual manner with other threads for the right to synchronize on the object; once it has gained control of the object, all its synchronization claims on the object are restored to the status quo ante - that is, to the situation as of the time that the wait method was invoked. Thread T then returns from the invocation of the wait method. Thus, on return from the wait method, the synchronization state of the object and of thread T is exactly as it was when the wait method was invoked.
A thread can also wake up without being notified, interrupted, or timing out, a so-called spurious wakeup. While this will rarely occur in practice, applications must guard against it by testing for the condition that should have caused the thread to be awakened, and continuing to wait if the condition is not satisfied. In other words, waits should always occur in loops, like this one:
synchronized (obj) {
while ()
obj.wait(timeout);
… // Perform action appropriate to condition
}

这一段可以总结为:

  • 当线程T被唤醒后,它会从WaitSet中被移除,然后重新获得线程调度权(能够线程调度,并不意味着该线程能够获得执行权)

  • 重新获得到调度权之后,线程T会和其他线程来竞争执行权。如果竞争成功,那么线程T对于该对象的同步状态就和调用wait方法之前一样

  • 存在一种虚假唤醒,所以使得我们对于wait方法的使用:

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

    synchronized代码块:使得调用wait()时,线程已经获取到obj对象的锁;while可以使得当线程重新被唤醒之后,再次判断等待条件是否满足。

If the current thread is interrupted by any thread before or while it is waiting, then an InterruptedException is thrown. This exception is not thrown until the lock status of this object has been restored as described above.
Note that the wait method, as it places the current thread into the wait set for this object, unlocks only this object; any other objects on which the current thread may be synchronized remain locked while the thread waits.
This method should only be called by a thread that is the owner of this object’s monitor.

  • 在等待期间被其他线程中断,那么InteruptedException要想抛出,也是在线程重新获得执行权之后
  • wait()的调用,只会影响线程对于当前对象的同步声明,不影响线程对于其他对象的同步状态
  • 再次强调,wait()只能够拥有该对象锁的线程调用

至此,wait()的文档注释我们就解读完毕。下面我们来思考几个问题:

  1. 如果在没有获得对象锁的情况下调用wait(),那么会造成什么现象?
public class MyTest {

    public static void main(String[] args) {
        Object o = new Object();
        try {
            o.wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这样,程序会抛出一个异常:java.lang.IllegalMonitorStateException。

Thrown to indicate that a thread has attempted to wait on an object’s monitor or to notify other threads waiting on an object’s monitor without owning the specified monitor.

  1. wait()和Thread.sleep()的比较

    • 相同点:都会阻断线程的执行,使得线程进入阻塞状态

    • 不同点:

      • wait()是Object类的实例方法,sleep()是Thread类的静态方法
      • 调用wait()之前,当前线程必须持有该对象的锁,在调用wait()之后,该对象锁会被释放;而sleep方法没有这一限制,并且也不会释放对象的锁
      • 调用无参wait()之后线程会一直等待,除非其他线程调用notify或notifyAll方法,当当前线程再次获得对象锁时才恢复执行;而sleep方法在等待指定时间后被唤醒。

notify()

同wait()一样,notify()是定义在Object类中的实例方法,我们来看文档说明:

Wakes up a single thread that is waiting on this object’s monitor. If any threads are waiting on this object, one of them is chosen to be awakened. The choice is arbitrary and occurs at the discretion of the implementation. A thread waits on an object’s monitor by calling one of the wait methods.

可以总结为:

  • 唤醒一个正在等待该对象锁的线程
  • pick的标准是任意的,取决于JVM的实现

The awakened thread will not be able to proceed until the current thread relinquishes the lock on this object. The awakened thread will compete in the usual manner with any other threads that might be actively competing to synchronize on this object; for example, the awakened thread enjoys no reliable privilege or disadvantage in being the next thread to lock this object.

总结为:被唤醒的线程并不会立即得到执行,而是在同其他线程竞争获得锁的拥有权之后,才会恢复执行。在竞争锁的过程中,被唤醒的线程并不会享有特权。

This method should only be called by a thread that is the owner of this object’s monitor. A thread becomes the owner of the object’s monitor in one of three ways:
By executing a synchronized instance method of that object.
By executing the body of a synchronized statement that synchronizes on the object.
For objects of type Class, by executing a synchronized static method of that class.

调用notify方法的线程必须是该对象锁的拥有者。线程成为对象锁的拥有者有三种方式:

  • 执行了该对象的实例同步方法
  • 执行了一段同步代码块,并且synchronized关键字后跟的就是该对象
  • 对于Class对象而言,执行了该类的静态同步方法

Only one thread at a time can own an object’s monitor.

这句话很关键:在某一时刻,只有一个线程可以持有一个对象的锁。

这里实际上就需要涉及到对synchronized关键字的深入理解,笔者打算放在下一篇来对synchronized进行总结。

notifyAll()

它的行为和notify()是类似的,只不过它是会唤醒所有等待锁的线程,然后他们再参与锁的竞争,最终竞争到锁的那个线程才会恢复执行。

示例:wait()和notify()结合

假定我们有一个类:它有个实例变量counter,外加两个方法,一个方法负责将counter自增,另一个负责将counter自减。要求:创建两个线程,一个线程调用自增方法,另一个线程调用自减方法,使得输出结果为10101010…

public class Calculator {

    private int counter = 0;

    public synchronized void increase() {
        while (counter != 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        counter++;
        System.out.println(counter);
        notify();

    }

    public synchronized void decrease() {
        while (counter == 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        counter--;
        System.out.println(counter);
        notify();
    }
}
class IncreaseRunnable implements Runnable {

    private final Calculator calculator;


    public IncreaseRunnable(Calculator calculator) {
        this.calculator = calculator;
    }

    @Override
    public void run() {

        System.out.println("increase start");
        for (int i = 0; i < 10; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            calculator.increase();
        }
    }
}
class DecreaseRunnable implements Runnable {

    private final Calculator calculator;


    public DecreaseRunnable(Calculator calculator) {
        this.calculator = calculator;
    }

    @Override
    public void run() {

        System.out.println("decrease start");

        for (int i = 0; i <    10; i++) {
            try {
                Thread.sleep((long) (Math.random() * 1000));
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            calculator.decrease();
        }
    }
}
public static void main(String[] args) {

    Calculator calculator = new Calculator();
    IncreaseRunnable increaseRunnable = new IncreaseRunnable(calculator);
    DecreaseRunnable decreaseRunnable = new DecreaseRunnable(calculator);

    new Thread(increaseRunnable).start();
    new Thread(decreaseRunnable).start();
}    

这样,我们就能实现了功能。在Calculator类的increase()和decrease()实现中,对于条件的判断我们用了while,大家可以尝试换成if,然后增加线程的个数,看看有什么变化。

WaitSet的本质

根据前面的内容,我们可以知道,当wait()之后,当前执行线程会进入阻塞状态,并且会释放掉持有的该对象的锁,并且将自身放入到该对象对应的WaitSet中,那么WaitSet到底是个什么样的结构?我们下面根据openJDK的C++代码来看一下:

http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/4689eaf1a5c9/src/share/vm/runtime

在这个路径下找到objectMonitor.cpp和objectMonitor.hpp文件:

在objectMonitor.hpp中:

在这里插入图片描述

在这里插入图片描述

我们可以找到这样的两个类的声明。ObjectWaiter我们可以视为是线程对象的代理,而ObjectMonitor就是我们口中常说的锁对象。在ObjectMonitor类中,定义了一个wait()方法,它实际上就是Object中wait()方法的逻辑实现。我们可以在objectMonitor.cpp中找到该方法的实现:

在这里插入图片描述

由于代码很长,我们就不往下截取了。但是这个方法会调用AddWaiter()方法:
在这里插入图片描述

这块逻辑还是很容易懂的,而且从这块,就可以看出,WaitSet实际上就是一个循环双向链表结构

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值