Java 为什么再造管程(六)

本文探讨了Java并发编程中的管程概念,解释了为何在已有synchronized的情况下还需要实现Lock接口。Lock提供了中断支持、超时获取和非阻塞获取锁的能力,弥补了synchronized的不足。并分享了并发大师Doug Lea的三个用锁最佳实践,强调了正确使用锁以避免性能问题和死锁。此外,文章通过示例展示了如何使用Lock实现一个阻塞队列。
摘要由CSDN通过智能技术生成

文章导读:并发编程理论篇
(一)可见性,原子性,有序性的源头
(二)Java 如何解决可见性、有序性和原子性问题
(三)为什么存在等待 - 通知机制
(四)宏观角度看并发
(五)管程:并发编程的万能钥匙

在前面,我通过白话文的方式,简单介绍了并发编程的理论篇,感兴趣的可以看看前面的文章。接下来,简单聊聊并发编程工具篇。

Java 为什么再造管程

我们都知道,Java 语言本身提供的 synchronized 是管程的一种实现。另外,Java SDK 并发包通过 LockCondition 两个接口也是管程的一种实现,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。

不知道你是否会和我有同样的疑问,既然有了 synchronized,为什么还需要实现 Lock?

有人说,synchronized 性能不如 SDK 里面的 Lock。其实那是在 JDK1.6 之前。1.6 版本之后,synchronized 做了很多优化,将性能追了上来,所以 1.6 之后的版本又有人推荐使用 synchronized 了。那性能是否可以成为“重复造轮子”的理由呢?显然不能。因为性能问题优化一下就可以了,完全没必要“重复造轮子”

前面介绍死锁的时候,提出了一个破坏不可抢占条件方案,但是这个方案 synchronized 没有办法解决。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。但我们希望的是:对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。

而 Java SDK 里面 Lock 对此提供了支持。

  1. 能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
  2. 支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
  3. 非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。

除此之外,还支持多条件变量的等待环形机制

这三种方案可以全面弥补 synchronized 的问题。到这里相信你应该也能理解了,这三个方案就是“重复造轮子”的主要原因,体现在 API 上,就是 Lock 接口的三个方法。详情如下:

// 支持中断的API
// 阻塞式地获取锁,立即处理interrupt信息,并抛出异常
void lockInterruptibly() throws InterruptedException;
// 支持超时的API
// 在timeout时间内阻塞式地获取锁,成功返回true,超时返回false,同时立即处理interrupt信息,并抛出异常
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 支持非阻塞获取锁的API
// 尝试获取一次锁,不管成功失败,都立即返回true、false
boolean tryLock();

// 另外还有个lock(),类似于synchronized。
// lock()  阻塞式地获取锁

用锁的最佳实践

并发大师 Doug Lea 推荐的三个用锁的最佳实践,它们分别是:

  1. 永远只在更新对象的成员变量时加锁
  2. 永远只在访问可变的成员变量时加锁
  3. 永远不在调用其他对象的方法时加锁

这三条规则,前两条估计你一定会认同,最后一条你可能会觉得过于严苛。但是专栏里作者还是倾向于我们去遵守,因为调用其他对象的方法,实在是很不安全,也许“其他”方法里面有线程 sleep() 的调用,也可能会有奇慢无比的 I/O 操作,这些都会严重影响性能。更可怕的是,“其他”类的方法可能也会加锁,然后双重加锁就可能导致死锁。

Lock 实战:快速实现一个阻塞队列

这是一道挺经典的面试题:如何快速实现一个阻塞队列
是的,就是利用 Lock 以及 条件变量。

一个阻塞队列,需要两个条件变量,一个是队列不空(空队列不允许出队),另一个是队列不满(队列已满不允许入队),因此需要两个Condition条件变量。

了解了这两个条件,实现一个阻塞队列就很简单了…

public class BlockedQueue<T>{
  final Lock lock = new ReentrantLock();
  // 条件变量:队列不满  
  final Condition notFull = lock.newCondition();
  // 条件变量:队列不空  
  final Condition notEmpty = lock.newCondition();

  // 入队
  public void enq(T x) {
    lock.lock();
    try {
      while (队列已满){
        // 等待队列不满
        notFull.await();
      }  
      // 省略入队操作...
      //入队后,通知可出队
      notEmpty.signal();
    }finally {
      lock.unlock();
    }
  }
  // 出队
  public void deq(){
    lock.lock();
    try {
      while (队列已空){
        // 等待队列不空
        notEmpty.await();
      }  
      // 省略出队操作...
      //出队后,通知可入队
      notFull.signal();
    }finally {
      lock.unlock();
    }  
  }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值