栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

Java 锁概(二)ReentrantLock

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

Java 锁概(二)ReentrantLock

1 ReentrantLock的概述

ReentrantLock来自于JDK1.5,位于JUC包的locks子包,独占(互斥)式可重入锁。Synchronized的功能他都有,并且具有更加强大的功能。
实现了Lock接口,具有通用的操作锁的方法。由于来自JUC包,内部是使用AQS队列同步器来辅助实现的,重写了AQS的独占式获取锁的方法,并实现了可重入性。
ReentrantLock还具有公平与非公平两个获取锁模式,这个并非AQS提供的,而是ReentrantLock基于AQS自己实现的。
除了Lock接口的通用方法。此类还定义了 isLocked 和 getLockQueueLength 方法,以及一些相关的 protected 访问方法,这些方法对检测和监视可能很有用。
要想深入学习ReentrantLock的源码实现,应该首先学习并了解AQS。下面的源码涉及到AQS的均没有讲,在之前的文章中有详细介绍!

1.1 ReentrantLock的API方法

除了Lock接口的通用方法之外,它还具有如下自己特有的方法:

  方法名称
  描述


  int getHoldCount() 
  查询当前线程保持此锁定的次数(重入次数),也就是该方法调用前的(lock方法调用次数- unlock方法调用次数)。


  int getWaitQueueLength(Condition condition) 
  返回在与此锁相关联的给定条件队列中等待的线程数的估计。


  boolean hasQueuedThread(Thread thread) 
  查询给定线程是否正在等待获取此锁。


  boolean hasQueuedThreads() 
  查询是否有线程正在等待获取此锁。


  boolean hasWaiters(Condition condition) 
  查询任何线程是否等待与此锁相关联的给定条件


  boolean isFair() 
  查询此锁是否是公平锁。


  boolean isHeldByCurrentThread() 
  查询此锁是否由当前线程持有。


  boolean isLocked() 
  查询此锁是否由任何线程持有。


  protected Thread getOwner() 
  返回当前拥有此锁的线程,如果不拥有,则返回 null


  protected Collection getQueuedThreads()
  返回包含可能正在等待获取此锁的线程的集合


  int getQueueLength() 
  返回等待获取此锁的线程数的估计。


  protected Collection getWaitingThreads(Condition condition)
  返回包含可能在与此锁相关联的给定条件下等待的线程的集合。
1.2 可重入

ReentrantLock是一个可重入的互斥锁。顾名思义,“互斥锁”表示锁在某一时间点只能被同一线程所拥有。“可重入”表示锁可被某一线程多次获取。当然 synchronized 也是可重入的互斥锁。
synchronized关键字隐式的支持重进入,比如一个synchronized修饰的递归方法,在方法执行时,执行线程在获取了锁之后仍能连续多次地获得该锁。
ReentrantLock虽然没能像synchronized关键字一样支持隐式的重进入,但是在调用lock()方法时,已经获取到锁的线程,能够再次调用lock()方法获取锁而不被阻塞。主要的实现是在实现tryAcquire(int acquires)方法时考虑占有锁的线程再次获取锁的场景。
在ReentrantLock中, AQS 的state 同步状态值表示线程获取该锁的可重入次数, 在默认情况下,state的值为0 表示当前锁没有被任何线程持有。当一个线程第一次获取该锁时会尝试使用CAS设置state 的值为l ,如果CAS 成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在该线程没有释放锁的情况下第二次获取该锁后,状态值被设置为2 , 这就是可重入次数。在该线程释放该锁时,会尝试使用CAS 让状态值减1,如果减l 后状态值为0,则当前线程释放该锁。

1.3 公平与非公平

根据线程获取锁的抢占机制,锁可以分为公平锁和非公平锁。

如果在绝对时间上,先对锁进行获取的请求一定先被满足,也就是等待时间最长的线程最优先获取锁,也可以说锁获取是顺序的。那么这个锁是公平的,反之,是不公平的。

公平锁是严格的以FIFO的方式进行锁的竞争,但是非公平锁是无序的锁竞争,刚释放锁的线程很大程度上能比较快的获取到锁,队列中的线程只能等待,所以非公平锁可能会有“饥饿”的问题。但是重复的锁获取能减小线程之间的切换,而公平锁则是严格的线程切换,这样对操作系统的影响是比较大的,所以非公平锁的吞吐量是大于公平锁的,这也是为什么JDK将非公平锁作为默认的实现。
ReentrantLock既可以是公平锁又可以是非公平锁。当此类的构造方法

ReentrantLock(boolean fair) 接收true作为参数时,ReentrantLock就是公平锁,线程依次排队获取公平锁,即锁将被等待最长时间的线程占有。

** 与默认情况(使用非公平锁)相比,使用公平锁的程序在多线程环境下效率比较低。而且即使是公平锁也不一定能保证线程调度的公平性,后来的线程调用tryLock方法同样可以不经过排队而获得该锁。**

2 ReentrantLock的原理 2.1 基本结构
ReentrantLock具有三个内部类,Sync、NonfairSync、FairSync。Sync直接继承了AbstractQueuedSynchronizer,应该是提供了通用的方法实现。

而NonfairSync和FairSync则直接继承了Sync。从名字能够猜测出,公平锁内部调用的是FairSync类,非公平锁内部调用的是NonfairSync类,它们具有自己的特殊方法实现,它们都间接的继承了AbstractQueuedSynchronizer抽象类。

所以说,ReentrantLock锁内部还是依赖AQS来实现的,只不过由于需要支持公平模式和非公平模式,而实现了两个内部类。在我们指定使用的模式之后,再选择初始化不同的AQS实现来支持即可。

2.2.1 构造器ReentrantLock()

public ReentrantLock()

创建一个 ReentrantLock 的实例。这等同于使用 ReentrantLock(false)。即默认无参构造器,是非公平模式。

public ReentrantLock(boolean fair) {
    //很简单,根据参数选择初始化不同的AQS对象
    sync = fair ? new FairSync() : new NonfairSync();
}

2.2.2 ReentrantLock(boolean fair)

public ReentrantLock(boolean fair)

创建一个具有给定公平策略的 ReentrantLock。false表示不公平,true表示公平。

  public ReentrantLock(boolean fair) {
   //很简单,根据参数选择初始化不同的AQS对象
   sync = fair ? new FairSync() : new NonfairSync();
  }
  • 2.3 解锁原理 2.3.1 unlock释放锁

    JUC中只要是实现Lock的锁,那么解锁的方法,一般都统一调用开放给外部的unlock ()方法。

public void unlock() {
    //内部调用AQS的release模版方法
    sync.release(1);
}
2.3.2 release独占式释放锁

release方法我们很熟悉了,就是AQS提供的模版方法,用于独占式释放锁,这个方法在AQS章节中有非常详细的讲解。
在这里面主要的就是我们自己重写的tryRelease方法,其中关键的就是重入锁的释放逻辑。

protected final boolean tryRelease(int releases) {
    //获取释放之后的state值c,ReentrantLock中是减去1
    int c = getState() - releases;
    //如果当前线程不是获取锁的线程,那么抛出IllegalMonitorStateException异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    //free用于记录释放成功与否的标志位,默认为false
    boolean free = false;
    //如果c为0,那表示完全释放了锁,此时清除记录线程,free为true
    //否则,说明该锁被重入了,那么lock了几次就需要unlock几次,因为每一次只是state减去1,并且此次并没有完全释放
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    //设置新的state
    setState(c);
    //返回free
    return free;
}

2.5.3 tryRelease尝试释放锁

tryRelease是AQS中定义的方法,在AQS中的实现是抛出异常,需要子类自己重写。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}

ReentrantLock在公平和非公平模式下对此有相同实现,即锁的释放逻辑都是一样的,该实现在Sync中:

  • - Sync的tryRelease实现
      
    - 尝试释放锁
      
    
    - @param releases 参数,ReentrantLock中传递1
      
    - @return true 成功 false 失败
      */
      protected final boolean tryRelease(int releases) {
       //获取释放之后的state值c,ReentrantLock中是减去1
       int c = getState() - releases;
       //如果当前线程不是获取锁的线程,那么抛出IllegalMonitorStateException异常
       if (Thread.currentThread() != getExclusiveOwnerThread())
    
       throw new IllegalMonitorStateException();
      
      //free用于记录释放成功与否的标志位,默认为false
       boolean free = false;
       //如果c为0,那表示完全释放了锁,此时清除记录线程,free为true
       //否则,说明该锁被重入了,那么lock了几次就需要unlock几次,因为每一次只是state减去1,并且此次并没有完全释放
       if (c == 0) {
      
    
       free = true;
       setExclusiveOwnerThread(null);
      }
       //设置新的state
       setState(c);
       //返回free
       return free;
      }
    

ReentrantLock中,tryRelease的实现其实就是将线程持有锁的次数state减1,即将state值减1,若减少后state值为0,那么表示线程将完全释放锁,设置获取锁的线程为null,更新state值,返回true。
如果返回false,说明此次释放锁并没有完全释放。由于执行该方法的线程必然持有锁,故该方法对state的更新不需要任何CAS操作。

3 ReentrantLock总结

ReentrantLock是基于AQS实现的独占式可重入锁,同时还有公平模式和非公平模式,即可选择实现未非公平锁和公平锁!

从源码代码中可以看出来,非公平锁的非公平之处在于:在头结点释放锁唤醒后继线程的时候,如果有新来的线程在尝试获取锁,那么新来的线程在和后继结点中的线程 的竞争中可能成功,有两个地方都有支持这样的可能性:

  1. 在NonfairSync实现的的lock方法中,CAS获取到锁(只有在当前锁未被任何线程占有时才能成功)。

  2. 在nonfairTryAcquire方法中,CAS获取到锁(只有在当前锁未被任何线程占有时才能成功)。

    如果被新来的线程获取到了锁,那么此时获取到锁的线程,不需要加入队列。而队列中被唤醒的后继结点由于获取不到锁只能继续等待获得锁的结点释放了锁之后继续尝试。
    只要是进入同步队列排了队的结点(线程),就只能按照队列顺序去获取锁,而没有排队的线程,则可能直接插队获取锁。如果在上面的两处获取锁都失败后,线程会被构造结点并加入同步队列等待,再也没有先一步获取锁的可能性。
    可以看出来,非公平模式下,同步队列中等待很久的线程相比还未进入队列等待的线程并没有优先权,甚至竞争也处于劣势:在队列中的线程要等待唤醒,并且还要检查前驱结点是否为头结点。在锁竞争激烈的情况下,在队列中等待的线程可能迟迟竞争不到锁,这也就非公平在高并发情况下会出现的饥饿问题。
    但是非公平模式会有良好的性能,直接获取锁线程不必加入等待队列就可以获得锁,免去了构造结点并加入同步队列的繁琐操作,因为加入到队列尾部时也是需要CAS竞争的,可能会造成CPU的浪费,并且还可能会因为后续的线程等待、唤醒,造成线程上下文切换,非常耗时。
    而且即使是公平锁也不一定能保证线程调度的公平性,公平模式下,后来的线程调用tryLock方法同样可以不经过排队而获得该锁。因为tryLock方法内部是直接调用的nonfairTryAcquire方法,这个方法我们说了它不具备公平性!

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/1002765.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号