wait为什么要在同步块中使用? 为什么sleep就不用再同步块中?

 

(1)wait为什么要在同步块中使用?

  首先wait和notify方法是Object类中的

        

至于为什么它们是放在Object,我们稍后再分析;

 

wait为什么要在同步块中使用? 

仔细回顾一下,如果wait()方法不在同步块中,代码的确会抛出异常:

public class WaitInSyncBlockTest {
 
    @Test
    public void test() {
        try {
            new Object().wait();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

结果是:

  

 

Lost Wake-Up Problem

事情得从一个多线程编程里面臭名昭著的问题"Lost wake-up problem"说起。

这个问题并不是说只在Java语言中会出现,而是会在所有的多线程环境下出现。

假如有两个线程,一个消费者线程,一个生产者线程。生产者线程的任务可以简化成将count加一,而后唤醒消费者;消费者则是将count减一,而后在减到0的时候陷入睡眠:

生产者伪代码:

count+1;

notify();

消费者伪代码:

while(count<=0)

   wait()

count--

熟悉多线程的朋友一眼就能够看出来,这里面有问题。什么问题呢?

生产者是两个步骤:

  1. count+1;

  2. notify();

消费者也是两个步骤:

  1. 检查count值;

  2. 睡眠或者减一;

万一这些步骤混杂在一起呢?比如说,初始的时候count等于0,这个时候消费者检查count的值,发现count小于等于0的条件成立;就在这个时候,发生了上下文切换,生产者进来了,噼噼啪啪一顿操作,把两个步骤都执行完了,也就是发出了通知,准备唤醒一个线程。这个时候消费者刚决定睡觉,还没睡呢,所以这个通知就会被丢掉。紧接着,消费者就睡过去了……

        

 

这就是所谓的lost wake up问题。

那么怎么解决这个问题呢?

现在我们应该就能够看到,问题的根源在于,消费者在检查count到调用wait()之间,count就可能被改掉了。

这就是一种很常见的竞态条件。

很自然的想法是,让消费者和生产者竞争一把锁,竞争到了的,才能够修改count的值。

于是生产者的代码是:

tryLock()
count+1
 
notify()
releaseLock()

消费者的代码是:

tryLock()
while(count <= 0)
    wait()
 
count-1
releaseLock()

 

Java强制我们的wait()/notify()调用必须要在一个同步块中,就是不想让我们在不经意间出现这种lost wake up问题。

不仅仅是这两个方法,包括java.util.concurrent.locks.Condition的await()/signal()也必须要在同步块中:

private ReentrantLock lock = new ReentrantLock();
 
private Condition condition = lock.newCondition();
 
@Test
public void test() {
    try {
        condition.signal();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

 

准确的来说,即便是我们自己在实现自己的锁机制的时候,也应该要确保类似于wait()和notify()这种调用,要在同步块内,防止使用者出现lost wake up问题。 

 

换一个实例分析这个lost wake up问题

----------------------------------------------------------------------------------------------------

假设我们要自定义一个blocking queue,如果没有使用synchronized的话,我们可能会这样写:

class BlockingQueue {
    Queue<String> buffer = new LinkedList<String>();

    public void give(String data) {
        buffer.add(data);
        notify();                   // Since someone may be waiting in take!
    }

    public String take() throws InterruptedException {
        while (buffer.isEmpty())    // 不能用if,因为为了防止虚假唤醒
            wait();
        return buffer.remove();
    }
}

这段代码可能会导致如下问题:

  1. 一个消费者调用take,发现buffer.isEmpty
  2. 在消费者调用wait之前,由于cpu的调度,消费者线程被挂起,生产者调用give,然后notify
  3. 然后消费者调用wait (注意,由于错误的条件判断,导致wait调用在notify之后,这是关键)
  4. 如果很不幸的话,生产者产生了一条消息后就不再生产消息了,那么消费者就会一直挂起,无法消费,造成死锁。

解决这个问题的方法就是:总是让give/notify和take/wait为原子操作。

也就是说wait/notify是线程之间的通信,他们存在竞态,我们必须保证在满足条件的情况下才进行wait。换句话说,如果不加锁的话,那么wait被调用的时候可能wait的条件已经不满足了(如上述)。由于错误的条件下进行了wait,那么就有可能永远不会被notify到,所以我们需要强制wait/notify在synchronized中

 

wait与notify原理

  重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的MutexLock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。前面我们在讲Java对象头的时候,讲到了monitor这个对象,在hotspot虚拟机中,通过ObjectMonitor类来实现monitor。他的锁的获取过程的体现会简单很多.

     

 

wait 和notify

wait和notify是用来让线程进入等待状态以及使得线程唤醒的两个操作
wait()必须被synchronized来使用,

public class ThreadWait extends Thread{
  private Object lock;
  public ThreadWait(Object lock) {
     this.lock = lock;
  } 

  @Override
  public void run() {
    
      synchronized (lock){
        System.out.println("开始执行 thread wait");
      
        try {
          lock.wait();
         } catch (InterruptedException e) {
          e.printStackTrace();
         } 
       System.out.println("执行结束 thread wait");
      }
   }
  }
public class ThreadNotify(Object lock) {
  this.lock = lock;
} 

@Override
public void run() {
    synchronized (lock){
        System.out.println("开始执行 thread notify");
        lock.notify();
        System.out.println("执行结束 thread notify");
    }
  }
}

 

wait 和notify的原理

  1. 调用wait() 首先会获取监视器锁,获得成功后,会让线程进入等待状态进入等待队列并且释放锁;
  2. 然后当其他线程调用notify或者notifyall以后,会选择从等待队列中唤醒任意一个线程
  3. 而执行完notify方法以后,并不会立马唤醒线程,原因是当前线程仍然持有这把锁,处于等待状态的线程无法获得锁。必须要等到当前的线程执行完按monitorexit指令之后,也就是被释放之后,处于等待队列的线程就可以开始竞争锁了。

     

wait和notify为什么要放在synchronized里面

wait方法的语义有两个,

  • 释放当前的对象锁、
  • 使得当前线程进入阻塞队列,

而这些操作都和监视器是相关的,所以wait必须要获得一个监视器锁。
notify也一样,它是唤醒一个线程,所以需要知道待唤醒的线程在哪里,就必须找到这个对象获取这个对象的锁然后去到这个对象的等待队列去唤醒一个线程。

 

 

------------------------------------------------------------------------------------------------------------------------------------------------------------

为什么sleep就不用再同步块中?

 

sleep()方法是再Thread类中的

Thread.sleep()的操作的目的是想让当前线程休息一会,只是暂时不想干活而已,如果这个线程一开始一开始抢到一把锁,

比如A线程如果先抢到一个锁,然后B线程因为A线程抢到了就等着。接着A sleep了。B无论如何没有任何机会去拿到这个锁。你可以认为这样就是你预期的,也可以认为这样实际上因为A的实现,B的执行被卡住,浪费了CPU。因为你当你用了sleep的时候就意味着你想要让当前线程不考虑其他线程的感受,只是自己暂时不干活而已。
 
回到问题:为什么sleep就不用再同步块中?
 
  你把sleep放在同步块中,但是为什么sleep()需要放在同步块中呢?放在同步块中是为了多线程并发处理,大家按照顺序依次合理干活,不要造成死锁饥饿现象,但是sleep()方法就是想让当前线程进入阻塞状态,不要干活,我也不释放我已经拥有的锁,等到sleep()的时间到了,我再进入就绪态,等待cpu派活给我。sleep根本就不存在多线程并发访问问题,所以就不需要放在同步块中。
 
 
 
sleep不释放锁 线程是进入阻塞状态还是就绪状态?

  答案是进入阻塞状态,确切的说Thread在Java的状态TIMED_WAITING(但这个状态其实并没那么重要,可以认为是java的内部细节,用户不用太操心)。往下一层,在不同OS上底层的sleep的实现细节不太一样。但是大体上就是挂起当前的线程,然后设置一个信号或者时钟中断到时候唤醒。sleep后的的Thread在被唤醒前是不会消耗任何CPU的(确切的说,大部分OS都会这么实现,除非某个OS的实现偷懒了)。这点上,wait对当前线程的效果差不多是一样的,也会暂停调度,等着notify或者一个超时的时间。期间CPU也不会被消耗。
 
关于wait、sleep可以看这篇:  java sleep和wait的区别的疑惑?

 

 

 

------------------------------------------------------------------------------------------------------------------------------------------------------------

另一个问题: 为什么wait方法在object类中,sleep方法在Thread类中?

 

wait方法是让当前线程释放锁。然后让别的线程继续竞争。阻塞线程

notify通知 唤醒一个阻塞的线程 随机通知一个

这些都应该属于资源锁的动作,而作为锁,java中锁一般锁谁?锁的是对象,因为对象是资源,是我们需要操作的实体,而Object是所有对象的父类

 

sleep()是让某个线程暂停运行一段时间,其控制范围是由当前线程决定,也就是说,在线程里面决定.

 

posted @ 2020-10-30 17:03  myseries  阅读(4318)  评论(0编辑  收藏  举报