Java并发系列之ReentrantLock源码分析

在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来完成的。有关于ConditionObject的具体实现大家可以查阅我的这篇文章《Java并发系列[4]----AbstractQueuedSynchronizer源码分析之条件队列》,这里就不重复赘述了。至此,我们对ReentrantLock源码的剖析也告一段落,希望阅读本篇文章能够对读者们理解并掌握ReentrantLock起到一定的帮助作用。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

您可能感兴趣的文章:

  • Java并发系列之Semaphore源码分析
  • Java并发系列之CountDownLatch源码分析
  • Java并发系列之CyclicBarrier源码分析
  • Java并发系列之ConcurrentHashMap源码分析
  • Java并发系列之AbstractQueuedSynchronizer源码分析(共享模式)
  • Java并发系列之AbstractQueuedSynchronizer源码分析(独占模式)
  • Java并发系列之AbstractQueuedSynchronizer源码分析(概要分析)
  • Java并发系列之AbstractQueuedSynchronizer源码分析(条件队列)
  • Java并发系列之Semaphore源码分析
  • Java并发系列之ReentrantLock源码分析
(0)

相关推荐

  • Java并发系列之AbstractQueuedSynchronizer源码分析(共享模式)

    通过上一篇的分析,我们知道了独占模式获取锁有三种方式,分别是不响应线程中断获取,响应线程中断获取,设置超时时间获取.在共享模式下获取锁的方式也是这三种,而且基本上都是大同小异,我们搞清楚了一种就能很快的理解其他的方式.虽然说AbstractQueuedSynchronizer源码有一千多行,但是重复的也比较多,所以读者不要刚开始的时候被吓到,只要耐着性子去看慢慢的自然能够渐渐领悟.就我个人经验来说,阅读AbstractQueuedSynchronizer源码有几个比较关键的地方需要弄明白,分别是

  • Java并发系列之CyclicBarrier源码分析

    现实生活中我们经常会遇到这样的情景,在进行某个活动前需要等待人全部都齐了才开始.例如吃饭时要等全家人都上座了才动筷子,旅游时要等全部人都到齐了才出发,比赛时要等运动员都上场后才开始.在JUC包中为我们提供了一个同步工具类能够很好的模拟这类场景,它就是CyclicBarrier类.利用CyclicBarrier类可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作.下图演示了这一过程. 在CyclicBarrier类的内部有一个计数器,每个线程在到达屏障点的时候都会调用await

  • Java并发系列之Semaphore源码分析

    Semaphore(信号量)是JUC包中比较常用到的一个类,它是AQS共享模式的一个应用,可以允许多个线程同时对共享资源进行操作,并且可以有效的控制并发数,利用它可以很好的实现流量控制.Semaphore提供了一个许可证的概念,可以把这个许可证看作公共汽车车票,只有成功获取车票的人才能够上车,并且车票是有一定数量的,不可能毫无限制的发下去,这样就会导致公交车超载.所以当车票发完的时候(公交车以满载),其他人就只能等下一趟车了.如果中途有人下车,那么他的位置将会空闲出来,因此如果这时其他人想要上车

  • Java并发系列之AbstractQueuedSynchronizer源码分析(独占模式)

    在上一篇<Java并发系列[1]----AbstractQueuedSynchronizer源码分析之概要分析>中我们介绍了AbstractQueuedSynchronizer基本的一些概念,主要讲了AQS的排队区是怎样实现的,什么是独占模式和共享模式以及如何理解结点的等待状态.理解并掌握这些内容是后续阅读AQS源码的关键,所以建议读者先看完我的上一篇文章再回过头来看这篇就比较容易理解.在本篇中会介绍在独占模式下结点是怎样进入同步队列排队的,以及离开同步队列之前会进行哪些操作.AQS为在独占模

  • Java并发之ReentrantLock类源码解析

    ReentrantLock内部由Sync类实例实现. Sync类定义于ReentrantLock内部. Sync继承于AbstractQueuedSynchronizer. AbstractQueuedSynchronizer继承于AbstractOwnableSynchronizer. AbstractOwnableSynchronizer类中只定义了一个exclusiveOwnerThread变量,表示当前拥有的线程. 除了Sync类,ReentrantLock内部还定义了两个实现类. No

  • Java并发系列之AbstractQueuedSynchronizer源码分析(概要分析)

    学习Java并发编程不得不去了解一下java.util.concurrent这个包,这个包下面有许多我们经常用到的并发工具类,例如:ReentrantLock, CountDownLatch, CyclicBarrier, Semaphore等.而这些类的底层实现都依赖于AbstractQueuedSynchronizer这个类,由此可见这个类的重要性.所以在Java并发系列文章中我首先对AbstractQueuedSynchronizer这个类进行分析,由于这个类比较重要,而且代码比较长,为了

  • Java并发系列之CountDownLatch源码分析

    CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行.它的内部提供了一个计数器,在构造闭锁时必须指定计数器的初始值,且计数器的初始值必须大于0.另外它还提供了一个countDown方法来操作计数器的值,每调用一次countDown方法计数器都会减1,直到计数器的值减为0时就代表条件已成熟,所有因调用await方法而阻塞的线程都会被唤醒.这就是CountDownLatch的内部机制,看起来很简单,无非就是阻塞一部分线程让其在达到某个条

  • Java并发系列之ConcurrentHashMap源码分析

    我们知道哈希表是一种非常高效的数据结构,设计优良的哈希函数可以使其上的增删改查操作达到O(1)级别.Java为我们提供了一个现成的哈希结构,那就是HashMap类,在前面的文章中我曾经介绍过HashMap类,知道它的所有方法都未进行同步,因此在多线程环境中是不安全的.为此,Java为我们提供了另外一个HashTable类,它对于多线程同步的处理非常简单粗暴,那就是在HashMap的基础上对其所有方法都使用synchronized关键字进行加锁.这种方法虽然简单,但导致了一个问题,那就是在同一时间

  • Java并发系列之AbstractQueuedSynchronizer源码分析(条件队列)

    通过前面三篇的分析,我们深入了解了AbstractQueuedSynchronizer的内部结构和一些设计理念,知道了AbstractQueuedSynchronizer内部维护了一个同步状态和两个排队区,这两个排队区分别是同步队列和条件队列.我们还是拿公共厕所做比喻,同步队列是主要的排队区,如果公共厕所没开放,所有想要进入厕所的人都得在这里排队.而条件队列主要是为条件等待设置的,我们想象一下如果一个人通过排队终于成功获取锁进入了厕所,但在方便之前发现自己没带手纸,碰到这种情况虽然很无奈,但是它

  • Java并发系列之ReentrantLock源码分析

    在Java5.0之前,协调对共享对象的访问可以使用的机制只有synchronized和volatile.我们知道synchronized关键字实现了内置锁,而volatile关键字保证了多线程的内存可见性.在大多数情况下,这些机制都能很好地完成工作,但却无法实现一些更高级的功能,例如,无法中断一个正在等待获取锁的线程,无法实现限定时间的获取锁机制,无法实现非阻塞结构的加锁规则等.而这些更灵活的加锁机制通常都能够提供更好的活跃性或性能.因此,在Java5.0中增加了一种新的机制:Reentrant

  • Java并发编程之Condition源码分析(推荐)

    Condition介绍 上篇文章讲了ReentrantLock的加锁和释放锁的使用,这篇文章是对ReentrantLock的补充.ReentrantLock#newCondition()可以创建Condition,在ReentrantLock加锁过程中可以利用Condition阻塞当前线程并临时释放锁,待另外线程获取到锁并在逻辑后通知阻塞线程"激活".Condition常用在基于异步通信的同步机制实现中,比如dubbo中的请求和获取应答结果的实现. 常用方法 Condition中主要的

随机推荐