Java并发系列[5]----ReentrantLock源码分析

在Java5.0以前,协调对共享对象的访问可使用的机制只有synchronized和volatile。咱们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可见性。在大多数状况下,这些机制都能很好地完成工做,但却没法实现一些更高级的功能,例如,没法中断一个正在等待获取锁的线程,没法实现限定时间的获取锁机制,没法实现非阻塞结构的加锁规则等。而这些更灵活的加锁机制一般都可以提供更好的活跃性或性能。所以,在Java5.0中增长了一种新的机制:ReentrantLock。ReentrantLock类实现了Lock接口,并提供了与synchronized相同的互斥性和内存可见性,它的底层是经过AQS来实现多线程同步的。与内置锁相比ReentrantLock不只提供了更丰富的加锁机制,并且在性能上也不逊色于内置锁(在之前的版本中甚至优于内置锁)。说了ReentrantLock这么多的优势,那么下面咱们就来揭开它的源码看看它的具体实现。html

1.synchronized关键字的介绍java

Java提供了内置锁来支持多线程的同步,JVM根据synchronized关键字来标识同步代码块,当线程进入同步代码块时会自动获取锁,退出同步代码块时会自动释放锁,一个线程得到锁后其余线程将会被阻塞。每一个Java对象均可以用作一个实现同步的锁,synchronized关键字能够用来修饰对象方法,静态方法和代码块,当修饰对象方法和静态方法时锁分别是方法所在的对象和Class对象,当修饰代码块时需提供额外的对象做为锁。每一个Java对象之因此能够做为锁,是由于在对象头中关联了一个monitor对象(管程),线程进入同步代码块时会自动持有monitor对象,退出时会自动释放monitor对象,当monitor对象被持有时其余线程将会被阻塞。固然这些同步操做都由JVM底层帮你实现了,但以synchronized关键字修饰的方法和代码块在底层实现上仍是有些区别的。synchronized关键字修饰的方法是隐式同步的,即无需经过字节码指令来控制的,JVM能够根据方法表中的ACC_SYNCHRONIZED访问标志来区分一个方法是不是同步方法;而synchronized关键字修饰的代码块是显式同步的,它是经过monitorenter和monitorexit字节码指令来控制线程对管程的持有和释放。monitor对象内部持有_count字段,_count等于0表示管程未被持有,_count大于0表示管程已被持有,每次持有线程重入时_count都会加1,每次持有线程退出时_count都会减1,这就是内置锁重入性的实现原理。另外,monitor对象内部还有两条队列_EntryList和_WaitSet,对应着AQS的同步队列和条件队列,当线程获取锁失败时会到_EntryList中阻塞,当调用锁对象的wait方法时线程将会进入_WaitSet中等待,这是内置锁的线程同步和条件等待的实现原理。安全

2.ReentrantLock和Synchronized的比较多线程

synchronized关键字是Java提供的内置锁机制,其同步操做由底层JVM实现,而ReentrantLock是java.util.concurrent包提供的显式锁,其同步操做由AQS同步器提供支持。ReentrantLock在加锁和内存上提供的语义与内置锁相同,此外它还提供了一些其余功能,包括定时的锁等待,可中断的锁等待,公平锁,以及实现非块结构的加锁。另外,在早期的JDK版本中ReentrantLock在性能上还占有必定的优点,既然ReentrantLock拥有这么多优点,为何还要使用synchronized关键字呢?事实上确实有许多人使用ReentrantLock来替代synchronized关键字的加锁操做。可是内置锁仍然有它特有的优点,内置锁为许多开发人员所熟悉,使用方式也更加的简洁紧凑,由于显式锁必须手动在finally块中调用unlock,因此使用内置锁相对来讲会更加安全些。同时将来更加可能会去提高synchronized而不是ReentrantLock的性能。由于synchronized是JVM的内置属性,它能执行一些优化,例如对线程封闭的锁对象的锁消除优化,经过增长锁的粒度来消除内置锁的同步,而若是经过基于类库的锁来实现这些功能,则可能性不大。因此当须要一些高级功能时才应该使用ReentrantLock,这些功能包括:可定时的,可轮询的与可中断的锁获取操做,公平队列,以及非块结构的锁。不然,仍是应该优先使用synchronized。并发

3.获取锁和释放锁的操做源码分析

咱们首先来看一下使用ReentrantLock加锁的示例代码。性能

 1 public void doSomething() {
 2     //默认是获取一个非公平锁
 3     ReentrantLock lock = new ReentrantLock();
 4     try{
 5         //执行前先加锁
 6         lock.lock();   
 7         //执行操做...
 8     }finally{
 9         //最后释放锁
10         lock.unlock();
11     }
12 }

如下是获取锁和释放锁这两个操做的API。优化

1 //获取锁的操做
2 public void lock() {
3     sync.lock();
4 }
5 //释放锁的操做
6 public void unlock() {
7     sync.release(1);
8 }

能够看到获取锁和释放锁的操做分别委托给Sync对象的lock方法和release方法。ui

 1 public class ReentrantLock implements Lock, java.io.Serializable {
 2     
 3     private final Sync sync;
 4 
 5     abstract static class Sync extends AbstractQueuedSynchronizer {
 6         abstract void lock();
 7     }
 8     
 9     //实现非公平锁的同步器
10     static final class NonfairSync extends Sync {
11         final void lock() {
12             ...
13         }
14     }
15     
16     //实现公平锁的同步器
17     static final class FairSync extends Sync {
18         final void lock() {
19             ...
20         }
21     }
22 }

每一个ReentrantLock对象都持有一个Sync类型的引用,这个Sync类是一个抽象内部类它继承自AbstractQueuedSynchronizer,它里面的lock方法是一个抽象方法。ReentrantLock的成员变量sync是在构造时赋值的,下面咱们看看ReentrantLock的两个构造方法都作了些什么?spa

1 //默认无参构造器
2 public ReentrantLock() {
3     sync = new NonfairSync();
4 }
5 
6 //有参构造器
7 public ReentrantLock(boolean fair) {
8     sync = fair ? new FairSync() : new NonfairSync();
9 }

调用默认无参构造器会将NonfairSync实例赋值给sync,此时锁是非公平锁。有参构造器容许经过参数来指定是将FairSync实例仍是NonfairSync实例赋值给sync。NonfairSync和FairSync都是继承自Sync类并重写了lock()方法,因此公平锁和非公平锁在获取锁的方式上有些区别,这个咱们下面会讲到。再来看看释放锁的操做,每次调用unlock()方法都只是去执行sync.release(1)操做,这步操做会调用AbstractQueuedSynchronizer类的release()方法,咱们再来回顾一下。

 1 //释放锁的操做(独占模式)
 2 public final boolean release(int arg) {
 3     //拨动密码锁, 看看是否可以开锁
 4     if (tryRelease(arg)) {
 5         //获取head结点
 6         Node h = head;
 7         //若是head结点不为空而且等待状态不等于0就去唤醒后继结点
 8         if (h != null && h.waitStatus != 0) {
 9             //唤醒后继结点
10             unparkSuccessor(h);
11         }
12         return true;
13     }
14     return false;
15 }

这个release方法是AQS提供的释放锁操做的API,它首先会去调用tryRelease方法去尝试获取锁,tryRelease方法是抽象方法,它的实现逻辑在子类Sync里面。

 1 //尝试释放锁
 2 protected final boolean tryRelease(int releases) {
 3     int c = getState() - releases;
 4     //若是持有锁的线程不是当前线程就抛出异常
 5     if (Thread.currentThread() != getExclusiveOwnerThread()) {
 6         throw new IllegalMonitorStateException();
 7     }
 8     boolean free = false;
 9     //若是同步状态为0则代表锁被释放
10     if (c == 0) {
11         //设置锁被释放的标志为真
12         free = true;
13         //设置占用线程为空
14         setExclusiveOwnerThread(null);
15     }
16     setState(c);
17     return free;
18 }

这个tryRelease方法首先会获取当前同步状态,并将当前同步状态减去传入的参数值获得新的同步状态,而后判断新的同步状态是否等于0,若是等于0则代表当前锁被释放,而后先将锁的释放状态置为真,再将当前占有锁的线程清空,最后调用setState方法设置新的同步状态并返回锁的释放状态。

4.公平锁和非公平锁

咱们知道ReentrantLock是公平锁仍是非公平锁是基于sync指向的是哪一个具体实例。在构造时会为成员变量sync赋值,若是赋值为NonfairSync实例则代表是非公平锁,若是赋值为FairSync实例则代表为公平锁。若是是公平锁,线程将按照它们发出请求的顺序来得到锁,但在非公平锁上,则容许插队行为:当一个线程请求非公平的锁时,若是在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中全部等待的线程直接得到这个锁。下面咱们先看看非公平锁的获取方式。

 1 //非公平同步器
 2 static final class NonfairSync extends Sync {
 3     //实现父类的抽象获取锁的方法
 4     final void lock() {
 5         //使用CAS方式设置同步状态
 6         if (compareAndSetState(0, 1)) {
 7             //若是设置成功则代表锁没被占用
 8             setExclusiveOwnerThread(Thread.currentThread());
 9         } else {
10             //不然代表锁已经被占用, 调用acquire让线程去同步队列排队获取
11             acquire(1);
12         }
13     }
14     //尝试获取锁的方法
15     protected final boolean tryAcquire(int acquires) {
16         return nonfairTryAcquire(acquires);
17     }
18 }
19 
20 //以不可中断模式获取锁(独占模式)
21 public final void acquire(int arg) {
22     if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
23         selfInterrupt();
24     }
25 }

能够看到在非公平锁的lock方法中,线程第一步就会以CAS方式将同步状态的值从0改成1。其实这步操做就等于去尝试获取锁,若是更改为功则代表线程刚来就获取了锁,而没必要再去同步队列里面排队了。若是更改失败则代表线程刚来时锁还未被释放,因此接下来就调用acquire方法。咱们知道这个acquire方法是继承自AbstractQueuedSynchronizer的方法,如今再来回顾一下该方法,线程进入acquire方法后首先去调用tryAcquire方法尝试去获取锁,因为NonfairSync覆盖了tryAcquire方法,并在方法中调用了父类Sync的nonfairTryAcquire方法,因此这里会调用到nonfairTryAcquire方法去尝试获取锁。咱们看看这个方法具体作了些什么。

 1 //非公平的获取锁
 2 final boolean nonfairTryAcquire(int acquires) {
 3     //获取当前线程
 4     final Thread current = Thread.currentThread();
 5     //获取当前同步状态
 6     int c = getState();
 7     //若是同步状态为0则代表锁没有被占用
 8     if (c == 0) {
 9         //使用CAS更新同步状态
10         if (compareAndSetState(0, acquires)) {
11             //设置目前占用锁的线程
12             setExclusiveOwnerThread(current);
13             return true;
14         }
15     //不然的话就判断持有锁的是不是当前线程
16     }else if (current == getExclusiveOwnerThread()) {
17         //若是锁是被当前线程持有的, 就直接修改当前同步状态
18         int nextc = c + acquires;
19         if (nextc < 0) {
20             throw new Error("Maximum lock count exceeded");
21         }
22         setState(nextc);
23         return true;
24     }
25     //若是持有锁的不是当前线程则返回失败标志
26     return false;
27 }

nonfairTryAcquire方法是Sync的方法,咱们能够看到线程进入此方法后首先去获取同步状态,若是同步状态为0就使用CAS操做更改同步状态,其实这又是获取了一遍锁。若是同步状态不为0代表锁被占用,此时会先去判断持有锁的线程是不是当前线程,若是是的话就将同步状态加1,不然的话此次尝试获取锁的操做宣告失败。因而会调用addWaiter方法将线程添加到同步队列。综上来看,在非公平锁的模式下一个线程在进入同步队列以前会尝试获取两遍锁,若是获取成功则不进入同步队列排队,不然才进入同步队列排队。接下来咱们看看公平锁的获取方式。

 1 //实现公平锁的同步器
 2 static final class FairSync extends Sync {
 3     //实现父类的抽象获取锁的方法
 4     final void lock() {
 5         //调用acquire让线程去同步队列排队获取
 6         acquire(1);
 7     }
 8     //尝试获取锁的方法
 9     protected final boolean tryAcquire(int acquires) {
10         //获取当前线程
11         final Thread current = Thread.currentThread();
12         //获取当前同步状态
13         int c = getState();
14         //若是同步状态0则表示锁没被占用
15         if (c == 0) {
16             //判断同步队列是否有前继结点
17             if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
18                 //若是没有前继结点且设置同步状态成功就表示获取锁成功
19                 setExclusiveOwnerThread(current);
20                 return true;
21             }
22         //不然判断是不是当前线程持有锁
23         }else if (current == getExclusiveOwnerThread()) {
24             //若是是当前线程持有锁就直接修改同步状态
25             int nextc = c + acquires;
26             if (nextc < 0) {
27                 throw new Error("Maximum lock count exceeded");
28             }
29             setState(nextc);
30             return true;
31         }
32         //若是不是当前线程持有锁则获取失败
33         return false;
34     }
35 }

调用公平锁的lock方法时会直接调用acquire方法。一样的,acquire方法首先会调用FairSync重写的tryAcquire方法来尝试获取锁。在该方法中也是首先获取同步状态的值,若是同步状态为0则代表此时锁恰好被释放,这时和非公平锁不一样的是它会先去调用hasQueuedPredecessors方法查询同步队列中是否有人在排队,若是没人在排队才会去修改同步状态的值,能够看到公平锁在这里采起礼让的方式而不是本身立刻去获取锁。除了这一步和非公平锁不同以外,其余的操做都是同样的。综上所述,能够看到公平锁在进入同步队列以前只检查了一遍锁的状态,即便是发现了锁是开的也不会本身立刻去获取,而是先让同步队列中的线程先获取,因此能够保证在公平锁下全部线程获取锁的顺序都是先来后到的,这也保证了获取锁的公平性。
那么咱们为何不但愿全部锁都是公平的呢?毕竟公平是一种好的行为,而不公平是一种很差的行为。因为线程的挂起和唤醒操做存在较大的开销而影响系统性能,特别是在竞争激烈的状况下公平锁将致使线程频繁的挂起和唤醒操做,而非公平锁能够减小这样的操做,因此在性能上将会优于公平锁。另外,因为大部分线程使用锁的时间都是很是短暂的,而线程的唤醒操做会存在延时状况,有可能在A线程被唤醒期间B线程立刻获取了锁并使用完释放了锁,这就致使了共赢的局面,A线程获取锁的时刻并无推迟,但B线程提早使用了锁,而且吞吐量也得到了提升。

5.条件队列的实现机制

内置条件队列存在一些缺陷,每一个内置锁都只能有一个相关联的条件队列,这致使多个线程可能在同一个条件队列上等待不一样的条件谓词,那么每次调用notifyAll时都会将全部等待的线程唤醒,当线程醒来后发现并非本身等待的条件谓词,转而又会被挂起。这致使作了不少无用的线程唤醒和挂起操做,而这些操做将会大量浪费系统资源,下降系统的性能。若是想编写一个带有多个条件谓词的并发对象,或者想得到除了条件队列可见性以外的更多控制权,就须要使用显式的Lock和Condition而不是内置锁和条件队列。一个Condition和一个Lock关联在一块儿,就像一个条件队列和一个内置锁相关联同样。要建立一个Condition,能够在相关联的Lock上调用Lock.newCondition方法。咱们先来看一个使用Condition的示例。

 1 public class BoundedBuffer {
 2 
 3     final Lock lock = new ReentrantLock();
 4     final Condition notFull = lock.newCondition();   //条件谓词:notFull
 5     final Condition notEmpty = lock.newCondition();  //条件谓词:notEmpty
 6     final Object[] items = new Object[100];
 7     int putptr, takeptr, count;
 8 
 9     //生产方法
10     public void put(Object x) throws InterruptedException {
11         lock.lock();
12         try {
13             while (count == items.length)
14                 notFull.await();  //队列已满, 线程在notFull队列上等待
15             items[putptr] = x;
16             if (++putptr == items.length) putptr = 0;
17             ++count;
18             notEmpty.signal(); //生产成功, 唤醒notEmpty队列的结点
19         } finally {
20             lock.unlock();
21         }
22     }
23 
24     //消费方法
25     public Object take() throws InterruptedException {
26         lock.lock();
27         try {
28             while (count == 0)
29                 notEmpty.await(); //队列为空, 线程在notEmpty队列上等待
30             Object x = items[takeptr];
31             if (++takeptr == items.length) takeptr = 0;
32             --count;
33             notFull.signal();  //消费成功, 唤醒notFull队列的结点
34             return x;
35         } finally {
36             lock.unlock();
37         }
38     }
39     
40 }

一个lock对象能够产生多个条件队列,这里产生了两个条件队列notFull和notEmpty。当容器已满时再调用put方法的线程须要进行阻塞,等待条件谓词为真(容器不满)才醒来继续执行;当容器为空时再调用take方法的线程也须要阻塞,等待条件谓词为真(容器不空)才醒来继续执行。这两类线程是根据不一样的条件谓词进行等待的,因此它们会进入两个不一样的条件队列中阻塞,等到合适时机再经过调用Condition对象上的API进行唤醒。下面是newCondition方法的实现代码。

 1 //建立条件队列
 2 public Condition newCondition() {
 3     return sync.newCondition();
 4 }
 5 
 6 abstract static class Sync extends AbstractQueuedSynchronizer {
 7     //新建Condition对象
 8     final ConditionObject newCondition() {
 9         return new ConditionObject();
10     }
11 }

ReentrantLock上的条件队列的实现都是基于AbstractQueuedSynchronizer的,咱们在调用newCondition方法时所得到的Condition对象就是AQS的内部类ConditionObject的实例。全部对条件队列的操做都是经过调用ConditionObject对外提供的API来完成的。有关于ConditionObject的具体实现你们能够查阅个人这篇文章《Java并发系列[4]----AbstractQueuedSynchronizer源码分析之条件队列》,这里就不重复赘述了。至此,咱们对ReentrantLock源码的剖析也告一段落,但愿阅读本篇文章可以对读者们理解并掌握ReentrantLock起到必定的帮助做用。

相关文章
相关标签/搜索