热门标签 | HotTags
当前位置:  开发笔记 > 运维 > 正文

Java并发系列之ReentrantLock源码分析

这篇文章主要为大家详细介绍了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起到一定的帮助作用。

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


推荐阅读
  • Tomcat/Jetty为何选择扩展线程池而不是使用JDK原生线程池?
    本文探讨了Tomcat和Jetty选择扩展线程池而不是使用JDK原生线程池的原因。通过比较IO密集型任务和CPU密集型任务的特点,解释了为何Tomcat和Jetty需要扩展线程池来提高并发度和任务处理速度。同时,介绍了JDK原生线程池的工作流程。 ... [详细]
  • 本文介绍了Web学习历程记录中关于Tomcat的基本概念和配置。首先解释了Web静态Web资源和动态Web资源的概念,以及C/S架构和B/S架构的区别。然后介绍了常见的Web服务器,包括Weblogic、WebSphere和Tomcat。接着详细讲解了Tomcat的虚拟主机、web应用和虚拟路径映射的概念和配置过程。最后简要介绍了http协议的作用。本文内容详实,适合初学者了解Tomcat的基础知识。 ... [详细]
  • 单点登录原理及实现方案详解
    本文详细介绍了单点登录的原理及实现方案,其中包括共享Session的方式,以及基于Redis的Session共享方案。同时,还分享了作者在应用环境中所遇到的问题和经验,希望对读者有所帮助。 ... [详细]
  • 李逍遥寻找仙药的迷阵之旅
    本文讲述了少年李逍遥为了救治婶婶的病情,前往仙灵岛寻找仙药的故事。他需要穿越一个由M×N个方格组成的迷阵,有些方格内有怪物,有些方格是安全的。李逍遥需要避开有怪物的方格,并经过最少的方格,找到仙药。在寻找的过程中,他还会遇到神秘人物。本文提供了一个迷阵样例及李逍遥找到仙药的路线。 ... [详细]
  • 本文介绍了Hyperledger Fabric外部链码构建与运行的相关知识,包括在Hyperledger Fabric 2.0版本之前链码构建和运行的困难性,外部构建模式的实现原理以及外部构建和运行API的使用方法。通过本文的介绍,读者可以了解到如何利用外部构建和运行的方式来实现链码的构建和运行,并且不再受限于特定的语言和部署环境。 ... [详细]
  • 本文回顾了3.21开学以来的学习情况,包括javaWeb课程的迷糊感和未预习导致的不知所措,以及对VOJ题目的归类和解答。午饭前完成了阶乘相关的两道题目。下午的数据结构课听懂了队列的讲解,但有几个疑问未能及时复习。设计模式课程因预习效率低而感到困惑,同时也没搞清楚下节课的内容。晚上去图书馆学习。通过反思和总结,对自己的学习收获有了更深刻的认识。 ... [详细]
  • 处理docker容器时间和宿主机时间不一致问题的方法
    本文介绍了处理docker容器时间和宿主机时间不一致问题的方法,包括复制主机的localtime到容器、处理报错情况以及重启容器的步骤。通过这些方法,可以解决docker容器时间和宿主机时间不一致的问题。 ... [详细]
  • 本文介绍了Java的集合及其实现类,包括数据结构、抽象类和具体实现类的关系,详细介绍了List接口及其实现类ArrayList的基本操作和特点。文章通过提供相关参考文档和链接,帮助读者更好地理解和使用Java的集合类。 ... [详细]
  • 本文介绍了如何使用python从列表中删除所有的零,并将结果以列表形式输出,同时提供了示例格式。 ... [详细]
  • 本文介绍了电流源并联合并的方法,以及谐振电路的原理。谐振电路具有很强的选频能力,通过将电感和电容连接在一起,电流和电压会产生震荡。谐振频率的大小取决于电感和电容的大小,而电路中的电阻会逐渐降低震荡的幅度。电阻和电容组成的电路中,当电容放完电后,电阻两端的电压为0,电流不再流过电容。然而,电感是一种特殊的器件,当有电流流过时,线圈会产生感应磁场,阻止电流的流动,从而使电流不会减小。 ... [详细]
  • Linux环境变量函数getenv、putenv、setenv和unsetenv详解
    本文详细解释了Linux中的环境变量函数getenv、putenv、setenv和unsetenv的用法和功能。通过使用这些函数,可以获取、设置和删除环境变量的值。同时给出了相应的函数原型、参数说明和返回值。通过示例代码演示了如何使用getenv函数获取环境变量的值,并打印出来。 ... [详细]
  • 标题: ... [详细]
  • 本文介绍了南邮ctf-web的writeup,包括签到题和md5 collision。在CTF比赛和渗透测试中,可以通过查看源代码、代码注释、页面隐藏元素、超链接和HTTP响应头部来寻找flag或提示信息。利用PHP弱类型,可以发现md5('QNKCDZO')='0e830400451993494058024219903391'和md5('240610708')='0e462097431906509019562988736854'。 ... [详细]
  • Java学习笔记之面向对象编程(OOP)
    本文介绍了Java学习笔记中的面向对象编程(OOP)内容,包括OOP的三大特性(封装、继承、多态)和五大原则(单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则)。通过学习OOP,可以提高代码复用性、拓展性和安全性。 ... [详细]
  • 微软小娜企业版发布新版本,提供构建自定义技能的套件
    微软将向企业级市场发布微软小娜企业版的新版本,该版本提供了构建自定义技能的套件,使企业员工可以更方便地使用数字助理。目前该套件仍处于内测期间,只有部分企业可以获得,其他有兴趣的企业需要继续等待。新版本的套件可以帮助员工构建各种自定义技能,如检查休假余额、创建服务凭证等。微软通过让多个开发人员编辑和管理机器人通道注册配置来改善开发者的体验,团队可以自行访问和更改技能注册,满足企业实际需求。微软小娜企业版已经在各个行业得到采用,能够帮助员工专注于优先事项,将非优先处理的任务交给微软小娜处理。 ... [详细]
author-avatar
卟抛棄D
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有