Java并发6:等待通知机制


Java并发6:等待通知机制

等待-通知机制

上篇文章通过while(!actr.apply(this, target));来破坏占用且等待条件。

apply()操作耗时非常短,且并发冲突量也不大,那这个方案还可行,可能循环上几次或者几十次就能一次性获取转出账户和转入账户。

但若 apply() 操作耗时长,或者并发冲突量大的时候,就不适用了,可能要循环上万次才能获取到锁,代码不断执行太消耗 CPU 。

最好的方案应该是:

如果线程要求的条件(转出账本和转入账本同在文件架上)不满足,则线程阻塞自己,进入等待状态;

当线程要求的条件(转出账本和转入账本同在文件架上)满足后,通知等待的线程重新执行。

使用线程阻塞的方式就能避免循环等待消耗 CPU 的问题。

一个完整的等待 - 通知机制:

线程首先获取互斥锁,当线程要求的条件不满足时,释放互斥锁,进入等待状态;

当要求的条件满足时,通知等待的线程,重新获取互斥锁。

synchronized实现等待-通知机制

```配合``` wait()```、```notify()```、```notifyAll() ```实现。
同一时刻,只允许一个线程进入``` synchronized``` 保护的临界区,当有一个线程进入临界区后,其他线程就只能进入图中左边的等待队列里等待。 **等待队列和互斥锁是一对一的关系,每个互斥锁都有自己独立的等待队列。** ![wait()操作工作原理](https://images-1252818907.cos.ap-chengdu.myqcloud.com/images/wait()操作工作原理.png) 调用`` wait() ``方法,当前线程被阻塞,进入到右边的等待队列,这个等待队列也是互斥锁的等待队列。 线程在进入等待队列的同时,会释放持有的互斥锁,线程释放锁后,其他线程就有机会获得锁,并进入临界区了。 线程要求的条件满足时,通过Java 对象的``` notify() ```和 ```notifyAll() ```方法唤醒阻塞的线程。 当条件满足时调用``` notify()```,会通知等待队列(互斥锁的等待队列)中的线程,告诉它条件曾经满足过。 **notify() 只能保证在通知时间点,条件是满足的。** 而被通知线程的执行时间点和通知的时间点基本上不会重合,所以当线程执行的时候,很可能条件已经不满足了(保不齐有其他线程插队)。 ![notify()操作工作原理图](https://images-1252818907.cos.ap-chengdu.myqcloud.com/images/notify()操作工作原理图.png) **```wait()```、```notify()```、```notifyAll() ```方法操作的等待队列是互斥锁的等待队列。** 所以如果``` synchronized``` 锁定的是``` this```,那么对应的一定是``` this.wait()```、```this.notify()```、```this.notifyAll()```; 如果 ```synchronized ```锁定的是``` target```,那么对应的一定是 ```target.wait()```、```target.notify()```、```target.notifyAll() ```。 **```wait()```、```notify()```、```notifyAll() ```这三个方法能够被调用的前提是已经获取了相应的互斥锁,即 wait()、notify()、notifyAll() 都是在 synchronized{}内部被调用的。** 如果在``` synchronized{}```外部调用,或者锁定的``` this```,而用``` target.wait()``` 调用的话,JVM 会抛出一个运行时异常:```java.lang.IllegalMonitorStateException```。 ### 利用等待-通知实现转账 + 等待-通知机制条件判断范式 这种范式可以解决上面提到的条件曾经满足过这个问题。因为当``` wait()``` 返回时,有可能条件已经发生变化了,曾经条件满足,但是现在已经不满足了,所以要重新检验条件是否满足。 范式,意味着是经典做法,所以没有特殊理由不要尝试换个写法。 ```java while(条件不满足) { wait(); }
  • 代码
class Allocator {
  private List<Object> als;
  // 一次性申请所有资源
  synchronized void apply(Object from, Object to){
    // 经典写法
    while(als.contains(from) || als.contains(to)){
      try{
        //集合不为空,说明有其他Account线程已经申请了资源,当前申请资源的Account线程就wait()释放
        //实例allocator锁
        wait();
      }catch(Exception e){}   
    } 
    als.add(from);
    als.add(to);  
  }
  // 归还资源
  synchronized void free(
    Object from, Object to){
    als.remove(from);
    als.remove(to);
    notifyAll();
  }
}

notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程,尽量使用notifyAll()避免线程被饿死。

思考

```方法和``` sleep() ```方法都能让当前线程挂起一段时间,区别是什么?
+ ```wait()```方法会释放对象的“锁标志” 调用对象的```wait()```方法后,当前线程暂停执行,释放锁标志,并将当前线程放入**对象等待池**中。 调用```notify()```方法后,将从**对象等待池**中移出任意一个线程并放入**锁标志等待池**中,**只有锁标志等待池中的线程可以获取锁标志,它们随时准备争夺锁的拥有权。** 调用```notifyAll()```方法后,**会将对象等待池中的所有线程都移动到该对象的锁标志等待池**。 + ```sleep()```方法**必须**要指定等待的时间 可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。 但是```sleep()```方法不会释放“锁标志”,如果有```synchronized```同步块,其他线程仍然不能访问共享数据。 + ```wait()```是```Object```的对象方法,```sleep()```是```Thread```的类方法。 + ```wait()```需要被唤醒,```wait(1000L)```不需要,```sleep()```不需要。 + ```wait()```需要获取到监视器,即只能在同步方法和同步快中使用,否则抛异常,```sleep()```不需要。 ### 别人的总结 >```java >public class MyLock { > // 测试转账的main方法 > public static void main(String[] args) throws InterruptedException { > Account src = new Account(10000); > Account target = new Account(10000); > CountDownLatch countDownLatch = new CountDownLatch(9999); > for (int i = 0; i < 9999; i++) { > new Thread(() -> { > src.transactionToTarget(1, target); > //放在finally里面?更安全? > countDownLatch.countDown(); > }).start(); > } > countDownLatch.await(); > System.out.println("src="+src.getBanalce() ); > System.out.println("target=" + target.getBanalce()); > } > > static class Account { //账户类 > public Account(Integer banalce) { > this.banalce = banalce; > } > > private Integer banalce; > > public void transactionToTarget(Integer money, Account target) {//转账方法 > Allocator.getInstance().apply(this, target); > this.banalce -= money; > target.setBanalce(target.getBanalce() + money); > //放在finally里面?更安全? > Allocator.getInstance().release(this, target); > } > > public Integer getBanalce() { > return banalce; > } > > public void setBanalce(Integer banalce) { > this.banalce = banalce; > } > } > > static class Allocator { //单例锁类 > private Allocator() { > } > //优化 list换成set更快 > private List<Account> locks = new ArrayList<>(); > > public synchronized void apply(Account src, Account tag) { > while (locks.contains(src) || locks.contains(tag)) { > try { > this.wait(); > } catch (InterruptedException e) { > } > } > locks.add(src); > locks.add(tag); > } > > public synchronized void release(Account src, Account tag) { > locks.remove(src); > locks.remove(tag); > this.notifyAll(); > } > > public static Allocator getInstance() { > return AllocatorSingle.install; > } > > static class AllocatorSingle { > public static Allocator install = new Allocator(); > } > } >}

——wang


文章作者: Wendell
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Wendell !
评论
  目录