并发七:Lock 和Condition
Lock 和Condition
Lock
Java SDK 并发包通过 Lock 和 Condition 两个接口来实现管程,其 中 Lock 用于解决互斥问题,Condition 用于解决同步问题。
synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。但我们希望的是:
对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时, 如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。也就不会死锁。
如果我们重新设计一把互斥锁去解决这个问题,那该怎么设计呢?有三种方案。
能够响应中断。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么 线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞 状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能 够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。
支持超时。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个 错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
非阻塞地获取锁。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线 程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。
这三种方案可以全面弥补 synchronized 的问题。到这里相信你应该也能理解了,这三个方 案就是“重复造轮子”的主要原因,体现在 API 上,就是 Lock 接口的三个方法。
1 | //支持中断的API |
如何保证可见性
Java SDK 里面 Lock 的使用,有一个经典的范例,就是try{}finally{},需要重点关注 的是在 finally 里面释放锁。 一点需要解释一下,那就是可见性是怎么保证的。已经知道 Java 里多线程的可见性是通 过 Happens-Before 规则保证的,而 synchronized 之所以能够保证可见性,也是因为有 一条 synchronized 相关的规则:synchronized 的解锁 Happens-Before 于后续对这个锁 的加锁。那 Java SDK 里面 Lock 靠什么保证可见性呢?利用了volatile相关的Happens-Before规则。
可重入锁
ReentrantLock,这个翻译过来叫可 重入锁,。所谓可重入锁,顾名思义,指的是线程可以重复获取同一把锁。例如下面代码中,当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入 的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。
1 | class X{ |
公平锁和非公平锁
在使用 ReentrantLock 的时候,会发现 ReentrantLock 这个类有两个构造函数,一个是 无参构造函数,一个是传入 fair 参数的构造函数。fair 参数代表的是锁的公平策略,如果传 入 true 就表示需要构造一个公平锁,反之则表示要构造一个非公平锁。
在前面管程章节中,我们介绍过入口等待队列,锁都对应着一 个等待队列,如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需 要从等待队列中唤醒一个等待的线程。如果是公平锁,唤醒的策略就是谁等待的时间长,就 唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。
最佳实践
- 永远只在更新对象的成员变量时加锁
- 永远只在访问可变的成员变量时加锁
- 永远不在调用其他对象的方法时加锁
Condition
Condition 实现了管程模型里面的条件变量。
Java 语言内置的管程里只有一个条件变量,而 Lock&Condition 实现的管程是支持多个条件变量的,这是二者的一个重要区别。在很多并发场景下,支持多个条件变量能够让我们的并发程序可读性更好,实现起来也更容 易。例如,实现一个阻塞队列,就需要两个条件变量。
利用条件变量实现阻塞队列
一个阻塞队列,需要两个条件变量,一个是队列不空(空队列不允许出队),一个是队列不满(队列已满不允许入队)。
Lock 和 Condition 实现的管程,线程等待和通知需要调用 await()、signal()、signalAll(),它们的语义和 wait()、notify()、notifyAll() 是相同的。 但是不一样的是,Lock&Condition 实现的管程里只能使用前面的 await()、signal()、 signalAll(),而后面的 wait()、notify()、notifyAll() 只有在 synchronized 实现的管程里才 能使用。
Java SDK 并发包里的 Lock 和 Condition 不过就是管程的一种实现而已。
同步异步
俗点来讲就是调用方是否需要等待结果,如果需要等待结果,就是同步;如 果不需要等待结果,就是异步。
同步,是Java代码默认的处理方式,如果程序想支持异步,可以通过以下两种方式:
调用方创建一个子线程,在子线程中执行方法调用,这种调用我们称为异步调用;
方法实现的时候,创建一个新的线程执行主要逻辑,主线程直接 return,这种方法我们 一般称为异步方法。
Author: corn1ng
Link: https://corn1ng.github.io/2020/01/11/新版并发/并发七:Lock 和Condition/
License: 知识共享署名-非商业性使用 4.0 国际许可协议