Java wait和notify方法解释

  • Post author:
  • Post category:java




wait与notify的API以及解释

public final void wait() throws InterruptedException {
    wait(0);
}

public final void wait(long timeout, int nanos) throws InterruptedException {}

public final native void wait(long timeout) throws InterruptedException;

public final native void notify();

public final native void notifyAll();

这几个方法都是Object的方法,Object在Java中是万物之母,也就是说,所有的类都会有这几个方法。这时候我们要来理解一下锁的概念了。

在开始wait的讲解之前,先了解一下Java的锁相关的东西做个铺垫。

我们知道,一个共享数据被多个线程同时操作时需要格外注意,因为可能会有线程同步的问题,一个很常用的方法就是当多线程操作同一共享数据的时候,我们会选择给这个操作过程加锁从而保证操作的原子性。我们使用synchronized()为操作加锁的时候需要传递一个参数,这个参数其实就是一个对象,当然如果是对一个静态方法加synchronized的时候,锁对象是类本身。

我们来看一下对象的内存结构:

1.对象是存储在堆内存的

2.对象中包含的数据主要有:对象头,实例变量,填充数据

3.对象头中又保存了锁的标记位和一个指向monitor对象起始地址的引用,这个Monitor对象中,包含了_EntryList,_Owner,_WaitSet这三个部分。

对象内部结构

下面我们对这个Monitor对象详细说明:

monitor是由ObjectMonitor实现的ObjectMonitor中有两个队列分别是_EntryList、_WaitSet,用来保存ObjectWaiter对象列表(每个等待锁的线程都会被封装成ObjectWaiter对象)。还有一个_Owner对象指向了持有ObjectMonitor对象的线程。当多个线程同时访问一段同步代码时,首先线程会先进入_EntryList这个入口列表,当某一线程得到执行并且获取到对象的monitor后进入_Owner区域并且此时_Owner会指向该线程,同时monitor中有一个计数器count+1,若紧接着该线程调用了wait()方法,那么这个线程会释放当前持有的monitor也就是说_Owner变量会恢复为null,count-1,同时这个线程会进入_WaitSet这个集合中等待某个线程唤醒它。如果这个线程被唤醒,那么它需要重新获取该monitor也就是说该对象的_Owner重新指向它它才能得到执行。当然如果该线程并没有调用wait()方法那么当这个线程执行完毕的时候,也一样会释放monitor也就是说对象的_Owner对象会恢复为null并且count-1,以便其他线程获取该monitor锁。

对象中monitor作用

以上参考了:

https://blog.csdn.net/kking_edc/article/details/108382333

看完以上讲解之后,其实我们对wait()和notify()应该有一定的概念了。现在我们回想一下,我们调用某个锁对象的wait()方法其实是想把这个线程先阻塞住,等到条件合适再继续执行。那么依照上面的分析,其实是把该线程放到该锁对象的_WaitSet集合中等待重新执行。

接下来我们来写一些代码用一用这俩方法:

public Object lock = new Object();

public void main() {
    Thread thread1 = new Thread(() -> {
        System.out.println("执行" + Thread.currentThread().getName() + "线程");
        synchronized (lock) {
            try {
                System.out.println("将" + Thread.currentThread().getName() + "阻塞在lock对象上");
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "线程执行完了");
    }, "thread1");
    Thread thread2 = new Thread(() -> {
        try {
            Thread.sleep(3000L);
            synchronized (lock) {
                lock.notify();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "thread2");


    thread1.start();
    thread2.start();
}

输出结果:

执行thread1线程
将thread1阻塞在lock对象上
thread1线程执行完了

上面例子中,分别定义2个线程thread1和thread2,当thread1开始执行时就调用lock.wait()阻塞住,在thread2中等待3s之后唤醒thread1,此时thread1会重新去获取锁并继续向下执行。

我们将例子改造一下来证明线程被wait之后会释放掉锁持有的同步锁。

public Object lock = new Object();

public void main() throws InterruptedException {
    Thread thread1 = new Thread(() -> {
        System.out.println("执行" + Thread.currentThread().getName() + "线程");
        synchronized (lock) {
            try {
                System.out.println("将" + Thread.currentThread().getName() + "阻塞在lock对象上");
                lock.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(Thread.currentThread().getName() + "线程执行完了");
    }, "thread1");
    Thread thread2 = new Thread(() -> {

        for (int i = 0; i < 5; i++) {
            synchronized (lock) {
                try {
                    System.out.println(Thread.currentThread().getName() + "线程获得了锁,并执行同步代码");
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }, "thread2");


    thread1.start();
    //将thread2线程启动向后延迟1s以确保thread1必定能先执行到
    Thread.sleep(1000L);
    thread2.start();
}

输出结果:

执行thread1线程
将thread1阻塞在lock对象上
thread2线程获得了锁,并执行同步代码
thread2线程获得了锁,并执行同步代码
thread2线程获得了锁,并执行同步代码
thread2线程获得了锁,并执行同步代码
thread2线程获得了锁,并执行同步代码

以上结果说明当一个线程被wait之后,是会释放掉该线程持有的锁的。假如锁没有被释放的话,那thread2的打印代码一句都执行不到。

最后还有个notifyAll()方法,其实和notify()方法的作用一模一样,唯一的不同是调用lock.notifyAll()之后,会唤醒lock对象的等待集合中的所有线程,但是具体哪个线程先得到执行权并获得锁执行代码是不一定的,这和cpu调度线程有关。

并且我们注意一下

1.不管是lock.wait()方法包在多少层的synchronized(lock)代码块中,执行的结果也是一样的,反正也是得释放锁。

2.不管他外层包了多少层的synchronized(lock),synchronized(lock1),它都是调用了哪个lock对象的wait()方法,线程就会等在该lock对象的等待集合中,其他的锁是不会释放的。

说到2的时候可能比较抽象,我们再将代码改造一下看看效果:

public Object lock = new Object();
public Object lock1 = new Object();

public void main() throws InterruptedException {
    Thread thread1 = new Thread(() -> {
        System.out.println("执行" + Thread.currentThread().getName() + "线程");
        synchronized (lock) {
            synchronized (lock1) {

                try {
                    System.out.println("将" + Thread.currentThread().getName() + "阻塞在lock对象上");
                    lock1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        System.out.println(Thread.currentThread().getName() + "线程执行完了");
    }, "thread1");
    Thread thread2 = new Thread(() -> {

        for (int i = 0; i < 5; i++) {
            synchronized (lock) {
                try {
                    System.out.println(Thread.currentThread().getName() + "线程获得了锁,并执行同步代码");
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }, "thread2");


    thread1.start();
    //将thread2线程启动向后延迟1s以确保thread1必定能先执行到
    Thread.sleep(1000L);
    thread2.start();
}

输出结果:

执行thread1线程
将thread1阻塞在lock对象上

此时我们在thread1中先获取了一个同步锁lock,紧接着在里面再获取一个lock1同步锁,这时候在代码中调用lock1.wait()将线层阻塞,此时该线程会等待在lock1的等待集合中并且释放锁lock1,但是thread2要执行的时候需要获取lock的锁,这时候lock的锁还被thread1占着,也就是说lock对象里的monitor中的_Owner的线程对象还是thread1,所以thread2永远得不到执行。

希望本文章能对Thread中wait和notify这个线程的交互机制还不熟悉的朋友有点参考意义。



版权声明:本文为wenhonglian原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。