Java SDK 并发包内容很丰富。可是最核心的仍是其对管程的实现。由于理论上利用管程,你几乎能够实现并发包里全部的工具类。在前面咱们提到过在并发编程领域,有两大核心问题:一个是互斥:即同一时刻只容许一个线程访问共享资源;
另外一个是 同步:即线程之间如何通讯、协做。
编程
这两大问题,管程都是可以解决的。Java SDK 并发包经过 Lock 和 Condition 两个接口来实现管程,其中 Lock 用于解决互斥问题,Condition 用于解决同步问题。安全
今天咱们重点介绍 Lock 的使用,在介绍 Lock 的使用以前,有个问题须要你首先思考一下:Java 语言自己提供的 synchronized 也是管程的一种实现,既然 Java 从语言层面已经实现了管程了,那为何还要在 SDK 里提供另一种实现呢?很显然它们之间是有巨大区别的。那区别在哪里呢?多线程
让咱们回顾下在以前的死锁
问题中。提出一个破坏不可抢占条件
的方案。
可是这个方案 synchronized 没有办法解决。缘由是 synchronized 申请资源的时候,若是申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。
但咱们但愿的是:并发
对于“不可抢占”这个条件,占用部分资源的线程进一步申请其余资源时,若是申请不到,能够主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
若是咱们从新设计一把互斥锁去解决这个问题,那该怎么设计呢?我以为有三种方案。app
synchronized 的问题是,持有锁 A 后,若是尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但若是阻塞状态的线程可以响应中断信号,也就是说当咱们给阻塞的线程发送中断信号的时候,可以唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。函数
若是线程在一段时间以内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。工具
若是尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。性能
这三种方案能够全面弥补 synchronized 的问题。这三个方案就是“重复造轮子”的主要缘由,体如今 API 上,就是 Lock 接口的三个方法。详情以下:线程
// 支持中断的 API void lockInterruptibly() throws InterruptedException; // 支持超时的 API boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 支持非阻塞获取锁的 API boolean tryLock();
Java SDK 里面 Lock 的使用,有一个经典的范例,就是try{}finally{}
。须要重点关注的是在 finally 里面释放锁。这个范例无需多解释。可是有一点须要解释一下,那就是可见性是怎么保证的。你已经知道 Java 里多线程的可见性是经过 Happens-Before 规则保证的,而 synchronized 之因此可以保证可见性,也是由于有一条 synchronized 相关的规则:synchronized 的解锁 Happens-Before 于后续对这个锁的加锁。
那 Java SDK 里面 Lock 靠什么保证可见性呢?例如在下面的代码中,线程 T1 对 value 进行了 +=1 操做,那后续的线程 T2 可以看到 value 的正确结果吗?翻译
class X { private final Lock rtl = new ReentrantLock(); int value; public void addOne() { // 获取锁 rtl.lock(); try { value+=1; } finally { // 保证锁能释放 rtl.unlock(); } } }
咱们来比较理论的讨论下这段代码的可见性是怎么保证的。
这里说下虽然 Java SDK 里面锁的实现很是复杂,这里我就不展开细说了,可是原理仍是须要简单介绍一下:它是利用了 volatile 相关的 Happens-Before 规则
。
Java SDK 里面的 ReentrantLock,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值(简化后的代码以下面所示)。也就是说,在执行 value+=1 以前,程序先读写了一次 volatile 变量 state,在执行 value+=1 以后,又读写了一次 volatile 变量 state。根据相关的 Happens-Before 规则:
顺序规则
:对于线程 T1,value+=1 Happens-Before 释放锁的操做 unlock();volatile 变量规则
:因为 state = 1 会先读取 state,因此线程 T1 的 unlock() 操做 Happens-Before 线程 T2 的 lock() 操做;传递性规则
:线程 T1 的 value+=1 Happens-Before 线程 T2 的 lock() 操做。class SampleLock { volatile int state; // 加锁 lock() { // 省略代码无数 state = 1; } // 解锁 unlock() { // 省略代码无数 state = 0; } }
若是你细心观察,会发现咱们建立的锁的具体类名是ReentrantLock
,这个翻译过来叫可重入锁
,这个概念前面咱们一直没有介绍过。所谓可重入锁,顾名思义,指的是线程能够重复获取同一把锁。
例以下面代码中,当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操做。此时,若是锁 rtl 是可重入的,那么线程 T1 能够再次加锁成功;若是锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。
class X { private final Lock rtl = new ReentrantLock(); int value; public int get() { // 获取锁 rtl.lock(); ② try { return value; } finally { // 保证锁能释放 rtl.unlock(); } } public void addOne() { // 获取锁 rtl.lock(); try { value = 1 + get(); ① } finally { // 保证锁能释放 rtl.unlock(); } } }
在使用ReentrantLock
的时候,你会发现ReentrantLock
这个类有两个构造函数,一个是无参构造函数,一个是传入 fair 参数的构造函数。fair 参数表明的是锁的公平策略,若是传入 true 就表示须要构造一个公平锁,反之则表示要构造一个非公平锁。
// 无参构造函数:默认非公平锁 public ReentrantLock() { sync = new NonfairSync(); } // 根据公平策略参数建立锁 public ReentrantLock(boolean fair){ sync = fair ? new FairSync() : new NonfairSync(); }
在以前咱们介绍过入口等待队列,锁都对应着一个等待队列,若是一个线程没有得到锁,就会进入等待队列,当有线程释放锁的时候,就须要从等待队列中唤醒一个等待的线程。若是是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;若是是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。
你已经知道,用锁虽然能解决不少并发问题,可是风险也是挺高的。虽然有不少最佳实践,可是我以为最值得推荐的是并发大师 Doug Lea《Java 并发编程:设计原则与模式》一书中,推荐的三个用锁的最佳实践,它们分别是:
最后一条你可能会以为过于严苛。可是我仍是倾向于你去遵照,由于调用其余对象的方法,实在是太不安全了,也许“其余”方法里面有线程 sleep() 的调用,也可能会有奇慢无比的 I/O 操做,这些都会严重影响性能。更可怕的是,“其余”类的方法可能也会加锁,而后双重加锁就可能致使死锁。
Java SDK 并发包里的 Lock 接口里面的每一个方法,你能够感觉到,都是通过深思熟虑的。除了支持相似 synchronized 隐式加锁的 lock() 方法外,还支持超时、非阻塞、可中断的方式获取锁,这三种方式为咱们编写更加安全、健壮的并发程序提供了很大的便利。
还有一些其余实践诸如:减小锁的持有时间、减少锁的粒度等业界广为人知的规则,其实本质上它们都是相通的,不过是在该加锁的地方加锁而已。你能够本身体会,本身总结,最终总结出本身的一套最佳实践来。