源码解读--AbstractQueuedSynchronizer讲解之Reentrantlock实现

本文主要是介绍源码解读--AbstractQueuedSynchronizer讲解之Reentrantlock实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一.锁

  说起AbstractQueuedSynchronizer(传说中的AQS),可能有些同学不知道,但是说到ReentrantLock,CountDownLatch,Seamphore,大家可能用用过吧。他们都是用锁来实现了,而锁里面又分独占锁和分享锁。具体锁的种类请看文章。ReentrantLock是独占锁,而CountDownLatch,Seamphore是分享锁。锁里面还有一个重要的分类:公平锁和非公平锁。java源码系列。

公平锁:线程一个一个排队,确保先来的那个等待的线程最早执行。

非公平锁:不能保证先来的那个等待的线程最早执行。

二.AQS

AQS是一个抽象类,经常被使用的是它的子类Sync,会具体实现AQS里面一些方法。AQS里面主要有三个变量。

private transient volatile Node head;  //队列头结点
private transient volatile Node tail;  //队列尾
private volatile int state;  //同步的状态

用了一个双向的链表来存储当前线程信息。大致示意图如下



原理介绍:

以ReentrantLock为例,他的初始状态state=0,当有线程获取锁的时候,state加1,接着后面还有线程过来获取这个锁,就需要在等待队列里面等待。如果之前的线程释放锁,那么后面等待队列里面的线程就可以获取到锁,执行任务。如果期间有中断,这个线程也会被终止掉。

三.源码实现

这里主要介绍以ReentrantLock为基础介绍AQS。

3.1 ReentrantLock锁初始化

public ReentrantLock() {  //默认是new一个非公平的锁,线程之间需要按顺序排队,效率会高一点sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {  //通过true和false来指定是否创建一个公平锁sync = fair ? new FairSync() : new NonfairSync();
}

3.2ReentrantLock 获取锁

public void lock() {  //获取锁sync.lock();  //默认用的是非公平锁,那我们就先来看看非公平锁的实现
}
final void lock() {if (compareAndSetState(0, 1))   //如果还没有线程获取到资源,就将当前线程设置到独占锁setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);  //如果已经有线程占用了资源,其他线程需要排队
}
 
public final void acquire(int arg) { //尝试着去获取资源,如果没有获取到,就会把当前线程封装成一个节点放到同步队列里面,如果添加队列成功,就获取队列里面的资源,如果期间发生了中断,就将当前线程中断掉if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt(); //如果线程在阻塞的过程中发生了中断,自身线程也需要中断
}
protected final boolean tryAcquire(int acquires) {  return nonfairTryAcquire(acquires);
}
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 == getExclusiveOwnerThread()) {  //检查当前线程是独占模式,如果成立的话,这里可以是线程重入int nextc = c + acquires;   //资源增加acquires个
        if (nextc < 0) // 资源添加的超出int的范围,抛出越界异常
            throw new Error("Maximum lock count exceeded");
        setState(nextc);  //设置资源的状态
        return true;
    }return false;
}

private Node addWaiter(Node mode) {  //线程没有获取到资源,就需要放到同步队列里面Node node = new Node(Thread.currentThread(), mode);  //由当前线程和独占模式创建一个节点
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {  //如果队列不是空node.prev = pred;
        if (compareAndSetTail(pred, node)) { //就将这个node节点添加到队列的尾部pred.next = node;
            return node;
        }}enq(node);  //如果队列为空,就需要初始化这个队列,并将这个ndoe放到队列的尾部
    return node;
}
private Node enq(final Node node) {  //初始队列,并且将node节点入队列for (;;) {  //死循环(自旋)Node t = tail;
        if (t == null) { // 如果节点为空,就初始化一个空的节点当做队头和队尾
            if (compareAndSetHead(new Node()))tail = head;
        } else {  //之前if执行完了,队列里面有了一个空的数据node.prev = t;  //t就是那个之前添加的空的节点
            if (compareAndSetTail(t, node)) { //将node放到队尾t.next = node;
                return t;  //for 循环唯一的出口
            }}}
}
final boolean acquireQueued(final Node node, int arg) {  //获取队列资源,这里面会将获取不到资源的线程阻塞,当有资源可以获取的时候,它会接着执行,但是注意返回的中断的状态boolean failed = true; //结果是否是失败
    try {boolean interrupted = false;  //是否发生了中断
        for (;;) {final Node p = node.predecessor();  //找到node的前驱节点
            if (p == head && tryAcquire(arg)) {  //如果前驱节点是head并且获取资源成功setHead(node);  //将node设置为头节点
                p.next = null; //断链,破引用 以便系统GC
                failed = false;  //执行成功了
                return interrupted;
            }if (shouldParkAfterFailedAcquire(p, node) &&  //ndoe前驱节点不是head,需要返回阻塞的标记parkAndCheckInterrupt()) //如果已经返回了阻塞的标记,则阻塞线程,并且如果返回线程中断的状态interrupted = true;
        }} finally {if (failed)  //未知原因,执行失败,则取消获取资源cancelAcquire(node);
    }
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)  //如果前驱节点的状态是signal,直接返回true,是线程去阻塞
        return true;
    if (ws > 0) {// 线程是取消状态
        do {node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);  //从后往前过滤取消线程,直到第一个线程不是取消的线程为止
        pred.next = node;
    } else {  //如果不是取消状态 我们就需要将前驱节点的状态设置为signal,到下一次执行该方法是,前驱节点就是signal,返回为true
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }return false;  
}
private final boolean parkAndCheckInterrupt() { //中断线程 使用LockSupport.park中断当前线程LockSupport.park(this);
    return Thread.interrupted();  //返回的是线程的中断状态,可能这个线程在阻塞的过程中发生过中断
}

小结:到此获取锁已经讲完了。

1.线程先去获取锁,如果可以获取锁,直接返回。

2.如果线程不能获取到锁,就会添加到阻塞队列中,如果队列为空,还需要初始化队列。

3.在线程阻塞的过程中,可能会发生中断,最后可能还需要中断处理一下。

3.3ReentrantLock 释放锁

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

public final boolean release(int arg) {if (tryRelease(arg)) {  //是否可以释放资源Node h = head;
        if (h != null && h.waitStatus != 0) //如果队列头节点不为null并且它的状态不是0unparkSuccessor(h);  //唤醒后继节点
        return true;
    }return false;
}
protected final boolean tryRelease(int releases) {  //是否可以释放资源int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())  //校验如果当前线程不是独占式,那就有问题了,我们知道,我们前面加入的全部都是独占式的throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {  //如果资源全部放出来了free = true;  
        setExclusiveOwnerThread(null); //将独占式的线程设置为null
    }setState(c); //设置资源的状态
    return free;
}
private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)  //如果线程没有被取消,设置前驱node节点的的状态是0compareAndSetWaitStatus(node, ws, 0);
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {//如果后继节点是null或者节点是取消状态s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)  //从后往前一步步找到后继节点s = t;
    }if (s != null)LockSupport.unpark(s.thread); //唤醒后继节点
}

小结:到此释放锁已经讲完了。

1.先去看线程能不能获取到释放资源的能力

2.如果可以的话就跳过那些已经取消了的线程(是头节点的后继节点里面的线程)唤醒。

四.总结

本文RentrantLock为骨,讲解了非公平锁的实现,借助了底层了AQS,下面来总结一下大致的流程:

初始状态state=0,当有线程获取锁的时候,state加1,接着后面还有线程过来获取这个锁,就需要在等待队列里面等待。同步队列里面头结点获取资源后,会唤醒后继节点去获取锁,当他获取到锁就会执行自己的任务。如果期间有中断,这个线程也会被终止掉。


这篇关于源码解读--AbstractQueuedSynchronizer讲解之Reentrantlock实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1036588

相关文章

hdu1043(八数码问题,广搜 + hash(实现状态压缩) )

利用康拓展开将一个排列映射成一个自然数,然后就变成了普通的广搜题。 #include<iostream>#include<algorithm>#include<string>#include<stack>#include<queue>#include<map>#include<stdio.h>#include<stdlib.h>#include<ctype.h>#inclu

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

【Prometheus】PromQL向量匹配实现不同标签的向量数据进行运算

✨✨ 欢迎大家来到景天科技苑✨✨ 🎈🎈 养成好习惯,先赞后看哦~🎈🎈 🏆 作者简介:景天科技苑 🏆《头衔》:大厂架构师,华为云开发者社区专家博主,阿里云开发者社区专家博主,CSDN全栈领域优质创作者,掘金优秀博主,51CTO博客专家等。 🏆《博客》:Python全栈,前后端开发,小程序开发,人工智能,js逆向,App逆向,网络系统安全,数据分析,Django,fastapi

让树莓派智能语音助手实现定时提醒功能

最初的时候是想直接在rasa 的chatbot上实现,因为rasa本身是带有remindschedule模块的。不过经过一番折腾后,忽然发现,chatbot上实现的定时,语音助手不一定会有响应。因为,我目前语音助手的代码设置了长时间无应答会结束对话,这样一来,chatbot定时提醒的触发就不会被语音助手获悉。那怎么让语音助手也具有定时提醒功能呢? 我最后选择的方法是用threading.Time

Android实现任意版本设置默认的锁屏壁纸和桌面壁纸(两张壁纸可不一致)

客户有些需求需要设置默认壁纸和锁屏壁纸  在默认情况下 这两个壁纸是相同的  如果需要默认的锁屏壁纸和桌面壁纸不一样 需要额外修改 Android13实现 替换默认桌面壁纸: 将图片文件替换frameworks/base/core/res/res/drawable-nodpi/default_wallpaper.*  (注意不能是bmp格式) 替换默认锁屏壁纸: 将图片资源放入vendo

C#实战|大乐透选号器[6]:实现实时显示已选择的红蓝球数量

哈喽,你好啊,我是雷工。 关于大乐透选号器在前面已经记录了5篇笔记,这是第6篇; 接下来实现实时显示当前选中红球数量,蓝球数量; 以下为练习笔记。 01 效果演示 当选择和取消选择红球或蓝球时,在对应的位置显示实时已选择的红球、蓝球的数量; 02 标签名称 分别设置Label标签名称为:lblRedCount、lblBlueCount

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略

Kubernetes PodSecurityPolicy:PSP能实现的5种主要安全策略 1. 特权模式限制2. 宿主机资源隔离3. 用户和组管理4. 权限提升控制5. SELinux配置 💖The Begin💖点点关注,收藏不迷路💖 Kubernetes的PodSecurityPolicy(PSP)是一个关键的安全特性,它在Pod创建之前实施安全策略,确保P

MCU7.keil中build产生的hex文件解读

1.hex文件大致解读 闲来无事,查看了MCU6.用keil新建项目的hex文件 用FlexHex打开 给我的第一印象是:经过软件的解释之后,发现这些数据排列地十分整齐 :02000F0080FE71:03000000020003F8:0C000300787FE4F6D8FD75810702000F3D:00000001FF 把解释后的数据当作十六进制来观察 1.每一行数据

Java ArrayList扩容机制 (源码解读)

结论:初始长度为10,若所需长度小于1.5倍原长度,则按照1.5倍扩容。若不够用则按照所需长度扩容。 一. 明确类内部重要变量含义         1:数组默认长度         2:这是一个共享的空数组实例,用于明确创建长度为0时的ArrayList ,比如通过 new ArrayList<>(0),ArrayList 内部的数组 elementData 会指向这个 EMPTY_EL