wait和notify

wait和notify翻译过来的意思是等待和通知机制,和join的用途相似,因为多个线程之间是随机调度,抢占式执行的。

引入wait和notify,是为了能够从应用层面上,干预到多个不同线程代码的执行顺序,这里的干预,并不影响系统的线程调度策略(系统内核中的线程调度任然是无需的)。

而是在应用程序代码中,让后执行的线程,主动放弃被调度的机会,就可以让执行的线程,先把对应的代码执行完了。

举个例子:

有1、2、3、4、5号人去ATM机(ATM机上有一个锁,有人在办业务的时候就锁上)办业务,1号兄弟来的早,排在第一位,当1号进去取钱的时候,发现ATM机里面没有钱,所以1号兄弟就会先出来,等到有钱的时候再进去取,此时,其他人就会去竞争这个锁,争取进到ATM中办理他们的业务,但是,刚刚释放锁的1号兄弟,他仍然会参与到锁的竞争中,此时,完全有可能,1号兄弟又重新拿到了锁,这样地话,就会导致一个情况:1号大兄弟,反复获取锁,但是又无法完成实质性地逻辑(反复横跳),其他的人,又无法获取到锁,这个情况,被称为“线程饿死”,也称为“线程饥饿”。

线程饥饿这样的问题,是概率性事件,不像死锁那样,一旦出现之后,程序就一定会挂掉,但是它也会极大地影响其他线程地运行。

像上述ATM发生的情况概率还是挺高的。

1号线程拿到了锁,处于RUNNABLE状态,其他线程因为锁冲突出现阻塞,处于BLOCKED状态,近水楼台先得月,其它线程,需要系统唤醒之后,才能参与到锁竞争中,1号线程是不用唤醒得,他直接就能参与竞争(当然这里谁能竞争到这把锁,也是一个复杂的过程)。

这种情况,也是一种bug,虽然没有死锁那么严重,但是它也是需要处理的……

这里需要,1号大兄弟(线程),当他发现自己要执行的逻辑,前提条件不具备(ATM里面没钱),在当前情况下,1号大兄弟(线程)就应该主动放弃去对锁的竞争/主动放弃去CPU上参与调度执行(进入阻塞),一直等到,条件具备了(其他兄弟(线程)给ATM里存钱了),此时再退出阻塞,参与到锁竞争中。

此时就可以使用wait和notify了。

当1号大兄弟,发现当前他要执行的逻辑的前提条件不具备,则wait(放弃去ATM里取钱),当其他线程让条件满足之后(往ATM机里存钱了之后),再通过notify唤醒1号兄弟。

1、线程之间的调度是无序的。

2、上面的代码,1号大兄弟,当前要完成的工作是想去ATM中取钱,进入ATM要加锁,但是取钱的前提是ATM里面有钱。如果有钱就取走然后break跳出循环,取钱操作结束,如果没钱,就需要再试亿次,由于1号兄弟也不知道啥时候有钱,所以它只能反复地进行尝试……

此时如果ATM是没钱的,此时1号大兄弟,就会反复地尝试加锁解锁,这个过程中,其他线程大概率很难再继续获取到锁了。

所以我们应该改成:

wait在内部做了3件事:

1、释放锁

2、进入阻塞等待

(到了这里,其他线程就有机会获取到锁了)

3、当其他线程调用notify时,wait就会解除阻塞,并且重新获取到锁。

wait和join

join是需要等到调用它的线程执行完,当前线程才能继续执行。

wait则是等待另一个线程通过notify进行通知(不要求另一个线程执行完)。

wait的调用

和sychronized一样随便一个对象,都可以进行wait(),因为wait()是Object中的方法。

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

但是,直接调用wait()会出现异常:这里的异常信息翻译一下:不合法监视器(锁)异常。

我们刚才说了wait要做三件事:1、释放锁 2、进入阻塞等待 3、当其他线程调用notify的时候,wait就会解除阻塞,并且重新获取到锁。 

也就是说,wait一进来会先释放掉锁,释放锁的前提是,wait先拿到了锁(wait必须放到synchronized里面使用,且调用wait的对象,必须和synchronized中的锁对象是一致的)。

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

为了保证notify解的锁是刚才o的锁,在调用notify的时候也需要使用同样的锁对象(notify也需要搭配加锁使用), 在wait被唤醒之后,重新获取到的锁,仍然还是获取到o锁。

wait演示

代码如下:

public class ThreadDemo24 {
    public static void main(String[] args) throws InterruptedException {
        //wait:1、释放锁
        //2、阻塞等待
        //3、当其他线程调用notify时,重新获取到锁
        Object object = new Object();
        synchronized (object){
            System.out.println("wait之前:");
            object.wait();//有锁才能使用wait,锁对象必须和调用wait的对象相同,其他线程调用notify也必须是当前对象
            System.out.println("wait之后:");
        }
    }
}

此时只会打印wait之前,wait之后会由于代码陷入阻塞而无法打印: 

打开jconsole进行观察:补充:wait()、sleep()和join()都可能会被intterupt提前唤醒,所以需要处理异常。

wait放到synchronized里面,是因为它要释放锁,所以要先加上锁。而notify其实可以不妨到synchronized中,不需要提前加锁(但是Java中特别约定要把notify放到synchronized里面)(操作系统的原生API中也有wait和notify,系统原生的wait需要先加锁,notify不需要先加锁)。

wait()和notify()的演示

代码如下:

ublic class ThreadDemo25 {
    public static void main(String[] args) {
        //需要有一个统一的锁对象进行加锁,wait,notify
        Object locker = new Object();
        Thread t1 = new Thread(()->{
            synchronized (locker){
                try {
                    System.out.println("t1 wait之前");
                    locker.wait();
                    System.out.println("t1 wait之后");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread t2 = new Thread(()->{
            try {
                Thread.sleep(5000);//确保先wait再notify
                synchronized (locker) {
                    System.out.println("t2 notify之前");
                    locker.notify();//java规定notify也需要放在锁里面
                    System.out.println("t2 notify之后");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        t1.start();
        t2.start();
    }
}

注意:t2线程中的sleep一定要放在synchronized的外面,否则,由于随机调度,t1 t2的调度顺序不确定,就有可能t2先拿到锁,此时t1还没执行wait,t2 就先notify了,结果就不符合预期了。(需要保证,代码先wait再notify)如果先notify,那么t1的wait就无法被唤醒了。

上述代码的执行过程:t1执行起来之后,会先拿到锁,并且打印“wait之前0”,然后进入wait方法(释放锁,然后进入阻塞等待)。t2执行起来时候,先sleep(5000)(这个时间里面t1会先拿到锁),t2sleep结束之后,由于t1的wait已经释放了锁,t2就能拿到锁。接下来t2会打印“notify之前0”,然后执行notify操作,这个操作能够唤醒t1(此时t1就从WAITING状态变为RUNNABLE状态了),但是由于t2此时还没有释放锁,t1被唤醒之后,会因为锁竞争导致阻塞,所以t2会先打印“notify之后”,然后释放锁,t1重新获取到锁,打印“t1wait之后”。

补充:

1、wait提供了三个版本

第一个版本,wait是“死等”,即如果没有其他线程中调用notify方法的话,该线程就会一直等下去。第二个版本,是带有时间的等,单位是ms,如果这个时间内,没有其他线程进行notify操作唤醒它,那当前线程也就不等了,继续执行。第三个版本则是时间更精确地等。

一般使用第二个版本。 

2、wait和notify彼此之间是通过object对象联系起来地

object1.wait()<=>object2.notify()此时是无法唤醒的!!!需要两个对象一致才能够唤醒!!!

3、notifyAll 唤醒这个对象上所有等待的线程

假设有很多线程,都使用同一个对象wait,针对这个对象进行notifyAll,此时就会全部被唤醒。

但是,这些线程在wait返回的时候,要重新获取锁,就会因为锁竞争,使得这些线程是一个个串行执行(谁先拿到锁,谁后拿到锁,也是不确定的),相比之下,还是更倾向于使用notify。

4、wait和sleep的区别:

wait提供了一个带有时间的版本,sleep也能指定时间,都是时间到,就可以解除阻塞,继续执行了。wait和sleep都可以被提前唤醒(即使时间没有到,也可以被提前唤醒),wait提供notify唤醒,sleep提供interrupt唤醒。

使用wait:是不知道要等多长时间的前提下使用,所谓的超时时间,其实是用来“兜底的” (大多数情况下,都是时间没到就已经被唤醒了)

使用sleep:一定是知道要等多长时间的前提下使用的,虽然能被提前唤醒,但是通过异常唤醒,这个操作不应该作为“正常的业务流程”

所以不能使用sleep来代替wait

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值