Reentrantlock和背后金主AQS —————— 开开开山怪

2024-02-07 07:48

本文主要是介绍Reentrantlock和背后金主AQS —————— 开开开山怪,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1.Reentrantlock那些常用的方法
lock()方法
unlock()方法
lockInterruptibly()方法
2.等待队列图解

在这里插入图片描述

说起lock方法,其实lock() 和 unlock()相当于synchronize的左花括号和右花括号,但是当我们用到lock()的时候,需要手动的进行再合适的地方unlock()这样的话才能够释放锁,而synchronize到右花括号结束的话就释放锁。
下来我们就看看线程之间是如果争夺这个锁的。
先设一个前提,有三个线程都想要获取同一把锁进行对应的操作,Reentrantlock背后是怎样处理的

public class ReentrantLock implements Lock, java.io.Serializable {private static final long serialVersionUID = 7373984872572414699L;private final Sync sync;public ReentrantLock() {//无参的初始化默认为非公平同步sync = new NonfairSync();}public ReentrantLock(boolean fair) {//对于我们进行初始化的时候//Sync类继承了AQS类//FairSync类继承了Sync 为公平锁//NonfairSync继承了Sync  为非公平锁sync = fair ? new FairSync() : new NonfairSync();}public void lock() {//sync.lock();}public void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}public void unlock() {sync.release(1);}
}

其实在Reentrantlock中的代码很简单,但是不止我上边所展示的这些。
从上边的代码看。其实不管是这三个方法的哪一个方法,都离不开Sync,其实Sync是Reentrantlock的一个静态内部类。
Sync 继承了 AQS,这说明对于真正Reentrantlock可以做到同步的效果AQS这个类真是功不可没,并且成员sync也是有一定的分量。

剧透
对于同一个锁来说,也相当于是同一个Reentrantlock对象来说,
这个对象中维持一个等待队列,确切的说是背后的AQS对象维持了一个等待队列,Reentrantlock是依靠AQS的。这个等待队列是一个双向的链表。AQS通过一个 volitile int state 来保证线程之间的同步。
先康康AQS中的主要成员。

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizerimplements java.io.Serializable {private transient volatile Node head;private transient volatile Node tail;private volatile int state;

由于AQS类继承了AOS,所以AQS也相当于拥有AOS类的成员。

public abstract class AbstractOwnableSynchronizer implements java.io.Serializable {private transient Thread exclusiveOwnerThread;
}

插曲公平锁 和 非公平锁

我觉得例子总是让我们清晰易懂。
脑补此时有三个线程,想要获取同一个锁,如果第一个线程抢占到CPU资源,那么第一个线程会通过lock方法首先拥有锁,拥有锁体现为AQS中的int state变量会变为1.

当线程一的时间片段到了,此时线程一并没有运行到unlock方法,那就说明此时的锁没有被释放,同时也体现在AQS中的int state变量没有被更改回0.

此时线程二和线程三依次运行起来,检查AQS中int state的值为1,说明锁被其他线程所持有,那么线程二和三则被加入到AQS维持的等待队列中。

1.公平锁:
如果此时线程一释放了锁,其实释放锁的同时会唤醒等待队列中最老的线程(此时为线程二),此时线程二依旧在等待队列中保存,只有线程二获取到锁,才会从等待队列中删除。
那么此时如果线程一和线程二同时竞争CPU资源,线程一竞争到了cpu资源,又开始获取锁,对于公平锁来说,那当然要维持公平了,所以线程一在获取锁的过程中,程序先检查等待队列中是否有线程正在等待,如果有的话,先看最老的等待线程是否和当前想要获取锁的线程是同一个线程,很明显此时最老线程为线程二,当前线程为线程一,所以会直接将当前的线程一阻塞起来并加入到等待队列中(加入到线程三的后面)。
线程二此时因为已经被唤醒了,只是正等待竞争到cpu然后获取锁,在获取锁后就将自己在等待队列中删除。
公平锁相当于,只要是在等待队列中的线程都有获取锁的机会。
优点:所有的线程都能够得到资源,不会产生饥饿的状态
缺点:吞吐量会下降,因为不管有几个线程进行竞争,只要不是等待队列中的等待的线程,先加入到等待队列中,只允许按照申请锁的顺序进行获取锁,cpu唤醒等待线程的开销会增大

2.非公平锁:
看完上边公平锁,非公平锁也很好理解,当线程一再次想要获取锁的时候,程序不管等待队列的是否还有其他从未获取过锁的线程在等待,只要线程一再次抢占到cpu资源可以再次拥有锁。
此时就必得是谁能先抢占CPU的资源。这样就可能导致等待中的某些线程一直处于等待的状态,甚至没有获得锁的机会。
优点:可以减少CPU唤醒线程的开销,
缺点:会导致有些等待线程饿死的现象,申请的早,但迟迟没有机会获取锁。

下来我们就具体说说这个lock的方法和unlock方法(我拿非公平锁做例子
先看这个一层层的调用关系,你们可以对照源码比较方便,同时也是展示有用的代码,其他就不说了。

一.lock():

public class ReentrantLock implements Lock, java.io.Serializable { public void lock() {sync.lock();}

这边的sync.lock()调用的是NonfairSync中的lock()方法;NonfairSync是Reentrantlock的静态内部类。

static final class NonfairSync extends Sync {final void lock() {//采用CAS操作将AQS中的state变量进行修改为1,表明当前线程已经拥有锁if (compareAndSetState(0, 1))//将AQS中的exclusiveOwnerThread变量设置为当前的线程setExclusiveOwnerThread(Thread.currentThread());else//对于如果其他线程持有锁,而其当前的线程也想要获取锁的话//当前线程会运行到这里,因为AQS中的state状态已经被其他持有锁//的线程从0修改为1,当前线程只能去尝试获取锁。acquire(1);}}
acquire();
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizerimplements java.io.Serializable {public final void acquire(int arg) {//tryAcquire方法是由NonfairSync类进行实现的。//tryAcquire方法中进行AQS中状态的获取,// 如果是0,表明现在没有任何线程持有锁,那么会将AQS中的状态改为1.并将AQS中的exclusiveOwnerThread设为当前执行的线程。并且返回true。  //如果是1,表明已经有线程持有锁了,那判断AQS中的exclusiveOwnerThread是否是当前线程,如果是当前线程,那此时表明当前线程重入,那么只是将AQS的状态加1,返回true//如果不是当前运行的线程,那说明当前线程想要获取锁,而此时锁已经被占有那只能返回false。表明当前线程获取锁失败。if (!tryAcquire(arg) &&//当前线程试图想要通过tryAcquire(arg)获取锁acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();//当tryAcquire(arg)返回值为false时,有必要进行acquireQueued()的判断//返回false说明获取锁失败,那么就要对此种情况进行处理。//addWaiter(Node.EXCLUSIVE)方法就是将当前线程形成一个Node节点放入AQS维持等待队列中,返回值为Node。//在加入到队列的过程中,如果队列为空,表示AQS中的head 和tail的值为null,AQS也是通过head和tail来维持这个等待队列的。//同时也说明当前线程是第一个加入到等待队列中的,等待队列是一个带头节点的双向链表,//那么此时会先生成一个空的node作为头节点,然后再将当前线程所形成的节点加入头节点之后,形成这个等待队列.// acquireQueued()方法将当前的Node节点中的线程,也就是当前线程阻塞起来。//具体的过程看下面两个方法具体过程}
}
Node节点主要成员
static final class Node {static final int CANCELLED =  1;static final int SIGNAL    = -1;static final int CONDITION = -2;static final int PROPAGATE = -3;    volatile int waitStatus;volatile Node prev;volatile Node next;volatile Thread thread;Node nextWaiter;
}
acquireQueued()
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizerimplements java.io.Serializable {final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {//p为线程所包装的节点的前驱节点,因为在addwaiter的方法中已经将当前线程//包装为一个Node几点并且加入到等待队列中了//p可能为空的头节点,也可能为已经加入到等待队列中的其他线程所构成的节点final Node p = node.predecessor();//如果为头节点,表明当前线程是所构成的节点是第一个加入到等待队列中的,此时由机会去获取锁,//为什么这么说呢,跟排队一样当然是第一个排队的人最可能先得到服务//这里也一样,如果此时是等待队列中的第一个,那么有机会进行tryAcquire(arg),也就是锁的获取操作,如果成功获取锁,那就说明当前线程所形成的节点不用在等待队里中了,那么会进行一次队列删除操作。if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}//当当前线程获取锁没有成功时,就会进入到下面的判断,// shouldParkAfterFailedAcquire(p, node)这个方法主要是来判断当前线程是否具备阻塞的条件,//新节点刚加入的时候是不满足阻塞条件的,那么此时会再次循环//此时就会跑到上面的if判断,相当于又给了线程一个次获得锁的机会,当for循环第二次到 shouldParkAfterFailedAcquire(p, node)时候,此时当前线程就满足了阻塞条件,//因为第一次的时候shouldParkAfterFailedAcquire(p, node),将新加入的节点的前驱节点的状态改为SIGNAL,返回false//第二次的时候检查当前节点节点的前驱节点状态已经为SIGNAL,所以返回true。//只要当前节点的前驱节点的状态为SIGNAL,就表明当前线程可以进行阻塞。if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())// parkAndCheckInterrupt()方法就是当shouldParkAfterFailedAcquire(p, node)方法返回true的时候就执行,执行的内容就是将当前线程进行阻塞,如果当前线程设置类中断标志,那么当该线程被唤醒的时候返回true//如果没有设置中断标志,那么该线程被唤醒的时候返回false//并且当前线程已经形成节点在等待队列中了,就等着其他线程释放锁,在等待队里中国唤醒自己。interrupted = true;}} finally {if (failed)cancelAcquire(node);}}
}
shouldParkAfterFailedAcquire(p, node)
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizerimplements java.io.Serializable {private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {int ws = pred.waitStatus;//得到当前线程所在节点的前驱节点的状态,if (ws == Node.SIGNAL)//如果前驱节点的状态为SIGNAL(-1)状态,说明此事该线程的阻塞条件达到,//因为在之后在释放锁的时候会在等待队列中去唤醒头节点的的后驱节点//每一个等待节点都有可能变为头节点,在下图会看的更详细//所以当前节点的前驱节点的状态应该是SIGNAL状态,因为这样在之后能够根据它的前驱节点为SIGNAL而唤醒当前节点。return true;if (ws > 0) {//ws大于0表明前驱节点的状态为CANCELLED状态,表明为取消状态do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {//表示前驱节点为状态为0;//因为等待队列中的最后一个节点状态始终为0;//所以对新加入到等待队列中的节点来说它的前驱节点为当前队列中的最后一个节点//对于新加入的节点首次进入shouldParkAfterFailedAcquire方法//首先通过下面的这个方法将前驱节点的状态设置为SIGNAL状态。compareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}

二.Unlock方法()

 public void unlock() {sync.release(1);}
release(int)
 public final boolean release(int arg) {if (tryRelease(arg)) {//进行锁的释放,更改AQS中的state,和拥有锁的线程Node h = head;//此时的head就是等待队列的头节点//每次当一个线程释放自己有的锁的时候,//总是唤醒该锁等待队列中那个最早等待的,也就是头节点的后驱节点if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;}

/对于释放锁来说,如果一个线程产生了重入锁的情况那么
下面方法的getstate就不太是1. 可能会是2,3,4…

tryRelease(int)
protected final boolean tryRelease(int releases) {
//getstate得到AQS中的state的值,如果某一线程只获得过一次锁,那么此时该线程想要释放锁的话c = 1 -1 = 0;
//如果之前某一线程产生了重入锁的情况,那么此时getstate不再是1,
//所以对于重入锁的情况来说,现线程重入锁几次,就要释放锁几次,不然在那些等待队列中的线程是无法被唤醒的
//当某一线程只获得过一次锁,那么该线程此时释放锁的的话,c = 0int c = getState() - releases;if (Thread.currentThread() != getExclusiveOwnerThread())throw new IllegalMonitorStateException();boolean free = false;if (c == 0) {free = true;setExclusiveOwnerThread(null);//将AQS中将保存之前持有锁的线程的变量赋值为null。}//并且将AQS的state改为c的值setState(c);return free;}
unparkSuccessor(Node)
private void unparkSuccessor(Node node) {
//参数为头节点int ws = node.waitStatus;if (ws < 0)//表示头节点的状态为SIGNAL,将头节点的状态改为0compareAndSetWaitStatus(node, ws, 0);Node s = node.next;//得到头节点的后驱节点if (s == null || s.waitStatus > 0) {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);}

三. lockInterruptibly()

对于lockInterruptibly和lock来说不一样的一点在于lockInterruptibly将lock调用过程的的acquireQueued()方法换成了doAcquireInterruptibly方法。

采用lockInterruptibly方法进行获取锁,表明的是可以处理中断,意思很简单,就是两个线程都想要获取同一个锁,另外一个线程获取了锁,而当前的线程由于没有获取锁而加入到了等待队列,但是如果当前的线程被别的线程设置了Thread.interrupt。
那么此时当前被唤醒的性质就不是说由于某一个线程释放锁而被唤醒,而是因为中断被唤醒,所以在 doAcquireInterruptibly(int arg)方法中if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())成立之后,
那么此时这个线程被唤醒后就直接
throw new InterruptedException();由外部处理,说明当前线程放弃了获取锁。

   private void doAcquireInterruptibly(int arg)throws InterruptedException {final Node node = addWaiter(Node.EXCLUSIVE);boolean failed = true;try {for (;;) {final Node p = node.predecessor();if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return;}if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())//当由于中断唤醒,则直接抛出异常throw new InterruptedException();}} finally {if (failed)cancelAcquire(node);}}

应用例子:

public class Interrupt implements Runnable{private static final Lock lock = new ReentrantLock();private volatile int selected;public Interrupt(int select) {this.selected = select;}public void run() { if(selected == 0) {try { lock.lockInterruptibly();for(int i = 0; i < 1000; i++) {} lock.unlock();     }catch (InterruptedException e) {System.out.println("线程一说:我不想等了");}}else if(selected == 1){try {lock.lockInterruptibly();Thread.sleep(1000);lock.unlock();ystem.out.println("线程二说: 锁我用完了");} catch (InterruptedException e) {}}}
public class Test {public static void main(String[] args) {Thread one = new Thread(new Interrupt(0));Thread two = new Thread(new Interrupt(1));two.start();one.start();  one.interrupt();

在这里插入图片描述

2.等待队列的图解

在这里插入图片描述
在这里插入图片描述

这篇关于Reentrantlock和背后金主AQS —————— 开开开山怪的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

【编程底层思考】详解Java的JUC多线程并发编程底层组件AQS的作用及原理

Java中的AbstractQueuedSynchronizer(简称AQS)是位于java.util.concurrent.locks包中的一个核心组件,用于构建锁和其他同步器。AQS为实现依赖于FIFO(先进先出)等待队列的阻塞锁和相关同步器提供了一套高效、可扩展的框架。 一、AQS的作用 统一同步状态管理:AQS提供了一个int类型的成员变量state,用于表示同步状态。子类可以根据自己

重启顺风车的背后,是高德难掩的“野心”

以史鉴今,我们往往可以从今天的事情中,看到古人的智慧,也看到时代的进步。就如西汉后期文学家恒宽曾说的,“明者因时而变,知者随事而制”。 图源来自高德官方 近日,高德就展现了这样的智慧。在网约车市场陷入饱和状态时,高德审时度势,宣布重启顺风车业务,并在全国范围内大规模启动,首批覆盖珠三角、长三角及湖北省武汉市等共计65座城市,完成在出行服务领域的又一重要布局。 重启顺风车,增量市场的“蛋糕

深入理解java并发编程之aqs框架

跟synchronized 相比较,可重入锁ReentrankLock其实原理有什么不同?   所得基本原理是为了达到一个目的;就是让所有线程都能看到某种标记。synchronized通过在对象头中设置标记实现了这一目的,是一种JVM原生的锁实现方式。而ReentrantLock以及所有的基于Lock接口的实现类,都是通过一个volitile修饰的int型变量,并保证每个线程都能拥有对该in

黑神话悟空背后的技术揭秘与代码探秘

《重塑神话:黑神话悟空背后的技术揭秘与代码探秘》 引言 在国产游戏领域,《黑神话:悟空》无疑是一颗璀璨的明星,它不仅融合了深厚的中国文化元素,更在技术上实现了诸多突破,为玩家带来了前所未有的沉浸式体验。本文将深入剖析《黑神话:悟空》背后的关键技术,并通过代码案例展示其技术实现的魅力。 一、高精度动作捕捉技术 《黑神话:悟空》中的角色动作之所以如此逼真,得益于高精度动作捕捉技术的应用

【C++】C++ STL探索:Vector使用与背后底层逻辑

C++语法相关知识点可以通过点击以下链接进行学习一起加油!命名空间缺省参数与函数重载C++相关特性类和对象-上篇类和对象-中篇类和对象-下篇日期类C/C++内存管理模板初阶String使用String模拟实现 在string类文章中提及了STL容器间的接口是大差不差的,本篇将直接通过模拟实现Vector来讲解底层实现与使用。 🌈个人主页:是店小二呀 🌈C语言笔记专栏:C语言笔

Docker核心原理解读:深度剖析Docker Daemon,掌控容器背后的引擎

容器技术已经成为现代应用程序开发和部署中的核心工具,而在Docker生态系统中,Docker Daemon 扮演着至关重要的角色。它不仅是Docker架构的核心,还负责容器的管理、镜像的操作、资源的分配等复杂任务。本文将深入解读Docker Daemon的工作原理,探讨它在Docker系统中如何高效运行,以及它如何与其他组件协同工作。 一、Docker架构回顾 在深入了解Docker Daem

<编码:隐匿在计算机软硬件背后的语言>示例电路列表

<<编码: 隐匿在计算机软硬件背后的语言>>一书中的示例电路的线上可交互示例. 关于 <<编码: 隐匿在计算机软硬件背后的语言>> 一书的介绍, 参考豆瓣的介绍: 分章节介绍包含了各章节示例电路的简要操作说明, 电路截图以及在线交互操作链接, 方便读者按图查找. 点击以下或左侧书签栏各章节链接进入章节介绍. 第4章 手电筒剖析(Anatomy of a Flashlight) 示例电路

java线程 yield,sleep,join,synchronized wait notify notifyAll,ReentrantLock lock condition, 生产者消费者

yield,sleep,join yield,join,sleep,join是Thread中的方法,不需要 在synchronized 代码块中调用,和synchronized 没关系,也不会释放锁。 Thread.sleep(100);Thread.yield();Thread t;t.join(); (1)yield()不一定保证让出cpu yield()只是使当前线程重新回

Java并发之AQS与自旋锁(利用CAS实现)

一、概述   谈到并发,不得不谈ReentrantLock;而谈到ReentrantLock,不得不谈AbstractQueuedSynchronizer(AQS)!   类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock/Semaphore/CountDownLatch...。   以下是本文的目录大

王楠首次讲述Cocos Creator背后的故事

Cocos Creator发布至今,得到了许多开发者的支持和喜爱,甚至有小伙伴留言说:幸福来得太突然。水滴石穿,非一日之功。这款工具从诞生到问世究竟经历了怎么样的曲折,未来又会走向何方?这方面,大概没有谁比Cocos Creator制作人王楠更有发言权了。   今天不妨抽出10分钟,听听王楠的讲述,相信或多或少会对你有所启发。   开发Cocos Creator的初衷是什么?   我和几