Java线程阻塞与唤醒

    Thread.suspend和Thread.resume因为容易导致死锁,很早以前就被标记为@deprecated,不建议使用了。我们把线程使用互斥锁访问的共享资源叫做临界资源,把加锁和解锁之间的代码叫做临界区。如果当前线程获取了监视器并进入了临界区,这时当前线程被调用suspend方法,当前线程不会释放持有的监视器。如果使用resume方法唤醒线程前需要获取监视器,那么死锁就会发生。当然如果合理Thread.suspend和Thread.resume,可以避免死锁。不过,对于线程阻塞和唤醒,我们有更好的选择。

    下面的例子中,blinker线程负责控制控件闪烁,通过鼠标点击可以改变blinker线程状态。我们通过一个变量来标志线程状态。我们对mousePressed的方法调用没有加锁,因此可能出现并发问题。如果线程A和B同时进入方法体,线程A已经到了阶段3,线程B刚到阶段1,blinker.resume或blinker.suspend会被执行两次。

    private volatile boolean threadSuspended;

    public void mousePressed(MouseEvent e) {
        // 阶段1:刚进入方法体
        e.consume();
        // 阶段2:判断threadSuspended之前
        if (threadSuspended)
            blinker.resume(); // resume
        else
            blinker.suspend();  // suspend之后会继续占用对象监视器
        // 阶段3:修改threadSuspended之前
        threadSuspended = !threadSuspended; 
    }

    为了解决并发问题,假设方法mousePressed被synchronized修饰,blinker.resume挂起时,会继续占用对象监视器,之后调用mousePressed方法的线程都会阻塞,出现死锁。如果对mousePressed方法加锁,由于Thread.suspend调用之后会继续占用对象的监视器,导致死锁。如下所示:

    private volatile boolean threadSuspended;

    public synchronized void mousePressed(MouseEvent e) {
        e.consume();
        if (threadSuspended)
            blinker.resume(); // resume
        else
            blinker.suspend();  // suspend之后会继续占用对象的监视器

        threadSuspended = !threadSuspended; 
    }

 

     使用Object.wait和Object.notify可以实现线程的阻塞与唤醒,同时避免出现死锁的情况。因为Object.wait在阻塞线程的时候,会释放线程占用的监视器。调用对象的wait或者notify方法之前,都需要获得这个对象的监视器。获得对象监视器的方法有:1. 执行对象的synchronized方法;2. 用当前对象同步的同步块 synchronized(object) {};3. 对于类,通过执行类的静态方法获取。

  使用wait/notify来实现线程的阻塞与唤醒,如下所示:

    public synchronized void mousePressed(MouseEvent e) { // blinker线程内部,使用blinker线程对象同步
        e.consume();

        threadSuspended = !threadSuspended;

        if (!threadSuspended)
            notify();
    }

    然后在线程的run方法中,循环检查状态。

    // 用volatile修饰threadSuspended变量,保证多个线程间的内存可见性。
    private volatile boolean threadSuspended;

    public void run() {
        while (true) {
            try {
                if (threadSuspended) { 
                    // 只在threadSuspended为true时请求加锁,减少加锁
                    synchronized(this) {
                        while (threadSuspended)
                        // 线程可能被假唤醒,所以用while而不是if
                            wait();
                    }
                }
            } catch (InterruptedException e){
            }
            repaint();
        }
    }

    在run方法中读threadSuspended变量和在mousePressed方法中修改它,都会先获得同一个监视器。

    wait方法放在while循环中,是因为线程因为"no reason"可能被唤醒,这是官方文档的提法,有一种可能的原因是,notify被调用后,等待队列头的线程被唤醒,此时又有新的线程通过非公平的策略(自旋)争夺到监视器。对于threadSuspended变量的修改实现了同步,避免了并发问题。由于Thread.wait会释放占有的锁,因此不会出现死锁。

    上面的例子,只会有一个线程(blinker)会wait,而会有多个线程调用notify。wait/notify可以允许多个线程等待同一个对象的监视器。

------------------------------分割线----------------------------

虚假唤醒:

虚假唤醒描述了某些多线程API(如POSIX线程和Windows API)提供的条件变量使用的复杂性。即使条件变量似乎已经从等待线程的角度收到信号,等待的条件仍然可能是错误的。 造成这种情况的原因之一是虚假的唤醒; 也就是说,即使没有线程通知条件变量,线程也可能被从其等待状态唤醒。 为了正确,有必要在线程完成等待之后验证条件是否确实为真。 因为虚假唤醒可以重复发生,所以通过在条件为真时终止循环来实现。

Linux中的pthread_cond_wait()函数是使用futex系统调用实现的。 当进程收到信号时,Linux上的每个阻塞系统调用都会使用EINTR突然返回。 因为它可能会在futex系统调用之外的一小段时间内错过真正的唤醒,因此pthread_cond_wait()实现的时候不能用while。 只有调用者在返回之后再次检查条件才能避免这种竞争条件。 因此POSIX信号将产生虚假唤醒。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值