wait()方法的注意点

一、问题是什么?

这个问题是我昨天测试wait()方法的时候偶然发现的,即:

一个线程在同步块或者同步方法中使用同步对象调用 wait() 方法的时候,会出现另一个线程在同步块或者同步方法中不使用 notify() 方法,被 wait() 的线程就能自动被唤醒的现象。当然这个需要分两种情况,这个下面会具体说到

我花了半个下午加一个晚上的时间,查了很多资料,问了一些人,才勉强搞懂为什么会出现这种情况,在这里记录一下

二、问题回顾
1.

首先这是我昨天遇到的一个例子,我拿线程作为对象锁

class ThreadB2 extends Thread {
	
    /**
     * 线程 BBB 持有对象锁 this,即当前对象 threadB2
     */
    @Override
    public void run() {
        synchronized (this) {
            System.out.println(Thread.currentThread().getName() + " beg "
                    + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName() + " end "
                    + System.currentTimeMillis());
        }
    }
}

class ThreadA2 extends Thread {

    private ThreadB2 threadB2;

    public ThreadA2(ThreadB2 threadB2) {
        this.threadB2 = threadB2;
    }
	
    /**
     * 线程 AAA 持有对象锁 threadB2
     */
    @Override
    public void run() {
        synchronized (threadB2) {
            System.out.println(Thread.currentThread().getName() + " beg "
                    + System.currentTimeMillis());
            try {
                System.out.println("wait之前:" + threadB2.isAlive());
                threadB2.wait();
                System.out.println("wait之后:" + threadB2.isAlive());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " end "
                    + System.currentTimeMillis());
        }
    }
}

public class Run2 {

    public static void main(String[] args) {
        ThreadB2 threadB2 = new ThreadB2();
        threadB2.setName("BBB");
        ThreadA2 threadA2 = new ThreadA2(threadB2);
        threadA2.setName("AAA");

        threadA2.start();
        threadB2.start();
    }

}

结果则是有两种情况,出现最多的一种,也是我觉得最不可思议的一种,是以下这样:

AAA beg 1540790434044
wait之前:true
BBB beg 1540790434044
BBB end 1540790434044
wait 之后false
AAA end 1540790434044

当然,也有小部分情况,是下面这样的,也是我觉得”应该就是这种结果“的情况

BBB beg 1540790728351
BBB end 1540790731004
AAA beg 1540790738781
wait之前:false

此时控制台还是运行的状态,且永远不会停止

对于第二种情况我是认可的,表明线程 BBB 是先执行 run 方法,且先持有对象锁 threadB2 的,只有等到线程 BBB 执行完 run() 方法里面的代码,退出同步块,才会释放自身持有的对象锁 threadB2;然后线程 AAA 拿到对象锁 threadB2,对象锁 threadB2 执行 wait() 方法,表明持有该对象锁的线程 AAA 释放该对象锁,此时因为线程 BBB 已经执行完了(线程 BBB 的 isAlive 输出为 false),没有线程调用 notifyAll() 方法来唤醒线程 AAA,导致线程 AAA 就这样永远的等待下去

第一种情况我觉得很奇怪,同样在线程 AAA 中使用对象锁 threadB2 执行 wait 方法,导致持有该对象锁的线程 AAA 释放对象锁,线程 AAA 进入等待状态,按理说,应该是不能执行 wait() 后面的代码的,但结果告诉我们,执行了,这也是最使我不解的一个地方:既然没有使用notify()或者notifyAll()方法,那么被进入等待状态的线程AAA又是被哪个线程唤醒的呢?还是自己唤醒自己?

同时还可以发现,第一种情况,在 wait() 方法之前,线程 BBB 还是活着的,第二种情况,线程 BBB 因为是先执行完的,所有在 wait() 方法之前就是死的了

在查询了很多篇博客和在论坛里询问了一些人后,得到了下面的解释:

线程 AAA 以线程 BBB 的对象作为锁对象,如果线程 BBB 在线程 AAA 进入等待状态之前就已经死亡,那么线程 AAA 将永远等待下去;反之,如果线程 BBB 在线程 AAA 进入等待状态之前没有死亡,那么线程 AAA 在线程 BBB 执行完之后会被自动唤醒。如果要问是线程 AAA 是怎么被唤醒的,那么我猜测是线程 AAA 持有的对象锁 threadB2 调用 threadB2.notifyAll() 方法将线程 AAA 唤醒的

其实,这里的猜测是有根据的,因为该段代码和 join() 方法的源码很类似,join() 底层使用的是 wait() 方法,我们看一下 join() 底层的实现

public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    //语句1
    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

注意这里的语句1,是不是和我例子中使用 wait() 方法类似,只是我没有判断当前的锁对象对应的锁是否是活着的而与,当我把 wait() 方法改成 join() 方法,也能执行,输出上面的第一种情况,其实 join() 方法的原理就是我上面对第一种情况的分析,具体对源码的分析,可以看我之后的文章

2.

为了加以区分,我还想到了另外一种情况。这和第一个例子不太一样,不同之处在于此时两个线程都是拿同一个 Object 的对象作为对象锁的,而第一个例子,是一个线程拿另一个线程的对象对象作为对象锁,这两者是不同的,我们来分析一下这个例子

class ThreadA extends Thread {

    private Object lock;

    public ThreadA(Object lock) {
        this.lock = lock;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " beg "
                    + System.currentTimeMillis());

            System.out.println(Thread.currentThread().getName() + " end "
                    + System.currentTimeMillis());
        }
    }
}

class ThreadB extends Thread {

    private Object lock;
    private ThreadA threadA;
	
    //这里的对象锁是 lock,穿入的 ThreadA 只是想看一下线程 AAA 在 wait 之前是否还存活
    public ThreadB(Object lock, ThreadA threadA) {
        this.lock = lock;
        this.threadA = threadA;
    }

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(Thread.currentThread().getName() + " beg "
                    + System.currentTimeMillis());
            try {
                System.out.println("wait之前:" + threadA.isAlive());
                lock.wait();
                System.out.println("wait之后:" + threadA.isAlive());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " beg "
                    + System.currentTimeMillis());
        }
    }
}

public class Run {

    public static void main(String[] args) {
        Object lock = new Object();
        ThreadA threadA = new ThreadA(lock);
        threadA.setName("AAA");
        ThreadB threadB = new ThreadB(lock,threadA);
        threadB.setName("BBB");

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

}

最终的结果只有一种:

AAA beg 1540814622564
AAA end 1540814622564
BBB beg 1540814622564
wait之前:true

此时控制台还是显示运行状态,且永远不会停止

这种结果对应第一个例子的第二个结果,线程 AAA 在线程 BBB 进入等待状态(lock.wait())之前就已经执行完并死亡了(wait 之前输出 false),当线程 BBB 释放自己的对象锁,没有其他线程会将线程 BBB 唤醒,此时线程 BBB 就会永远的等待下去

三、总结

基本算是搞懂了,还是要感谢这个过程中帮助过我的人,无论是网上的博客,还是知识星球里替我解答疑惑的人,感谢你们。其实不懂的这个过程确实很痛苦,尤其是与你之前所学知识相违背的情况下,但是总能解决的,前提是,你必须能和这个问题“耗下去”

  • 8
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值