深入理解Java中的wait() 方法

2022-10-19 08:59:21

使用场景

当某个线程获取到锁后,发现当前还不满足执行的条件,就可以调用对象锁的wait方法,进入等待状态。

直到某个时刻,外在条件满足了,就可以由其他线程通过调用notify()或者notifyAll()方法,来唤醒此线程。

这篇文章将侧重于讨论wait()方法对于线程状态的影响,以及被唤醒后线程的状态变更。

条件

只有已经获取锁的线程,才可以调用锁的wait方法,否则会抛出异常IllegalMonitorStateException。

比如下面的代码,A获得了锁后,主动调用wait方法释放锁和CPU资源,然后就陷入了阻塞状态。

主线程在没获得锁的情况下,直接调用wait方法,会抛出异常。

@Log4j
public class WaitTest {

    public static void main(String[] args) {
        Object lock = new Object();
        Thread threadA = new Thread(() -> {
            synchronized (lock) {
                log.info("获取了锁");
                try {
                    log.info("休眠一会儿");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("调用wait..");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                log.info("被唤醒");
            }
        }, "A");
        threadA.start();

        lock.notify();

    }
}

现在我们创建一个B线程,抢占锁后,再调用notify方法。

@Log4j
public class WaitTest {

    public static void main(String[] args) {
        Object lock = new Object();
        Thread threadA = new Thread(() -> {
            synchronized (lock) {
                log.info("获取了锁");
                try {
                    log.info("休眠一会儿");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("调用wait..");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                log.info("被唤醒");
            }
        }, "A");
        threadA.start();

        Thread threadB = new Thread(()->{
            synchronized (lock) {
                log.info("获得了锁");

                log.info("叫醒A");
                lock.notify();
            }
        }, "B");
        threadB.start();
    }
}

可以唤醒A方法。

线程状态变化

首先要明确一点,线程正常运行时的状态时Runnable,调用Wait方法后,变为Waiting状态。

举例说明:

@Log4j
public class WaitTest {

    public static void main(String[] args) {
        Object lock = new Object();
        Thread threadA = new Thread(() -> {
            synchronized (lock) {
                log.info("获取了锁");
                try {
                    log.info("休眠一会儿");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("调用wait..");
                try {
                    log.info("wait前的线程状态" + Thread.currentThread().getState());
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                log.info("被唤醒");
            }
        }, "A");
        threadA.start();

        try {
            TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("线程A wait后的状态" + threadA.getState());

    }
}

运行结果:

现在的问题在于,主动wait的线程,被唤醒后,状态一定会由WAITING变为RUNNABLE吗?

我们再做一个测试。

先概括一下测试内容,A线程wait。2秒后,启动B线程,B线程再去唤醒A线程。记录唤醒前后的状态。

@Log4j
public class WaitTest {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread threadA = new Thread(() -> {
            synchronized (lock) {
                log.info("获取了锁");
                try {
                    log.info("休眠一会儿");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("调用wait..");
                try {
                    log.info("wait前的线程状态" + Thread.currentThread().getState());
                    lock.wait();
                    log.info("wait后的线程状态" + Thread.currentThread().getState());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                log.info("被唤醒");
            }
        }, "A");
        threadA.start();

        TimeUnit.SECONDS.sleep(2);

        Thread threadB = new Thread(()->{
            synchronized (lock) {
                log.info("获得了锁");
                log.info("叫醒A前,A的状态" + threadA.getState());
                log.info("叫醒A");
                lock.notify();
            }
        }, "B");
        threadB.start();
    }
}

运行结果

和我们的预测是一致的。

但实际上,这段代码还是有点问题的。上述代码中的B线程调用notify方法后,立刻释放了锁,但假如B调用notify后,发现自己还有很多任务没有完成,不释放锁,A线程的状态会怎么变化呢

修改后的代码

@Log4j
public class WaitTest {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread threadA = new Thread(() -> {
            synchronized (lock) {
                log.info("获取了锁");
                try {
                    log.info("休眠一会儿");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("调用wait..");
                try {
                    log.info("wait前的线程状态" + Thread.currentThread().getState());
                    lock.wait();
                    log.info("wait后的线程状态" + Thread.currentThread().getState());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                log.info("被唤醒");
            }
        }, "A");
        threadA.start();

        TimeUnit.SECONDS.sleep(2);

        Thread threadB = new Thread(()->{
            synchronized (lock) {
                log.info("获得了锁");
                log.info("叫醒A前,A的状态" + threadA.getState());
                log.info("叫醒A");
                lock.notify();
                log.info("发现还有很多事需要做,先不释放锁");
                log.info("我在做事过程中,A的状态: " + threadA.getState());
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("我做完了");
            }
        }, "B");
        threadB.start();
    }
}

测试结果

结果中出现了BLOCKED。

也就是说B调用notify方法时,并不会把线程的控制权立刻交出去,A被唤醒后,也不会将状态变为RUNNABLE。

而是先变为BLOCKED,然后参与锁的竞争,竞争成功重新获得锁后,才会向下执行。

Java中,每一个对象都会有一个对应的Monitor锁,Monitor维护着EntrySet和WaitSet。

线程阻塞时,会被放入EntrySet,对应的状态是BLOCKED状态,

线程调用wait方法后,会被加进WaitSet中,对应的状态是WAITING、TIMED_WAITING。

上面我们也分析了,线程“被唤醒”和“获得锁”是两个过程,被唤醒的线程需要重新参与锁竞争。

如果这么理解的话,线程应该是从WaitSet中苏醒后,又被加进了EntrySet队列(因为状态变为了BLOCKED)。

但也有人觉得,从效率角度看的话,jvm没有必要做这种“队列间的移动” (观点来源:https://www.zhihu.com/question/64725629/answer/224047354)

但我没发现答主摆出了什么比较靠谱的证据,所以jvm到底怎么实现的还有待考证。

有限等待

wait方法是可以给他传一个时间参数进去的,是一种自动唤醒机制:在指定时间内,如果没有其他线程唤醒自己,则主动唤醒自己。

如果传0或者不传,则表示永久等待

@Log4j
public class WaitTest {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread threadA = new Thread(() -> {
            synchronized (lock) {
                log.info("获取了锁");
                try {
                    log.info("休眠一会儿");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("调用wait..");
                try {
                    log.info("wait前的线程状态" + Thread.currentThread().getState());
                    lock.wait(1);
                    log.info("wait后的线程状态" + Thread.currentThread().getState());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                log.info("被唤醒");
            }
        }, "A");
        threadA.start();

    }
}

结果

这是常规使用,但是按照我们前面的分析,A被唤醒后,会继续参加锁的竞争,假设在A在 wait( t )的过程中(也就是t时间之内),有其他线程抢占了线程,A还能继续往下走吗。

为了让B线程占用锁,我们把A的wait时间设置为100毫秒。

跑一下测试代码

@Log4j
public class WaitTest {

    public static void main(String[] args) throws InterruptedException {
        Object lock = new Object();
        Thread threadA = new Thread(() -> {
            synchronized (lock) {
                log.info("获取了锁");
                try {
                    log.info("休眠一会儿");
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info("准备调用wait..");
                try {
                    log.info("wait前的线程状态" + Thread.currentThread().getState());
                    lock.wait(100);
                    log.info("wait后的线程状态" + Thread.currentThread().getState());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                log.info("被唤醒");
            }
        }, "A");
        threadA.start();

        Thread threadB = new Thread(()->{
            synchronized (lock) {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                log.info("线程A的状态" + threadA.getState());

                while (true) {

                }
            }
        }, "B");
        threadB.start();
    }
}

运行结果:

即使100毫秒过了,A也不能继续向下执行,因为他只是被唤醒了,但并没有成功得获取锁,所以会进入BLOCKED状态。

  • 作者:小楼夜听雨QAQ
  • 原文链接:https://blog.csdn.net/qq_37855749/article/details/117073990
    更新时间:2022-10-19 08:59:21