我们看一下重入锁ReentrantLock类关系图,它是实现了Lock接口的类。NonfairSync和FairSync都继承 自抽象类Sync,在ReentrantLock中有非公平锁NonfairSync和公平锁FairSync的实现。
在重入锁ReentrantLock类关系图中,我们可以看到NonfairSync和FairSync都继承自抽象类Sync,而 Sync类继承自抽象类AbstractQueuedSynchronizer(简称AQS)。如果我们看过JUC的源代码,会发现 不仅重入锁用到了AQS, JUC 中绝大部分的同步工具也都是基于AQS构建的。
研究任何框架或工具都就要一个入口,我们以重入锁为切入点来理解AQS的作用及实现。下面我们深入ReentrantLock源码来分析AQS是如何实现线程同步的。
AQS其实使用了一种典型的设计模式:模板方法。我们如果查看AQS的源码可以看到,AQS为一个抽象 类,AQS中大多数方法都是final或private的,也就是说AQS并不希望用户覆盖或直接使用这些方法,而 是只能重写AQS规定的部分方法。
我们以重入锁中相对简单的公平锁为例,以获取锁的 lock 方法为入口,一直深入到AQS,来分析多线程 是如何同步获取锁的。
获取锁时源码的调用过程,时序图如下:
ReentrantLock获取锁调用了 lock 方法,我们看下该方法的内部:调用了sync.lock()。
public void lock() {
sync.lock();
}
sync是Sync类的一个实例,Sync类实际上是ReentrantLock的抽象静态内部类,它集成了AQS来实现重 入锁的具体业务逻辑。AQS是一个同步队列,实现了线程的阻塞和唤醒,没有实现具体的业务功能。在 不同的同步场景中,需要用户继承AQS来实现对应的功能。
我们查看ReentrantLock源码,可以看到,Sync有两个实现类公平锁FairSync和非公平锁NoFairSync。
重入锁实例化时,根据参数fair为属性sync创建对应锁的实例。以公平锁为例,调用sync.lock事实上调用的是FairSync的lock方法。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
我们看下该方法的内部,执行了方法acquire(1),acquire为AQS中的final方法,用于竞争锁。
final void lock() {
acquire(1);
}
线程进入AQS中的acquire方法,arg=1。
这个方法逻辑:先尝试抢占锁,抢占成功,直接返回;
抢占失败,将线程封装成Node节点追加到AQS队列中并使线程阻塞等待。
(1)首先会执行tryAcquire(1)尝试抢占锁,成功返回true,失败返回false。抢占成功了,就不会执行 下面的代码了
(2)抢占锁失败后,执行addWaiter(Node.EXCLUSIVE)将x线程封装成Node节点追加到AQS队列。
(3)然后调用acquireQueued将线程阻塞,线程阻塞。 线程阻塞后,接下来就只需等待其他线程唤醒它,线程被唤醒后会重新竞争锁的使用。 接下来,我们看看这个三个方法具体是如何实现的。
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
selfInterrupt();
}
尝试获取锁:若获取锁成功,返回true;获取锁失败,返回false。
这个方法逻辑:获取当前的锁状态,如果为无锁状态,当前线程会执行CAS操作尝试获取锁;若当前线 程是重入获取锁,只需增加锁的重入次数即可。
线程抢占锁失败后,执行addWaiter(Node.EXCLUSIVE)将线程封装成Node节点追加到AQS队列。
addWaiter(Node mode)的mode表示节点的类型,Node.EXCLUSIVE表示是独占排他锁,也就是说重入 锁是独占锁,用到了AQS的独占模式。
Node定义了两种节点类型:
staticfinalNodeSHARED=newNode();//共享模式
staticfinalNodeEXCLUSIVE=null;//独占模式
会唤醒挂起的线程,使被阻塞的线程继续执行。
公平锁和非公平锁在获取锁和释放锁时有什么区别呢?
我们对比下两种锁的源码,非公平锁与非公平锁获取锁的差异有两处:
可重入锁ReentrantLock是互斥锁,互斥锁在同一时刻仅有一个线程可以进行访问,但是在大多数场景 下,大部分时间都是提供读服务,而写服务占有的时间较少。然而读服务不存在数据竞争问题,如果一 个线程在读时禁止其他线程读势必会导致性能降低,所以就出现了读写锁。
读写锁维护着一对锁,一个读锁和一个写锁。通过分离读锁和写锁,使得并发性比一般的互斥锁有了较 大的提升:在同一时间可以允许多个读线程同时访问,但是在写线程访问时,所有读线程和写线程都会 被阻塞。
读写锁的主要特性:
读写锁ReentrantReadWriteLock实现接口ReadWriteLock,该接口维护了一对相关的锁,一个用于只读 操作,另一个用于写入操作。只要没有 writer,读取锁可以由多个 reader 线程同时保持。写入锁是独 占的。
ReadWriteLock定义了两个方法。readLock()返回用于读操作的锁,writeLock()返回用于写操作的锁。 ReentrantReadWriteLock定义如下:
ReentrantReadWriteLock与ReentrantLock一样,其锁主体依然是Sync,它的读锁、写锁都是依靠 Sync来实现的。所以ReentrantReadWriteLock实际上只有一个锁,只是在获取读取锁和写入锁的方式 上不一样而已,它的读写锁其实就是两个类:ReadLock、writeLock,这两个类都是lock的实现。
通常情况下,为了保证多线程间的有效并发,会要求每个线程持有锁的时间尽可能短,但是在某些情况下,一个程序对同一个锁不间断、高频地请求、同步与释放,会消耗掉一定的系统资源,因为锁的请求、同步与释放本身会带来性能损耗,这样高频的锁请求就反而不利于系统性能的优化了,虽然单次同步操作的时间可能很短。锁粗化就是告诉我们任何事情都有个度,有些情况下我们反而希望把很多次锁的请求合并成一个请求,以降低短时间内大量锁请求、同步、释放带来的性能损耗。