Java多线程 - wait/notify/notifyAll

这几个方法都是Object的方法,主要配合synchronized同步一起使用。这涉及到Java的锁机制,可以参考我的一篇博文 Java多线程 - 锁机制

我们接下来看看这几个方法:

(1)wait

public final void wait()  throws InterruptedException,IllegalMonitorStateException

线程要调用Object实例对象的wait方法前,必须要先获取该Object实例对象锁,否则报IllegalMonitorStateException异常,所以wait方法必须在获取Object实例对象锁的同步块中调用。在线程调用Object实例对象的wait方法后,线程便进入“等待”状态,直到收到通知或者中断为止

// ① wait 实质
在线程(如A)调用Object实例(如lockA)的wait方法后,线程(A)会进入实例对象(lockA)的等待池中,
而等待池的线程处于wait状态,不会主动去获取Object实例(lockA)对象锁,等待notify或者notifyAll。
所以线程(A)等待的不是Object实例(lockA)对象锁,等待的是有其他线程调用Object实例(lockA)的notify或者notifyAll通知。

例子:

public static class RunnableA implements Runnable {
        private Object lockA;
        public RunnableA(Object lockA) {
            this.lockA = lockA;
        }
        @Override
        public void run() {
            synchronized (lockA) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.err.println("RunnableA before wait lockA");
                try {
                    // 必须在 synchronized (lockA) {} 内调用,线程会在这里阻塞,等待通知
                    lockA.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 在其他线程没有调用notify或者notifyAll方法之前,不会执行
                System.err.println("RunnableA after wait lockA");
            }
        }
    }

(2)notify

public final native void notify() throws IllegalMonitorStateException

和wait方法一样,调用之前一定要先获取Object实例对象锁,否则报IllegalMonitorStateException错误。
在线程调用Object实例对象notify方法后,会通知等待该Object实例对象锁的其他线程(也就是调用过该Object实例的wait方法处于等待的线程),获取到该Object对象锁的线程回继续执行wait方法后面的语句。
需要注意的是,在线程(A)调用Object实例的notify方法后,并不会立刻释放Object实例对象锁,synchronized同步块的代码会执行完了(跳出synchronized同步块)之后,线程(A)才会释放Object实例对象锁,线程(B,任意一个处于wait状态的线程)才能获取到Object实例对象锁,继续往下执行。
如果有多个线程(A,B,C)都处于wait状态,任意一个线程(如A)会收到通知,其他线程(B,C)的仍然处于wait状态。即使Object实例对象锁闲置了,其他线程(B,C)也不会主动改变状态,也不会主动去获取Object实例对象锁。除非有线程(A)调用了Object实例notify方法,wait的线程(B,C)才机会收到通知,继续执行。

// ② notify 实质 
// 结合 ① wait 实质
在线程(如B)调用Object实例(lockA)的notify方法后,收到通知的线程(A)会被移到
实例对象(lockA)的锁池,在这个池的线程会竞争实例对象(lockA)锁。竞争到锁的线程也会被移出锁池,
继续执行wait后面的语句。

例子:

public static class RunnableB implements Runnable {
    private Object lockA;
    public RunnableB(Object lockA) {
        this.lockA = lockA;
    }
    @Override
    public void run() {
        synchronized (lockA) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.err.println("RunnableB notify lockA");
            // 必须在 synchronized (lockA) {} 内调用
            lockA.notify();
            // 即使调用notify通知其他线程,这段代码还是会先执行,因为还没有释放lockA锁
            System.err.println("RunnableB after notify lockA");
        }
    }
}

在给出main方法:

public static void main(String[] args) {
    // 实例对象
    Object lockA = new Object();
    // RunnableA和RunnableB持有同一个实例对象
    Thread threadA = new Thread(new RunnableA(lockA));
    Thread threadB = new Thread(new RunnableB(lockA));
    // 启动threadA和threadB
    threadA.start();
    threadB.start();
}

打印结果:

RunnableA before wait lockA     // RunnableA开始等待,并释放lockA实例对象锁
RunnableB notify lockA          // RunnableB执行
RunnableB after notify lockA    // RunnableB继续执行,释放lockA实例对象锁
RunnableA after wait lockA      // RunnableA收到通知,获取到lockA实例对象锁,继续执行wait后面的语句

(3)notifyAll

public final native void notifyAll() throws IllegalMonitorStateException

和wait,notify方法一样,调用之前一定要先获取Object实例对象锁,否则报IllegalMonitorStateException错误。
在线程调用Object实例对象notifyAll方法后,会通知等待该Object实例对象锁的所有线程(也就是调用过该Object实例的wait方法处于等待的所有线程)。
根据notify的实质原理,有2个线程(A,B)调用了Object实例对象(lockA)的wait方法,此时,这2个线程(A,B)就被移到了Object实例对象(lockA)的等待池中。有个线程(C)调用了Object实例对象(lockA)的notifyAll方法,此时,处于wait状态的2个线程(A,B)都会被移到Object实例对象(lockA)的锁池中。那么这2个线程(A,B)就会开始竞争Object实例对象(lockA)锁。如果其中一个线程(A)争到了,执行wait后面的语句,跳出synchronized同步代码块,释放锁。此时的另一个线程(B)还在Object实例对象(lockA)的锁池中,因为Object实例对象(lockA)锁已经闲置了,线程(B)便可以获取Object实例对象(lockA)锁,也接着执行wait后面的语句。
所以在线程中调用Object实例对象的notifyAll方法,会让所有处于wait状态的线程都有机会竞争实例对象锁。

(4)wait(long)和wait(long,int)

这两个方法是设置等待超时时间的,后者在超值时间上加上ns,精度也难以达到,因此,该方法很少使用。对于前者,如果在等待线程接到通知或被中断之前,已经超过了指定的毫秒数,则它通过竞争重新获得锁,并从wait(long)返回。另外,需要知道,如果设置了超时时间,当wait()返回时,我们不能确定它是因为接到了通知还是因为超时而返回的,因为wait()方法不会返回任何相关的信息。但一般可以通过设置标志位来判断,在notify之前改变标志位的值,在wait()方法后读取该标志位的值来判断,当然为了保证notify不被遗漏,我们还需要另外一个标志位来循环判断是否调用wait()方法。

(5)sleep和wait的区别

两者都可以让线程阻塞,但是有2个明显的区别:

  • sleep是Thread类的方法,而wait是Object类的方法,根本不是同一类的方法。

  • sleep方法导致了程序暂停执行指定的时间,让出cpu给其他线程,但是他的监控状态依然保持者,当指定的时间到了又会自动恢复运行状态。在调用sleep方法的过程中,线程不会释放对象锁。而当调用wait方法的时候,线程会放弃对象锁,进入等待此对象的等待池,只有针对此对象调用notify方法后本线程才进入对象锁池准备。

(6)巩固

下面再给出一个例子(网上搜的),巩固一下我们对wait和notify的使用:

public class MyThreadPrinter implements Runnable {     

    private String name;     
    private Object prev;     
    private Object self;     

    private MyThreadPrinter(String name, Object prev, Object self) {     
        this.name = name;     
        this.prev = prev;     
        this.self = self;     
    }     

    @Override    
    public void run() {     
        int count = 10;     
        while (count > 0) {     
            synchronized (prev) {     
                synchronized (self) {     
                    System.out.print(name);     
                    count--;    
                    self.notify();     
                }     
                try {     
                    prev.wait();     
                } catch (InterruptedException e) {     
                    e.printStackTrace();     
                }     
            }     
        }     
    }     

    public static void main(String[] args) throws Exception {     
        Object a = new Object();     
        Object b = new Object();     
        Object c = new Object();     
        MyThreadPrinter pa = new MyThreadPrinter("A", c, a);     
        MyThreadPrinter pb = new MyThreadPrinter("B", a, b);     
        MyThreadPrinter pc = new MyThreadPrinter("C", b, c);     
        new Thread(pa).start();  
        Thread.sleep(100);  //确保按顺序A、B、C执行  
        new Thread(pb).start();  
        Thread.sleep(100);    
        new Thread(pc).start();     
        Thread.sleep(100);    
    }     
}    

我们来分析一下上面的例子。

(1)pa(启动或唤醒),先获取prev(c)对象锁(同时持有c对象锁是pc线程。第一次获取时,pc没有启动;第一次之后,pc早已经执行完synchronized(self)代码块,自然已经释放了c对象锁),然后获取self(a)对象锁(同时持有a对象锁是pb线程。第一次获取时,pb没有启动;第一次之后,pb调用a的wait方法,处于阻塞状态,释放了a对象锁),再调用self(a)的notify,唤醒pb(第一次启动时,pb并没有启动;第一次之后,pb处于wait状态,便收到通知,开始唤醒),最后调用prev(c)的wait方法,开始阻塞,等待prev(c)的notify或者notifyAll。

(2)pb(启动或唤醒),先获取prev(a)对象锁(同时持有a对象锁是pa线程。此时的pa处于wait状态,而且已经执行完synchronized(self)代码块,不再持有a对象锁),然后再获取self(b)对象锁(同时持有a对象锁是pc线程。第一次获取时,pc没有启动;第一次之后,pc调用b的wait方法,处于阻塞状态,释放b对象锁),再调用self(b)的notify,唤醒pc(第一次启动时,pc并没有启动;第一次之后,pc处于wait状态,便收到通知,开始唤醒),最后调用prev(a)的wait方法,开始阻塞,等待prev(a)的notify或者notifyAll。

(3)pc(启动或唤醒),先获取prev(b)对象锁(同时持有b对象锁是pb线程。此时的pb处于wait状态,而且已经执行完synchronized(self)代码块,不再持有b对象锁),然后再获取self(c)对象锁(同时持有c对象锁是pa线程。pa调用c的wait方法,处于阻塞状态,释放c对象锁),再调用self(c)的notify,唤醒pa(pa处于wait状态,便收到通知,开始唤醒),最后调用prev(b)的wait方法,开始阻塞,等待prev(b)的notify或者notifyAll。因为pa收到唤醒通知,有回到(1)。

所以运行的结果是ABCABCABCABC…

到这里,相信你也会很自如的使用wait和notify了。

参考资料

【Java并发编程】之十:使用wait/notify/notifyAll实现线程间通信的几点重要说明

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值