Java并发系列之五:ReentranLock

news2024/12/27 12:13:00

首先尝试用一句话对ReeentrantLock进行概括: ReentrantLock基于AQS,它实现了公平锁和非公平锁,在开发中可以用它对共享资源进行同步。此外,和synchronized一样,ReentrantLock支持可重入,但ReentrantLock在调度上更灵活,支持更多丰富的功能。

这段话中,包含了一些关键词,我将其标注出来,并且形成一张思维导图,这张图也就是本篇要讲解的脉络。

 

若想要较为系统地去理解这些特性,我觉得最好的方式就是通过源码,在一览源码之后自己再动手实践一遍,这样就能够做到知其然并知其所以然。

首先来看ReentrantLock的继承关系,ReentrantLock实现了Lock接口。在面向对象的概念中,既然ReentrantLock是Lock的一种具体实现,那么它必然拥有Lock的抽象意义,继续来看一下Lock是如何被定义的。

public class ReentrantLock implements Lock,java.io.Serializable {
    //.......
}

 

最开头的一段注释介绍了Lock的意义在于提供了区别于synchronized的另一种具有更多广泛操作的同步方式,它能支持更多灵活的结构,并且可以关联多个Condition对象。Condition是Java提供的一个用于线程通信的接口。

Lock.java源码文件虽然有350多行,但只对六个方法进行了定义,其余都是注释。注释篇幅比较长,我这里就来简单概括一下,感兴趣的同学可以自己去通读一下注释。

void lock(),顾名思义就是用于获取锁,假如当前锁被其他线程占用,那么将会等待直到获取为止。

void lockInterruptibly(),和lock()类似,也是用于获取锁,但区别在于,假如当前线程在等待锁的过程中被中断,那么将会退出等待,并抛出中断异常。

boolean tryLock(),尝试获取锁,无论是否获取都立即返回,返回值代表是否获取锁。

boolean tryLock(long time,TimeUnit unit),尝试获取锁并设定了等待超时时间,返回值代表是否获取锁。

void unlock(),顾名思义释放锁。

Condition newCondition(),新建一个绑定在当前Lock对象上的Condition对象。

TIPS:Condition对象是什么?简单来说,它表示一个条件,不同线程可以通过该条件来进行通信。比如某线程可以通过await方法注册在condition对象上进行等待,然后通过condition对象的signal方法将该线程唤醒。这有点类似Object锁的wait和notify方法。但不同的是,一个Lock对象可以关联多个Condition对象,多个线程可以被绑定在不同的Condition对象上,这样就可以分组等待唤醒。此外,Condition对象还提供了和限时、中断相关的功能,丰富了线程的调度策略。

至此为止,Lock接口差不多介绍完了。可以说它只是定义了一些方法的语义,规定了它的实现类需要满足这些语义。

接下来我们就要看看ReentrantLock是如何按照这些抽象约定来进行实现的。

说个题外话,Reentrant的翻译为“可重入”,但是ReentrantLock除了实现可重入还实现了其他特性,以Reentrant命名似乎有点狭隘,但也可能是该锁本身最大的特点。

言归正传,如果你已经理解了上一篇讲解的AQS,那么理解ReentrantLock源码会很轻松,如果你对AQS还不是很熟悉,建议先阅读上一篇讲解AQS的内容。

浏览ReentrantLock类的源码,我们重点关注以下三个方面:

属性: sync

内部类: Sync、NonfairSync、FairSync

方法

  继承/实现方法:实现Lock的方法

  私有方法:暂不关注

属性

ReentrantLock只有一个属性:Sync类型的变量sync。sync被final修饰,意味着一旦初始化, 就不可修改引用了。那么它的初始化时机是什么时候?

private final Sync sync;

在ReentrantLock构造器中:

public ReentrantLock() {
    sync = new NonfairSync() ;
}

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new Nonfai rSync() ;
}

默认无参构造器中,sync将被初始化为非公平锁对象,而在含参构造器中,可以通过参数指定其被初始化为公平锁还是非公平锁对象。

这里的NonfairSync和FairSync两个类,看名字能够知道其分别为实现了非公平性和公平性的锁。

内部类

Sync

首先看Sync的继承关系,Sync继承了AQS,那么说明AQS中所有预设的机制,这边就都可以借用了,这就和我们上期讲的AQS相关知识贯穿了起来。

Sync被abstract修饰,说明它提供了一些公共逻辑,但需要通过子类来进行实例化。NonfairSync、FairSync是它唯二的两个子类。

Sync没有属性,那么来看看它提供的公共方法。

方法

Sync中除了void lock() 和void read0bject(java.io.ObjectInputStream s)两个方法外,其余方法都被final修饰,意味着不可被子类修改,我认为这些方法是对AQS内部方法的封装和拓展,本身实现已经完整可靠,不希望被外部破坏。

非final方法:

void lock()是获取锁的操作,这里是空实现,说明需要子类根据自己的特征来进行实现。FairSync和NonFairSync因为涉及公平性的差别,所以获取锁的操作肯定是不一样的,需要自己实现。

read0bject(java.io.objectInputStream s),用于反序列化不是很常用,可以忽略。

final方法:

final boolean nonfairTryAcquire(int acquires)

这里就有点奇怪,刚才不是说公平性获取锁和非公平性获取锁的逻辑都应该分别在FairSync和NonFairSync中单独实现,那么在Sync这个基类中,为什么会出现nonfairTryAcquire这种方法?

我猜测应该是FairSync和NonFairSync中都需要用到该方法,那为什么FairSync中会用到

nonfairTryAcquire这种非公平性的方法?我们暂时存疑,下文再看。

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread . currentThread() ;
    int C = getState();
    if(c==0){
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread (current);
            return true;
        }
    }
    else if (current == getExclusiveOwner Thread()) {
        int nextc = C + acquires;
        if (nextc < 0) // overflow
            throw new Error ("Maximum lock count exceeded") ;
            setState (nextc) ;
            return true;
    }
    return false;
}

该方法逻辑比较简单:

1.获取state,该值由AQS维护。

2.当state为0,那么代表锁状态为空闲,便可以进行一次CAS来原子地更改state,如果state更改成功,则代表获取了锁,将当前线程置为独占线程,并返回true,否则返回false。

3.当state不为0,说明锁被占用,判断当前线程是否已经是独占线程,既然锁都已经被占用了,为什么还要判断当前线程是否是独占线程?这里就是对“可重入性”的实现。

“可重入性”的定义是:单个线程执行时重新进入同一个子程序仍然是线程安全的。

可以这么理解:假如A线程在某上下文中获取了某锁,当A线程想要再次获取该锁时,不会因为锁已经被自己占用,而需要先等待锁的释放。假如A线程既获得了锁,又在等待自己释放锁,那么就会造成死锁。

“可重入性”简单来说就是:一个线程可以不用释放而重复获取一个锁n次,只是在释放的时候也需要相应释放n次。

回到代码,当锁已经被占用。如果占用锁的线程不是当前线程,那么代表获取锁失败,返回false。如果正是自己,满足可重入性的条件,使用nextc这个值来记录重入的次数,因为释放锁的时候也要释放相应次数。

protected final boolean tryRelease(int releases) {
    int C = getState() 一releases;
    if (Thread. currentThread() != getExclusiveOwner Thread())
        throw new IllegalMoni torStateException() ;
        boolean free = false;
        if(c==0){
            free = true;
            setExclusiveOwner Thread(null) ;
        }
    setState(c);
    return free;
}

不难理解,释放锁是一个通用操作,所以写在Sync类中供子类调用很正常。

逻辑也不复杂,但是也可以看到一些细节。这个方法的返回值的boolean,注意,这里并非返回true代表释放成功,false代表释放失败。事实上,这里的返回值代表的是是否完全释放(因为可能存在重入,所以需要释放多次)。

下面几个方法都比较简单,都是提供对一些状态的查询,看一看就好。

protected final boolean isHeldExclusively(),判断当前线程是否为获得锁的独占线程。

final Condi ti on0bject newCondition(),基于当前Lock对象新建一个Condition对象。

final Thread get0wner(),获取正在占用锁的那个线程对象。

final int getHoldCount(),获取state的数值。

final boolean isLocked(),判断锁是否空闲。

关于Sync,内容不多,它是一个抽象类,它的两个子类NonfairSync、FairSync分别实现了公平性锁和非公平性锁。

那么,什么是公平性锁和非公平性锁?

公平锁就是锁的分配会按照请求获取锁的顺序,比如AQS中介绍的FIFO队列,实现的就是公平锁。非公平锁就是锁的分配不用按照请求锁的顺序,比如是抢占式的。

公平锁就保证了,只要你排队了,那么就一定能轮到你拿锁。而非公平锁是抢占式的,很可能某个线程一直抢不到锁,而又不断有新的线程加入进来抢锁,所以可能该线程一直处于阻塞状态,这种状态被称为饥饿。

既然这样,那为什么还要设计非公平锁?在很多情况下,非公平锁的效率更高。为什么更高?现实生活中不是排队比争抢效率更高吗?因为非公平锁意味着后请求锁的线程可能在前面的休眠线程恢复前拿到锁,这样就有可能提高并发的性能。当唤醒挂起的线程时,线程状态切换之间会产生短暂延时。非公平锁就可以利用这段时间完成操作。这是非公平锁某些时候比公平锁性能要好的原因之一。

NonfairSync

static final class NonfairSync extends Sync {
    private static final long ser ialVersionUID = 7316153563782823691L ;
    /** 
      * Performs lock. Try immediate barge, backing up to normal
      *acquire on failure.
      */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwner Thread (Thread. currentThread()) ;
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfai rTryAcquire(acquires);
    }
}

NonfairSync中只重写了Sync中的lock和AQS中的tryAcquire两个方法。我们依次来看。

final void lock()首先尝试一次对锁的获取,如果CAS成功,那么当前线程成功获得锁。如果一次尝试失败,则调用AQS提供的acquire方法。

这里有两个问题值得讨论:

1.可重入性

当程序调用acquire的时候不要忘记,acquire内部将会首先调用tryAcquire来尝试获取锁,而nonfairTryAcquire内部已经实现了可重入性,所以满足。

2.非公平性

当程序调用lock的时候,会先进行一次CAS的尝试,当尝试获取锁失败时,调用acquire。 在acquire内部,首先会调用一次tryAcquire,而nonfairTryAcquire会直接尝试获取锁,如果锁被占用且不可重入,那么就会继续执行AQS中后续的排队流程。虽然只有那么两次尝试抢占,但也体现出了非公平性。

FairSync

static final class FairSync extends Sync {
    private static final long ser ia lVersionUID = -3000897897090466540L;
    final void lock() {
        acquire(1);
    }

    /**
     * Fair version of tryAcquire. Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread . current Thread() ;
        int C = getState();
        if (c==0) {
            if (!hasQueuedPredecessors() &
                compareAndSetState(0, acquires)) {
                setExclusiveOwner Thread(current); 
                return true;
            }
        }
        else if (current == getExclus iveOwnerThread()) {
            int nextc = C + acquires;
            if (nextc < 0)
                throw new Error ("Maximum lock count exceeded") ;
            setState(nextc) ;
            return true;
        }
        return false;
    }
}

FairSync也重写了Sync中的lock和AQS中的tryAcquire两个方法。我们依次来看。

final void lock(),直接调用AQS的acquire方法,主要还是从可重入性和公平性来看。

1.可重入性

再重复一遍,当程序调用acquire的时候,会首先调用一次tryAcquire,这是AQS相关的知识。在tryAcquire方法内部,我们看到当锁已经被占用的时候(22-28行),将会进行可重入判断,这段类似的逻辑在nonfairTryAcquire方法中解读过。

2.公平性

在tryAcquire方法中,首先判断锁是否空闲,如果空闲,此时并不是直接尝试通过CAS获取锁,而是需要判断是否存在前置等待节点。如果不存在,那说明,在队列中确实已经轮到当前线程尝试获取锁,否则tryAcquire返回false,当前线程将会执行AQS中的后续等待逻辑。这里就体现出了公平性。

方法

讲完了三个内部类的源码,ReentrantLock在我们面前几乎可以说是一丝不挂,暴露得彻彻底底。此外,ReentrantLock作为锁实现Lock接口,而Lock定义的方法将会被上层调用,那么接下来就来对方法的实现进行探究,可以大胆猜测,NonfairSync、FairSync已经做了比较完整的封装,ReentrantLock公有方法的实现,可能只是对sync对象的简单调用。

public void lock(){
    sync.lock();
}

果然,只是对sync对象的lock方法进行调用,该方法在NonfairSync、FairSync已经进行了完整的实现,这里也能体现出多态。

lockInterruptibly与lock方法的区别在于,当线程在等待锁的期间,是否立即相应中断。lock方法中,线程会在等待获取锁之后,再响应中断,这点在讲解AQS那期中,讲到了中断信号延迟传递的机制。lockInterruptibly方法中,若线程在等待获取锁期间被调用了中断,那么将会立即抛出中断异常。在这里,lockInterruptibly方法直接调用sync对象的acquireInterruptibly方法,该方法的实现存在于AQS内部。

public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
}

TIPS:下面简单介绍一下Java的线程中断机制。

1.线程在RUNNABLE状态下

假如现在有一条线程,它的状态是RUNNABLE。若此时你调用它的interrupt中断方法,它将继续运行,并不会抛出中断异常,而是只修改Thread对象中一个标志中断状态的boolean值,true代表被调用中断,false代表未被调用中断。

那么这就有个问题,开发者怎么知道该线程究竟有没有被调用中断?这里JDK提供了两个API,一个名为isInterrupted,返回这个Thread对象内的中断状态值;另一个名为interrupted,返回这个Thread对象内的中断状态值并且将其置为false。

若你需要关注某线程在RUNNABLE状态下的中断状态,那么可以轮询isInterrupted方法。

2.线程在BLOCKED/WAITING状态下

假如现在有一条线程,它的状态是BLOCKED或者WAITING,若此时你调用它的interrupt中断方法,如果该线程是通过调用sleep、wait等方法进入BLOCKED或WAITING状态的,那么该线程将修改中断状态值并直接抛出中断异常。

另一方面,如果该线程是通过LockSupport.park方法进入BLOCKED状态的,那么不会抛出中断异常,而是将状态值置为true。

回到话题。

直接调用了sync对象的nonfairTryAcquire方法,这里有有一个注意点,无论ReentrantLock被指定为公平还是非公平,tryLock操作都是非公平的,这样设计是合理的。也是我们之前提到的,为什么nonfairTryAcquire逻辑写在了Sync中而不是NonfairSync中的原因。

public boolean tryLock(){
    return sync.nonfairTryAcquire(1);
}

下面几个方法相信不用我解读也已经是一目了然。

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout)) ;
}

public void unlock() {
    sync.release(1) ;
}

public Condition newCondition() {
    return sync.newCondition();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/835915.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通话降噪算法在手机和IOT设备上的应用和挑战

随着电子产品的升级换代&#xff0c;用户对通话质量的要求也越来越高。通话降噪算法对通话质量起到了关键核心的作用。计算资源的提升使得深度学习模型在便携式的低功耗芯片上面跑起来了&#xff0c;器件成本降低让IoT设备开始使用骨导传感器&#xff0c;&#xff0c;那怎么样才…

提升制造业智能化水平——免费MES系统的领航者

随着工业4.0的持续推进和数字化转型的浪潮涌现&#xff0c;制造业正面临着前所未有的机遇与挑战。在这个数字化时代&#xff0c;企业需要更高效、智能、灵活地管理生产流程&#xff0c;以应对日益激烈的市场竞争。而制造执行系统&#xff08;MES&#xff09;无疑成为了实现这一…

抽象类的顶级理解

目录 1.抽象类的介绍 2. 抽象类语法 3.模板设计模式 1.抽象类的介绍 在面向对象的概念中&#xff0c;所有的对象都是通过类来描绘的&#xff0c;但是反过来&#xff0c;并不是所有的类都是用来描绘对象的&#xff0c;如果 一个类中没有包含足够的信息来描绘一个具体的对象&…

【Vue3项目实战】vue3项目基于el-menu封装左侧菜单栏组件

文章目录 概述一、先看效果1.1 静态效果1.2 动态效果 二、核心思路三、全量代码3.1 文件目录结构3.2 /sidebar/index.vue 中3.3 /sidebar/sidebarItem.vue 中3.4 路由表结构 四、代码讲解五、SVG组件六、系列文章友链1、[配置husky、stylelint、commitlint&#xff0c;实现git提…

庄懂的TA笔记(十九)<特效:顶点 平移+缩放+旋转+幽灵夜巡效果)>

庄懂的TA笔记&#xff08;十九&#xff09;&#xff1c;特效&#xff1a;顶点 平移缩放旋转幽灵夜巡效果)&#xff1e; 大纲&#xff1a; 效果展示&#xff1a; 正文&#xff1a; 一、顶点平移&#xff1a; 1、代码实现&#xff1a; 1.1、声明移动范围&#xff0c;移动速度。 _…

‘<>‘ cannot be used with anonymous classes

‘&#xff1c;&#xff1e;‘ cannot be used with anonymous classes <>不能与匿名类一起使用 Description Resource Path Location Type <> cannot be used with anonymous classes SearchHitSupport.java /spring-data-elasticsearch/src/main/java/org/spri…

QWidget样式

1、设置边框样式&#xff1a; QWidget {font-family:Microsoft YaHei UI;background:#ffffff;/*border:3px solid rgba(207, 209, 208, 170);设置整体边框*/border-bottom: 3px solid rgba(207, 209, 208, 170);/*设置底部边框*/border-top: 3px solid rgba(207, 209, 208, 1…

谷歌云 | 电子商务 | 如何更好地管理客户身份以支持最佳的用户体验

【本文由Cloud Ace整理发布。Cloud Ace是谷歌云全球战略合作伙伴&#xff0c;拥有 300 多名工程师&#xff0c;也是谷歌最高级别合作伙伴&#xff0c;多次获得 Google Cloud 合作伙伴奖。作为谷歌托管服务商&#xff0c;我们提供谷歌云、谷歌地图、谷歌办公套件、谷歌云认证培训…

docker创建镜像并上传云端服务器

docker创建镜像并上传云端服务器 docker容器与镜像的关系1.基本镜像相关文件创建1.1 创建dockerfile文件1.2.创建do.sh文件1.3 创建upload_server_api.py文件1.4 创建upload_server_webui.py文件1.5 文件保存位置 2. 创建镜像操作2.1 创建镜像2.3 创建容器2.2 进入环境容器2.3 …

leetcode----JavaScript 详情题解(4)

目录 2722. 根据 ID 合并两个数组 2723. 添加两个 Promise 对象 2724. 排序方式 2725. 间隔取消 2726. 使用方法链的计算器 2727. 判断对象是否为空 2624. 蜗牛排序 2694. 事件发射器 2722. 根据 ID 合并两个数组 现给定两个数组 arr1 和 arr2 &#xff0c;返回一个新…

Flink 系列四 Flink 运行时架构

目录 前言 介绍 1、程序结构 1.1、Source 1.2、Transformation 1.3、Sink 1.4、数据流 2、Flink运行时组件 2.1、Dispatcher 2.2、JobManager 2.3、TaskManager 2.4、ResourceManager 3、任务提交流程 3.1、standalone 模式 3.2、yarn 模式 4、任务调度原理 4…

AI智慧安监视频监控汇聚平台EasyCVR调用接口出现跨域现象该如何解决?

视频监控汇聚EasyCVR可拓展性强、视频能力灵活、部署轻快&#xff0c;可支持的主流标准协议有GB28181、RTSP/Onvif、RTMP等&#xff0c;以及厂家私有协议与SDK接入&#xff0c;包括海康Ehome、海大宇等设备的SDK等&#xff0c;能对外分发RTSP、RTMP、FLV、HLS、WebRTC等格式的视…

Scrum敏捷开发流程图怎么画?

1. 什么是Scrum敏捷开发流程图&#xff1f; Scrum敏捷开发流程图是一种可视化工具&#xff0c;用于形象地描述Scrum敏捷开发方法中的工作流程和活动。Scrum敏捷开发流程图展示了项目从需求收集到产品交付的整个开发过程&#xff0c;帮助团队理解和跟踪项目进展&#xff0c;促…

vue使用FullCalendar插件实现日历会议预约功能

目录 1. vue 项目使用npm安装插件 2. vue 页面代码&#xff08;直接复制粘贴可用&#xff09; 3. vue FullCalendar的内置函数以及配置 前言&#xff1a;此案例是用FullCalendar插件做一个会议日程预约功能&#xff0c;此功能可查看自己的日程安排会议信息等...... FullC…

国产化车载智能座舱方案引领新时代

车载智能座舱是一项集成了多种技术的复杂工程&#xff0c;包括大量的硬件设备、大数据分析、实时交互、用户体验和技术创新研发等。由于涉及的技术领域繁多&#xff0c;智能座舱技术在实际应用中面临很多技术壁垒&#xff0c;如硬件性能、互联互通、集成性、数据采集、存储、处…

Flink正常消费一段时间后,大量反压,看着像卡住了,但又没有报错。

文章目录 前言一、原因分析二、解决方案 前言 前面我也有提到&#xff0c;发现flink运行一段时间后&#xff0c;不再继续消费的问题。这个问题困扰了我非常久&#xff0c;一开始也很迷茫。又因为比较忙&#xff0c;所以一直没有时间能够去寻找答案&#xff0c;只是通过每天重启…

智慧医院该啥样?白皮书给你答案

注意&#xff1a;案例数据均为虚拟数据 随着云计算、大数据、物联网、区块链、新一代互联网通信等新技术的不断发展,“新基建”的不断升级,新医改的不断深化,智慧医院成为我国医院现代化建设的重要发展方向。 党的十八大以来&#xff0c;数字经济更是上升为国家战略&#xff…

运动式蓝牙耳机哪种好、口碑最好的运动蓝牙耳机

为了保持身体健康&#xff0c;许多人在闲暇时选择进行一些日常运动。其中&#xff0c;很多人喜欢在运动时戴上耳机&#xff0c;让身体随着音乐的节奏运动&#xff0c;希望能够增强运动效果。正因如此&#xff0c;市场上涌现了许多优秀的运动耳机品牌&#xff0c;它们推出了一系…

人脸识别场景下Faiss大规模向量检测性能测试评估分析

在前面的两篇博文中&#xff0c;主要是考虑基于之前以往的人脸识别项目经历结合最近使用到的faiss来构建更加高效的检索系统&#xff0c;感兴趣的话可以自行移步阅读即可&#xff1a; 《基于facenetfaiss开发构建人脸识别系统》 Facenet算法的优点&#xff1a;高准确率&#…

MQTT协议详解「概念、特性、版本及作用」

MQTT&#xff08;Message Queuing Telemetry Transport&#xff0c;消息队列遥测传输&#xff09;是ISO标准下基于发布/订阅方式的轻量级消息协议。MQTT通常使用TCP / IP&#xff08;传输控制协议/Internet协议&#xff09;作为其传输&#xff0c;但也可以使用其他双向传输。MQ…