我是一个优良的人,但也有毛病,比我优良的人有很多很多。连本人都认输,何谈你的对手,与你一样优良,甚至比你优良的人。战败本人,就是最大的超过。
在Java5.0之前,协调对共享对象的拜访能够应用的机制只有synchronized和volatile。咱们晓得synchronized关键字实现了内置锁,而volatile关键字保障了多线程的内存可见性。
在大多数状况下,这些机制都能很好地实现工作,但却无奈实现一些更高级的性能,例如,无奈中断一个正在期待获取锁的线程,无奈实现限定工夫的获取锁机制,无奈实现非阻塞构造的加锁规定等。而这些更灵便的加锁机制通常都可能提供更好的活跃性或性能。
因而,在Java5.0中减少了一种新的机制:ReentrantLock。ReentrantLock类实现了Lock接口,并提供了与synchronized雷同的互斥性和内存可见性,它的底层是通过AQS来实现多线程同步的。与内置锁相比ReentrantLock不仅提供了更丰盛的加锁机制,而且在性能上也不逊色于内置锁(在以前的版本中甚至优于内置锁)。
说了ReentrantLock这么多的长处,那么上面咱们就来揭开它的源码看看它的具体实现。
1.synchronized关键字的介绍
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加锁的示例代码。
public void doSomething() { //默认是获取一个非偏心锁 ReentrantLock lock = new ReentrantLock(); try { //执行前先加锁 lock.lock(); //执行操作... } finally { //最初开释锁 lock.unlock(); }}
以下是获取锁和开释锁这两个操作的API。
//获取锁的操作public void lock() { sync.lock();}//开释锁的操作public void unlock() { sync.release(1);}
能够看到获取锁和开释锁的操作别离委托给Sync对象的lock办法和release办法。
public class ReentrantLock implements Lock, java.io.Serializable { private final Sync sync; abstract static class Sync extends AbstractQueuedSynchronizer { abstract void lock(); } //实现非偏心锁的同步器 static final class NonfairSync extends Sync { final void lock() { ... } } //实现偏心锁的同步器 static final class FairSync extends Sync { final void lock() { ... } }}
每个ReentrantLock对象都持有一个Sync类型的援用,这个Sync类是一个形象外部类它继承自AbstractQueuedSynchronizer,它外面的lock办法是一个形象办法。ReentrantLock的成员变量sync是在结构时赋值的,上面咱们看看ReentrantLock的两个构造方法都做了些什么?
//默认无参结构器public ReentrantLock() { sync = new NonfairSync();}//有参结构器public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync();}
调用默认无参结构器会将NonfairSync实例赋值给sync,此时锁是非偏心锁。有参结构器容许通过参数来指定是将FairSync实例还是NonfairSync实例赋值给sync。NonfairSync和FairSync都是继承自Sync类并重写了lock()办法,所以偏心锁和非偏心锁在获取锁的形式上有些区别,这个咱们上面会讲到。
再来看看开释锁的操作,每次调用unlock()办法都只是去执行sync.release(1)操作,这步操作会调用AbstractQueuedSynchronizer类的release()办法,咱们再来回顾一下。
//开释锁的操作(独占模式)public final boolean release(int arg) { //拨动密码锁, 看看是否可能开锁 if (tryRelease(arg)) { //获取head结点 Node h = head; //如果head结点不为空并且期待状态不等于0就去唤醒后继结点 if (h != null && h.waitStatus != 0) { //唤醒后继结点 unparkSuccessor(h); } return true; } return false;}
这个release办法是AQS提供的开释锁操作的API,它首先会去调用tryRelease办法去尝试获取锁,tryRelease办法是形象办法,它的实现逻辑在子类Sync外面。
//尝试开释锁protected final boolean tryRelease(int releases) { int c = getState() - releases; //如果持有锁的线程不是以后线程就抛出异样 if (Thread.currentThread() != getExclusiveOwnerThread()) { throw new IllegalMonitorStateException(); } boolean free = false; //如果同步状态为0则表明锁被开释 if (c == 0) { //设置锁被开释的标记为真 free = true; //设置占用线程为空 setExclusiveOwnerThread(null); } setState(c); return free;}
这个tryRelease办法首先会获取以后同步状态,并将以后同步状态减去传入的参数值失去新的同步状态,而后判断新的同步状态是否等于0,如果等于0则表明以后锁被开释,而后先将锁的开释状态置为真,再将以后占有锁的线程清空,最初调用setState办法设置新的同步状态并返回锁的开释状态。
4.偏心锁和非偏心锁
咱们晓得ReentrantLock是偏心锁还是非偏心锁是基于sync指向的是哪个具体实例。在结构时会为成员变量sync赋值,如果赋值为NonfairSync实例则表明是非偏心锁,如果赋值为FairSync实例则表明为偏心锁。如果是偏心锁,线程将依照它们发出请求的程序来取得锁,但在非偏心锁上,则容许插队行为:当一个线程申请非偏心的锁时,如果在发出请求的同时该锁的状态变为可用,那么这个线程将跳过队列中所有期待的线程间接取得这个锁。上面咱们先看看非偏心锁的获取形式。
//非偏心同步器static final class NonfairSync extends Sync { //实现父类的形象获取锁的办法 final void lock() { //应用CAS形式设置同步状态 if (compareAndSetState(0, 1)) { //如果设置胜利则表明锁没被占用 setExclusiveOwnerThread(Thread.currentThread()); } else { //否则表明锁曾经被占用, 调用acquire让线程去同步队列排队获取 acquire(1); } } //尝试获取锁的办法 protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }}//以不可中断模式获取锁(独占模式)public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) { selfInterrupt(); }}
能够看到在非偏心锁的lock办法中,线程第一步就会以CAS形式将同步状态的值从0改为1。其实这步操作就等于去尝试获取锁,如果更改胜利则表明线程刚来就获取了锁,而不用再去同步队列外面排队了。如果更改失败则表明线程刚来时锁还未被开释,所以接下来就调用acquire办法。
咱们晓得这个acquire办法是继承自AbstractQueuedSynchronizer的办法,当初再来回顾一下该办法,线程进入acquire办法后首先去调用tryAcquire办法尝试去获取锁,因为NonfairSync笼罩了tryAcquire办法,并在办法中调用了父类Sync的nonfairTryAcquire办法,所以这里会调用到nonfairTryAcquire办法去尝试获取锁。咱们看看这个办法具体做了些什么。
//非偏心的获取锁final boolean nonfairTryAcquire(int acquires) { //获取以后线程 final Thread current = Thread.currentThread(); //获取以后同步状态 int c = getState(); //如果同步状态为0则表明锁没有被占用 if (c == 0) { //应用CAS更新同步状态 if (compareAndSetState(0, acquires)) { //设置目前占用锁的线程 setExclusiveOwnerThread(current); return true; } //否则的话就判断持有锁的是否是以后线程 } else if (current == getExclusiveOwnerThread()) { //如果锁是被以后线程持有的, 就间接批改以后同步状态 int nextc = c + acquires; if (nextc < 0) { throw new Error("Maximum lock count exceeded"); } setState(nextc); return true; } //如果持有锁的不是以后线程则返回失败标记 return false;}
nonfairTryAcquire办法是Sync的办法,咱们能够看到线程进入此办法后首先去获取同步状态,如果同步状态为0就应用CAS操作更改同步状态,其实这又是获取了一遍锁。如果同步状态不为0表明锁被占用,此时会先去判断持有锁的线程是否是以后线程,如果是的话就将同步状态加1,否则的话这次尝试获取锁的操作宣告失败。于是会调用addWaiter办法将线程增加到同步队列。
综上来看,在非偏心锁的模式下一个线程在进入同步队列之前会尝试获取两遍锁,如果获取胜利则不进入同步队列排队,否则才进入同步队列排队。接下来咱们看看偏心锁的获取形式。
//实现偏心锁的同步器static final class FairSync extends Sync { //实现父类的形象获取锁的办法 final void lock() { //调用acquire让线程去同步队列排队获取 acquire(1); } //尝试获取锁的办法 protected final boolean tryAcquire(int acquires) { //获取以后线程 final Thread current = Thread.currentThread(); //获取以后同步状态 int c = getState(); //如果同步状态0则示意锁没被占用 if (c == 0) { //判断同步队列是否有前继结点 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { //如果没有前继结点且设置同步状态胜利就示意获取锁胜利 setExclusiveOwnerThread(current); return true; } //否则判断是否是以后线程持有锁 } else if (current == getExclusiveOwnerThread()) { //如果是以后线程持有锁就间接批改同步状态 int nextc = c + acquires; if (nextc < 0) { throw new Error("Maximum lock count exceeded"); } setState(nextc); return true; } //如果不是以后线程持有锁则获取失败 return false; }}
调用偏心锁的lock办法时会间接调用acquire办法。同样的,acquire办法首先会调用FairSync重写的tryAcquire办法来尝试获取锁。在该办法中也是首先获取同步状态的值,如果同步状态为0则表明此时锁刚好被开释,这时和非偏心锁不同的是它会先去调用hasQueuedPredecessors办法查问同步队列中是否有人在排队,如果没人在排队才会去批改同步状态的值,能够看到偏心锁在这里采取礼让的形式而不是本人马上去获取锁。
除了这一步和非偏心锁不一样之外,其余的操作都是一样的。综上所述,能够看到偏心锁在进入同步队列之前只查看了一遍锁的状态,即便是发现了锁是开的也不会本人马上去获取,而是先让同步队列中的线程先获取,所以能够保障在偏心锁下所有线程获取锁的程序都是先来后到的,这也保障了获取锁的公平性。
那么咱们为什么不心愿所有锁都是偏心的呢?毕竟偏心是一种好的行为,而不偏心是一种不好的行为。因为线程的挂起和唤醒操作存在较大的开销而影响零碎性能,特地是在竞争强烈的状况下偏心锁将导致线程频繁的挂起和唤醒操作,而非偏心锁能够缩小这样的操作,所以在性能上将会优于偏心锁。
另外,因为大部分线程应用锁的工夫都是十分短暂的,而线程的唤醒操作会存在延时状况,有可能在A线程被唤醒期间B线程马上获取了锁并应用完开释了锁,这就导致了双赢的场面,A线程获取锁的时刻并没有推延,但B线程提前应用了锁,并且吞吐量也取得了进步。
5.条件队列的实现机制
内置条件队列存在一些缺点,每个内置锁都只能有一个相关联的条件队列,这导致多个线程可能在同一个条件队列上期待不同的条件谓词,那么每次调用notifyAll时都会将所有期待的线程唤醒,当线程醒来后发现并不是本人期待的条件谓词,转而又会被挂起。这导致做了很多无用的线程唤醒和挂起操作,而这些操作将会大量节约系统资源,升高零碎的性能。如果想编写一个带有多个条件谓词的并发对象,或者想取得除了条件队列可见性之外的更多控制权,就须要应用显式的Lock和Condition而不是内置锁和条件队列。一个Condition和一个Lock关联在一起,就像一个条件队列和一个内置锁相关联一样。要创立一个Condition,能够在相关联的Lock上调用Lock.newCondition办法。咱们先来看一个应用Condition的示例。
public class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); //条件谓词:notFull final Condition notEmpty = lock.newCondition(); //条件谓词:notEmpty final Object[] items = new Object[100]; int putptr, takeptr, count; //生产办法 public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); //队列已满, 线程在notFull队列上期待 items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); //生产胜利, 唤醒notEmpty队列的结点 } finally { lock.unlock(); } } //生产办法 public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); //队列为空, 线程在notEmpty队列上期待 Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); //生产胜利, 唤醒notFull队列的结点 return x; } finally { lock.unlock(); } }}
一个lock对象能够产生多个条件队列,这里产生了两个条件队列notFull和notEmpty。当容器已满时再调用put办法的线程须要进行阻塞,期待条件谓词为真(容器不满)才醒来继续执行;当容器为空时再调用take办法的线程也须要阻塞,期待条件谓词为真(容器不空)才醒来继续执行。
这两类线程是依据不同的条件谓词进行期待的,所以它们会进入两个不同的条件队列中阻塞,等到适合机会再通过调用Condition对象上的API进行唤醒。上面是newCondition办法的实现代码。
//创立条件队列public Condition newCondition() { return sync.newCondition();}abstract static class Sync extends AbstractQueuedSynchronizer { //新建Condition对象 final ConditionObject newCondition() { return new ConditionObject(); }}
ReentrantLock上的条件队列的实现都是基于AbstractQueuedSynchronizer的,咱们在调用newCondition办法时所取得的Condition对象就是AQS的外部类ConditionObject的实例。所有对条件队列的操作都是通过调用ConditionObject对外提供的API来实现的。
至此,咱们对ReentrantLock源码的分析也告一段落,心愿浏览本篇文章可能对读者们了解并把握ReentrantLock起到肯定的帮忙作用。