liangcmwn / docs

mydocs on git
0 stars 0 forks source link

sleep/wait/notify/notifyAll #17

Open liangcmwn opened 4 years ago

liangcmwn commented 4 years ago
liangcmwn commented 4 years ago

sleep和wait

sleep和wait都可以让线程阻塞,也都可以指定超时时间,甚至还都会抛出中断异常InterruptedException。

而它们最大的区别就在于,sleep时线程依然持有锁,别人无法进当前同步方法;wait时放弃了持有的锁,其它线程有机会进入该同步方法。多次提到同步方法,因为wait必须在synchronized同步代码块中,否则会抛出异常IllegalMonitorStateException,notify也是如此,可以说wait和notify是就是为了在同步代码中做线程调度而生的。

下面一个简单的例子展现sleep和wait的区别:

// 日志行号记录
    private AtomicInteger count = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Main main = new Main();
        // 开启两个线程去执行test方法
        new Thread(main::test).start();
        new Thread(main::test).start();
    }

    private synchronized void test() {
        try {
            log("进入了同步方法,并开始睡觉,1s");
            // sleep不会释放锁,因此其他线程不能进入这个方法
            Thread.sleep(1000);
            log("睡好了,但没事做,有事叫我,等待2s");
            //阻塞在此,并且释放锁,其它线程可以进入这个方法
            //当其它线程调用此对象的notify或者notifyAll时才有机会停止阻塞
            //就算没有人notify,如果超时了也会停止阻塞
            wait(2000);
            log("我要走了,但我要再睡一觉,10s");
            //这里睡的时间很长,因为没有释放锁,其它线程就算wait超时了也无法继续执行
            Thread.sleep(10000);
            log("走了");
            notify();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    // 打印日志
    private void log(String s) {
        System.out.println(count.incrementAndGet() + " "
                + new Date().toString().split(" ")[3]
                + "\t" + Thread.currentThread().getName() + " " + s);
    }
liangcmwn commented 4 years ago

notify和notifyAll

同样是唤醒等待的线程,同样最多只有一个线程能获得锁,同样不能控制哪个线程获得锁。

区别在于:


    private AtomicInteger count = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        Main main = new Main();
        // 开启两个线程去执行test方法
        for (int i = 0; i < 10; i++) {
            new Thread(main::testWait).start();
        }
        Thread.sleep(1000);
        for (int i = 0; i < 5; i++) {
            main.testNotify();
        }
    }

    private synchronized void testWait() {
        try {
            log("进入了同步方法,开始wait");
            wait();
            log("wait结束");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private synchronized void testNotify() {
        notify();
    }

    private void log(String s) {
        System.out.println(count.incrementAndGet() + " "
                + new Date().toString().split(" ")[3]
                + "\t" + Thread.currentThread().getName() + " " + s);
    }

例子中有10个线程在wait,但notify了5次,然后其它线程一直阻塞,这也就说明使用notify时如果不能准确控制和wait的线程数对应,可能会导致某些线程永远阻塞。

使用notifyAll唤醒所有等待的线程: 只需要调用一次notifyAll,所有的等待线程都被唤醒,并且去竞争锁,然后依次(无序)获取锁完成了后续任务。

liangcmwn commented 4 years ago

为什么wait要放到循环中使用

一些源码中出现wait时,往往都是伴随着一个循环语句出现的,比如:

private synchronized void f() throws InterruptedException {
    while (!isOk()) {
        wait();
    }
    System.out.println("I'm ok");
}

既然wait会被阻塞直到被唤醒,那么用if+wait不就可以了吗?其他线程发现条件达到时notify一下不就行了?

理想情况确实如此,但实际开发中我们往往不能保证这个线程被notify时条件已经满足了,因为很可能有某个无关(和这个条件的逻辑无关)的线程因为需要线程调度而调用了notify或者notifyAll。此时如果样例中位置等待的线程不巧被唤醒,它就会继续往下执行,但因为用的if,这次被唤醒就不会再判断条件是否满足,最终程序按照我们不期望的方式执行下去。