← 返回首页
JavaSE系列教程(六十四)
发表时间:2020-02-11 00:31:55
讲解wait和notify.

首先,需要说明的是,wait和notify方法都是Object的实例方法,要执行这两个方法,有一个前提就是,当前线程必须获其对象的monitor(俗称“锁”),否则会抛出IllegalMonitorStateException异常,所以这两个方法必须在同步块代码里面调用。

1.wait和notify

wait:导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法,该方法是会释放锁的。 notify:唤醒正在等待对象监视器的单个线程。 如果任何线程正在等待这个对象,其中一个被选择被唤醒。 选择是任意的,并且由实施的判断发生。 线程通过调用wait方法之一等待对象的监视器。该方法不释放锁的。

实例: 创建两个线程交替打印 ^^ 和@@表情,每个表情连续打印10遍后进行交替输出且每个表情共打印50遍。 这里涉及到两个打印线程的同步问题。

class Emotion implements Runnable {

    private Object object;//要上锁的对象。

    private String type; //表情的类型

    public Emotion(Object object, String type) {
        this.object = object;
        this.type = type;
    }

    @Override
    public void run() {

        synchronized (object) {
            if ("^_^".equals(type)) {
                for (int i = 1; i <= 50; i++) {
                    System.out.println("^_^");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    if (i % 10 == 0 && i<50) {
                        object.notify();
                        try {
                            object.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    if(i==50){
                        object.notify(); //退出循环前,仅仅需要notify
                    }
                }

            } else {
                for (int i = 1; i <= 50; i++) {
                    System.out.println("@_@");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    if (i % 10 == 0 && i<50) {
                        object.notify();
                        try {
                            object.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }

                    if(i==50){
                        object.notify(); //退出循环前,仅仅需要notify
                    }
                }

            }
        }

    }
}

public class EmotionDemo {

    public static void main(String[] args) {
        Object object = new Object(); //上锁的对象。

        Emotion smile = new Emotion(object, "^_^");
        Emotion amaze = new Emotion(object, "@_@");

        Thread smileThread = new Thread(smile);
        Thread amazeThread = new Thread(amaze);

        smileThread.start();
        amazeThread.start();


    }
}

执行结果:
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
^_^
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@
@_@

2.wait和sleep的区别

sleep() 方法 wait() 方法
sleep()使当前线程进入停滞状态(阻塞当前线程),让出CUP的使用、目的是不让当前线程独自霸占该进程所获的CPU资源,以留一定时间给其他线程执行的机会; 当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,让出CPU的使用,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long timeout)超时时间到后还需要返还对象锁);其他线程可以访问;
sleep()是Thread类的Static(静态)的方法;因此他不能改变对象的机锁,所以当在一个Synchronized块中调用Sleep()方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。 wait和notify是Object类的方法,wait()使用notify或者notifyAlll或者指定睡眠时间来唤醒当前等待池中的线程。
在sleep()休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。 wiat()必须放在synchronized block中,否则会在program runtime时扔出”java.lang.IllegalMonitorStateException“异常。

所以sleep()和wait()方法的最大区别是:sleep()睡眠时,保持对象锁,仍然占有该锁; 而wait()睡眠时,释放对象锁。 但是wait()和sleep()都可以通过interrupt()方法打断线程的暂停状态,从而使线程立刻抛出InterruptedException(但不建议使用该方法)。

3.notify和notifyAll的区别

1).如果线程调用了对象的 wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁。

2).当有线程调用了对象的 notifyAll()方法(唤醒所有 wait 线程)或 notify()方法(只随机唤醒一个 wait 线程),被唤醒的的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。也就是说,调用了notify后只要一个线程会由等待池进入锁池,而notifyAll会将该对象等待池内的所有线程移动到锁池中,等待锁竞争.

3).优先级高的线程竞争到对象锁的概率大,假若某线程没有竞争到该对象锁,它还会留在锁池中,唯有线程再次调用 wait()方法,它才会重新回到等待池中。而竞争到对象锁的线程则继续往下执行,直到执行完了 synchronized 代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

所谓唤醒线程,另一种解释可以说是将线程由等待池移动到锁池,notifyAll调用后,会将全部线程由等待池移到锁池,然后参与锁的竞争,竞争成功则继续执行,如果不成功则留在锁池等待锁被释放后再次参与竞争。而notify只会唤醒一个线程。

4.线程同步案例

小张和小李共用一个套间,套间里面只有一个卫生间。某天小张先起床后,去卫生间嘘嘘,小李在门口等待,小张嘘嘘出来后,小李进去嘘嘘,小张在门口等待刷牙,小李嘘嘘出来后,小张进去刷牙,小李门口等待,小张刷完牙出来,小李进去刷牙,故事结束。

如果使用线程同步来描述上述故事呢?

1.使用同步代码块实现

package tolietdemo;

//租客类,客户类
class Customer implements Runnable {

    private Toliet toliet;

    public Customer(Toliet toliet) {
        this.toliet = toliet;
    }

    @Override
    public void run() {
        //开始来描述这个故事....
        //怎么写呢?
        try {
            //注意:一定要给卫生间先上锁,保证在一个同步代码块中。。。
            synchronized (toliet) {
                //判断一下是否是小张的线程
                if ("小张".equals(Thread.currentThread().getName())) {
                    //先嘘嘘...
                    toliet.xuxu();
                    //嘘嘘完了之后,释放卫生间的锁。。。。
                    toliet.notifyAll(); //唤醒小李进入卫生间解小便...
                    toliet.wait(); //释放锁。//小张进入到等待队列中..

                    toliet.brushTeeth();
                    toliet.notifyAll();//唤醒小李进入卫生间刷牙...

                } else {
                    toliet.xuxu(); //小李可以嘘嘘了。。。。
                    toliet.notifyAll();//唤醒小张去刷牙...
                    toliet.wait();
                    toliet.brushTeeth();
                    //故事就结束了。
                }
            }

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}


//卫生间类
class Toliet {
    //定义两个方法。
    //解小便的方法...
    public synchronized void xuxu() {
        try {
            System.out.println(Thread.currentThread().getName() + "正在卫生间嘘嘘...");
            Thread.sleep(50);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    //刷牙
    public synchronized void brushTeeth() {
        try {
            System.out.println(Thread.currentThread().getName() + "正在卫生间刷牙...");
            Thread.sleep(50);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}

public class TolietDemo {

    public static void main(String[] args) {

        Toliet toliet = new Toliet();

        Customer zhang = new Customer(toliet);
        Customer li = new Customer(toliet);  //这里一定是同一个卫生间对象。
        Thread zhangThread = new Thread(zhang, "小张");
        Thread liThread = new Thread(li, "小李");

        //小张先起床,意味:小张线程先执行。
        //如何保证小张线程先执行呢。
        zhangThread.start();
        //采用卑鄙无耻的手段。
        try {
            Thread.sleep(50);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        liThread.start();
    }
}

运行结果:

小张正在卫生间嘘嘘...
小李正在卫生间嘘嘘...
小张正在卫生间刷牙...
小李正在卫生间刷牙...

2.使用Lock实现

package lockdemo;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

//租客类,客户类
class Customer implements Runnable {

    private Toliet toliet;
    private Lock lock;

    public Customer(Toliet toliet) {
        this.toliet = toliet;
        lock = new ReentrantLock();
    }

    @Override
    public void run() {
        //开始来描述这个故事....
        //怎么写呢?
        try {
            //2秒钟内,尝试获得锁。
            if (lock.tryLock(2, TimeUnit.SECONDS)) {
                //判断一下是否是小张的线程
                if ("小张".equals(Thread.currentThread().getName())) {
                    toliet.xuxu();
                    //释放锁。
                    //注意:释放的不是卫生间的锁。toliet.wait();
                    lock.unlock();
                    //把休眠动作,一定要放到unlock之后,unlock不是阻塞
                    //为了在unlock之后,运行CPU时间片切换到其它线程。
                    Thread.sleep(50);
                    if (lock.tryLock(5, TimeUnit.SECONDS)) {
                        //又获得了锁。
                        toliet.brushTeeth();//刷牙
                        lock.unlock();
                    }
                } else {
                    toliet.xuxu();
                    lock.unlock();
                    Thread.sleep(50);
                    if (lock.tryLock(5, TimeUnit.SECONDS)) {
                        //又获得了锁。
                        toliet.brushTeeth();//刷牙
                        lock.unlock();
                    }
                }
            }

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}


//卫生间类
class Toliet {
    //定义两个方法。
    //解小便的方法...
    public void xuxu() {
        System.out.println(Thread.currentThread().getName() + "正在卫生间嘘嘘...");
    }

    //刷牙
    public void brushTeeth() {
       System.out.println(Thread.currentThread().getName() + "正在卫生间刷牙...");
    }
}

public class TolietDemo {

    public static void main(String[] args) {

        Toliet toliet = new Toliet();
        Customer c = new Customer(toliet);
        Thread zhangThread = new Thread(c, "小张");
        Thread liThread = new Thread(c, "小李");

        //小张先起床,意味:小张线程先执行。
        //如何保证小张线程先执行呢。
        zhangThread.start();
        //采用卑鄙无耻的手段。
        try {
            Thread.sleep(50);
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        liThread.start();
    }
}

运行结果:

小张正在卫生间嘘嘘...
小李正在卫生间嘘嘘...
小张正在卫生间刷牙...
小李正在卫生间刷牙...