线程通知(notify)与等待(wait)(基础系列三)

 

目录

为什么wait()和notify()方法是在Object类中?

1、wait()函数

2、wait(long timeout)函数

3、wait(long timeout,int nanos)函数

4、notify()函数

5、notifyAll()函数


Java中所有对象的父类都是Object类,继承即拥有,所以Object类中有所有子类都需要用到的方法。new Object ().getClass()、toString()、hashCode()、equals()、wait()、notify()

这里也涉及到一个面试题:为什么wait()和notify()方法是在Object类中?

为什么wait()和notify()方法是在Object类中?

首先wait()和notify()都需要在持有监视器锁的范围内使用,然后因为等待和唤醒必须是同一个锁,而锁可以是任意对象,所以可以被任意对象调用的方法是定义在object类中。如:对象.watify()和对象.notify()。

1、wait()函数

当一个线程调用一个共享变量的wait()方法时,该调用线程就会被阻塞挂起,当其他线程调用了该共享对象的notify()或者notifyAll()方法就会返回,或者其他线程调用了该线程的interrupt()方法,该线程抛出InterruptException异常而返回。

获取共享变量的监视器锁有两种方式:

1)、synchronized同步代码块,使用共享变量作为参数

synchronzed(共享变量){
    // 业务逻辑
}

 2)、synchronized修饰方法

public synchronzed void test(){
    // 业务逻辑
}

这里有个问题需要注意,一个线程可以从挂起状态变为可运行状态(就是被唤醒),即使该线程没有被其他线程调用notify()或者notifyAll()方法进行通知,或者被其他线程中断,或者等待超时,这就是虚假唤醒。

虚假唤醒解决的办法就是要在一个while循环判断,满足条件就退出,不满足就继续等待。

synchronzed(obj){
    while(条件不满足){
        obj.wait();
    }
}

 为了加深理解,下面使用一个经典的生产者消费者例子。这里使用一个queue变量作为一个共享变量,生产者在调用wait方法之前,会先通过synchronized 关键字获取该共享变量queue的监视器锁。如果当前队列没有空闲容量,则会调用wait方法进行阻塞,这里使用的while循环就是为了避免虚假唤醒问题。假如当前线程被虚假唤醒了,但是在循环条件判断的时候队列容量还是没有空闲的,那依然会调用wait方法阻塞当前线程。

public class ProducterConsumer {
    private static Queue queue = new PriorityQueue ();
    private final Integer MAX_SIZE = 1;
    private final Integer EMPTY = 0;

    /**
     * 生产者
     */
    public void producer() {
        synchronized (queue) {
            // 消费队列满,则等待队列空闲
            while (queue.size () == MAX_SIZE) {
                try {
                    System.out.println ("挂起当前线程,并释放通过同步块获取的queue上的锁,让消费者可以获取该锁");
                    queue.wait ();
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
            }
            System.out.println ("空闲则生产元素,并通知消费者线程");
            queue.add ("1");
            queue.notifyAll ();
        }
    }

    /**
     * 消费者
     */
    public void consumer() {
        synchronized (queue) {
            //消费队列为空
            while (queue.size () == EMPTY) {
                try {
                    System.out.println ("挂起当前线程,并释放通过同步块获取的queue上的锁,让生产者可以获取该锁");
                    queue.wait ();
                } catch (InterruptedException e) {
                    e.printStackTrace ();
                }
            }
            System.out.println ("消费元素,并通知唤醒生产线程");
            System.out.println ("元素==> " + queue.element ());
            queue.notifyAll ();
        }
    }
}

 如上代码中,生产者线程首先通过synchronized 获取共享变量queue的锁,这样其他线程在获取这个共享变量queue的锁就会被阻塞挂起。当生产者线程调用wait方法后就会释放当前共享变量的锁,只有这里释放共享变量的锁,别的线程才能有机会去竞争获取锁,不然就会变成死锁,造成死锁必须具备四个条件:互斥条件、请求并持有条件、不可剥夺条件、环路等待条件。但是要想避免死锁,目前只能破坏两个条件,请求并持有条件和环路等待条件。所以最好的办法就是申请资源的顺序要保持有序,就是线程获取资源的顺序一致,这样一个线程释放了锁,另一个线程就可以去竞争获取锁。

需要注意的是,当前线程调用共享变量的wait方法后,只会释放当前共享变量上的锁,如果当前线程还持有其他共享变量的锁,那这些锁就不会被释放。

还是来举个栗子证明一下。

public class ReleaseShareArgLock {
    private static volatile Object resourceA = new Object ();
    private static volatile Object resourceB = new Object ();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread (() -> {
            try {
                //获取resourceA共享变量的锁
                synchronized (resourceA) {
                    System.out.println ("线程A获取resourceA共享变量的锁");

                    // 获取resourceB共享变量的锁
                    synchronized (resourceB) {
                        System.out.println ("线程A获取resourceB共享变量的锁");

                        System.out.println ("线程A释放resourceA共享变量的锁");

                        //释放resourceA的锁
                        resourceA.wait ();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace ();
            }
        });

        Thread threadB = new Thread (() -> {
            try {
                TimeUnit.SECONDS.sleep (1);

                //获取resourceA共享变量的锁
                synchronized (resourceA) {
                    System.out.println ("线程B获取resourceA共享变量的锁");

                    System.out.println ("线程B尝试获取resourceB共享变量的锁");

                    // 获取resourceB共享变量的锁
                    synchronized (resourceB) {
                        System.out.println ("线程B获取resourceB共享变量的锁");

                        System.out.println ("线程B释放resourceA共享变量的锁");

                        //释放resourceA的锁
                        resourceA.wait ();
                    }
                }
            } catch (InterruptedException e) {
                e.printStackTrace ();
            }
        });

        threadA.start ();
        threadB.start ();

        threadA.join ();
        threadB.join ();

        System.out.println ("main over");
    }

}

输出:threadA线程获取resourceA的共享变量的锁,调用wait方法后,释放resourcesA共享变量的锁,但是当前线程持有的其他共享变量的监视器锁不会释放,所以会一直阻塞resourceB的共享变量,threadB线程获取不了锁。

 

2、wait(long timeout)函数

该方法比wait方法多了一个超时参数,意思就是当一个线程调用一个共享变量的wait方法后,如果在指定timeout时间没有线程调用其共享变量的notify或notifyAll方法,则会因为超时而返回。参数不能为负数,否则会抛异常,如果参数为0,则等于无参的wait()方法。

3、wait(long timeout,int nanos)函数

源码如下,也就是只有nanos大于1的时候,timeout才会自增。

public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

4、notify()函数

一个线程调用共享变量的notify方法后,会唤醒一个在该共享变量上调用wait方法而阻塞的线程。一个共享变量可能会有多个线程在阻塞等待,所以具体唤醒哪一个是随机的。

而且,被唤醒的线程不能马上从wait方法返回并继续执行,因为每一个调用wait方法阻塞而释放锁的线程,都必须重新竞争获取共享变量的监视器锁才可以返回。

5、notifyAll()函数

顾名思义,这个方法会唤醒所有在该共享变量调用wait方法的线程。举个栗子,使用notify方法,则只会唤醒其中一个线程,执行结束。

public class NotifyTest {
    private static Object obj = new Object ();

    public static void main(String[] args) throws InterruptedException {
        Thread threadA = new Thread (new Runnable () {
            @Override
            public void run() {
                synchronized (obj) {
                    System.out.println ("this is threadA");
                    try {
                        System.out.println ("threadA begin wait");
                        obj.wait ();
                        System.out.println ("threadA end wait");
                    } catch (InterruptedException e) {
                        e.printStackTrace ();
                    }
                }
            }
        });
        Thread threadB = new Thread (new Runnable () {
            @Override
            public void run() {

                synchronized (obj) {
                    System.out.println ("this is threadB");
                    try {
                        System.out.println ("threadB begin wait");
                        obj.wait ();
                        System.out.println ("threadB end wait");
                    } catch (InterruptedException e) {
                        e.printStackTrace ();
                    }
                }
            }
        });
        Thread threadC = new Thread (new Runnable () {
            @Override
            public void run() {
                synchronized (obj) {
                    System.out.println ("threadC start notify");
                    obj.notify ();
                    //obj.notifyAll ();
                }
            }
        });

        threadA.start ();
        threadB.start ();
        Thread.sleep (1000);
        threadC.start ();

        threadA.join ();
        threadB.join ();
        System.out.println ("main over");
    }
}



输出:
this is threadA
threadA begin wait
this is threadB
threadB begin wait
threadC start notify
threadA end wait

如果线程C唤醒使用obj.notifyAll()方法,则会唤醒所有线程

this is threadA
threadA begin wait
this is threadB
threadB begin wait
threadC start notify
threadB end wait
threadA end wait
main over

注意:在共享变量上调用notifyAll方法只会唤醒调用这个方法之前调用了wait系列方法而被放入共享变量等待集合的线程,如果在notifyAll方法只会调用wait方法的线程,则不会被唤醒。 

 比如注释上面代码中线程C前面的休眠1秒时间,这个时候ABC三个线程的调用顺序就不一定了,所以就会出现线程C在其他线程之前执行的情况,那这样就不会唤醒后面执行的线程。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值