ReetrantLock源码分析
ReentrantLock类的大部分逻辑,都是其均继承自AQS的内部类Sync实现的
啥是AQS:
Java并发编程核心在于java.concurrent.util包而juc当中的大多数同步器实现都是围绕着共同的基础行为,比如「等待队列、条件队列、独占获取、共享获取」等,而这个行为的抽象就是基于AbstractQueuedSynchronizer简称AQS 它定义了一套多线程访问共享资源的同步器框架,是一个「依赖状态(state)的同步器」。
以公平锁为例子:
public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(true); lock.lock(); //加锁 断点处 try { Thread.sleep(5000); }catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); }
公平锁、非公平锁
public ReentrantLock(boolean fair) { //ReetrantLock的有参构造函数 sync = fair ? new FairSync() : new NonfairSync(); }
在加锁行打断点运行, 我们可以看到参数:
image-20201105163128535
记住这几个值,后面会用到.
单步步入 F7
image-20201105163427610
看到lock()调用了Sync实例中的lock()方法
我们可以看到Sync是在ReentrantLock类中的一个抽象内部类 继承于AbstractQueuedSynchronizer (AQS)(抽象队列同步器)
点击Sync类中lock抽象接口方法 我们可以发现ReetrantLock有公平和非公平锁两种方式实现方式.由于本章我们使用公平锁讲解所以我们选择公平锁的实现方式继续向下调试代码
/** * Sync object for fair locks */ static final class FairSync extends Sync { //公平锁实现方式 继承于Sync private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1);//获取锁 传入参数1 } ..
向下⬇️
public final void acquire(int arg) { //acquire方法接收参数 1 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //这里有两个操作 使用短路与 如果第一个操作成功第二个操作不执行 //1.尝试以独占模式获取锁,在成功时返回 //2.线程进入队列排队 selfInterrupt(); }
方式1 首先尝试获取锁
/** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { //参数:1 final Thread current = Thread.currentThread(); //获取当前线程 int c = getState(); //还记得我们第一个断点截图的state值是0 if (c == 0) { //是0 说明我们是第一次获取锁,如果不是0说明是重入,会进入else if (!hasQueuedPredecessors() && //公平锁和非公平锁,主要是在方法 tryAcquire 中,是否有 !hasQueuedPredecessors() 判断。 compareAndSetState(0, acquires)) {//通过cas操作state更新,期望值是0,新值是1 setExclusiveOwnerThread(current);//设置当前拥有独占访问的线程。 return true; } } else if (current == getExclusiveOwnerThread()) { //如果 是重入情况 会进入这里. //进入前会判断是否是当前线程拿的锁 int nextc = c + acquires; //假设重入次数为2 那么nextc = 2+1 =3 if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); //更新state值 return true; } return false; }
先判断state是否为0,「如果为0就执行上面提到的lock方法的前半部分」,通过CAS操作将state的值从0变为1,否则判断当前线程是否为exclusiveOwnerThread,然后把state++,也就是重入锁的体现,「我们注意前半部分是通过CAS来保证同步,后半部分并没有同步的体现」,原因是:后半部分是线程重入,再次获得锁时才触发的操作,此时当前线程拥有锁,所以对ReentrantLock的属性操作是无需加锁的。「如果tryAcquire()获取失败,则要执行addWaiter()向等待队列中添加一个独占模式的节点。」
public final boolean hasQueuedPredecessors() { Node t = tail; // 根据初始化顺序倒序获取字段 Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } //在这个判断中主要就是看当前线程是不是同步队列的首位,是:true、否:false //这部分涉及公平锁的实现,CLH(Craig,Landin andHagersten)。三个作者的首字母组合
啥是CLH
CLH锁即Craig, Landin, and Hagersten (CLH) locks。CLH锁是一个自旋锁。能确保无饥饿性。提供先来先服务的公平性。 为什么说JUC中的实现是基于CLH的“变种”,因为原始CLH队列,一般用于实现自旋锁。而JUC中的实现,获取不到锁的线程,一般会时而阻塞,时而唤醒。
1.获取不到锁的线程,会进入队尾,然后自旋,直到其前驱线程释放锁,具体位置是放在tail后的null位置,并让新对象的next指向Null 2.如果head成功拿到了锁 此时 把head中包含的线程指向为Null并将head的前任设置为head后清除现任head
方式2 没获取到锁,需要进入队列排队:
进入方式需要改造项目
让多个线程进行抢占资源
改造如下:
import java.util.concurrent.locks.ReentrantLock; class Scratch { public static void main(String[] args) { ReentrantLock lock = new ReentrantLock(true); for (int i = 0; i < 5; i++) { new Thread(()->{ lock.lock(); try { Thread.sleep(100); System.out.println(Thread.currentThread().getName()+"工作结束...."); }catch (InterruptedException e) { e.printStackTrace(); }finally { lock.unlock(); } }).start(); } } }
回到这里发现等待队列addWaiter方法添加了一个Null 将自己加入CLH队列的尾部 关注公众号:[JAVA宝典]
image-20201105171529838
发现等待队列addWaiter方法添加了一个Null null的含义是:
使用null是私有占用资源模式
,使用new Node()是 共享模式
.(顺带一提,writelock不互斥,就是使用的共享模式)
Node还有几个等待状态:
/** * Status field, taking on only the values: * SIGNAL: The successor of this node is (or will soon be) * blocked (via park), so the current node must * unpark its successor when it releases or * cancels. To avoid races, acquire methods must * first indicate they need a signal, * then retry the atomic acquire, and then, * on failure, block. * CANCELLED: This node is cancelled due to timeout or interrupt. * Nodes never leave this state. In particular, * a thread with cancelled node never again blocks. * CONDITION: This node is currently on a condition queue. * It will not be used as a sync queue node * until transferred, at which time the status * will be set to 0. (Use of this value here has * nothing to do with the other uses of the * field, but simplifies mechanics.) * PROPAGATE: A releaseShared should be propagated to other * nodes. This is set (for head node only) in * doReleaseShared to ensure propagation * continues, even if other operations have * since intervened. * 0: None of the above * * The values are arranged numerically to simplify use. * Non-negative values mean that a node doesn't need to * signal. So, most code doesn't need to check for particular * values, just for sign. * * The field is initialized to 0 for normal sync nodes, and * CONDITION for condition nodes. It is modified using CAS * (or when possible, unconditional volatile writes). */ volatile int waitStatus;
在addWaiter(Node.EXCLUSIVE)处断点:
/** * 根据给定的模式为当前节点创建一个Node * * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared // * @return the new node */ private Node addWaiter(Node mode) { //线程对应的Node 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的前驱指向尾节点 node.prev = pred; //将当前node设置为新的尾节点 //如果cas操作失败,说明线程竞争 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //lockfree的方式插入队尾 enq(node); //只有在 tail == null时才进入 return node; }
先找到等待队列的tail节点pred,如果pred!=null,就把当前线程添加到pred后面进入等待队列,如果不存在tail节点执行enq()
private Node enq(final Node node) { //经典的lockfree算法:循环+CAS for (;;) { Node t = tail; //尾节点为空 if (t == null) { // Must initialize //初始化头节点 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
这里进行了循环,「如果此时存在了tail就执行同上一步骤的添加队尾操作,如果依然不存在,就把当前线程作为head结点。」 插入节点后,调用acquireQueued()进行阻塞
/** * Acquires in exclusive uninterruptible mode for thread already in * queue. Used by condition wait methods as well as acquire. * * @param node the node * @param arg the acquire argument * @return {@code true} if interrupted while waiting */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
先获取当前节点的前一节点p,如果p是head的话就再进行一次tryAcquire(arg)操作,如果成功就返回,否则就执行「shouldParkAfterFailedAcquire、parkAndCheckInterrupt来达到阻塞效果;」
unlock
在unlock处打断点
进入了
public void unlock() { sync.release(1); }
image-20201106142501425
继续跟进发现进入release方法,继续查看tryRelease(arg)方法
尝试释放锁有三种实现 我们点ReetrantLock实现方式
源码:
protected final boolean tryRelease(int releases) { //最后进入到实际解锁源码中 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) //判断持有线程和当前执行线程是否是同一个,否则报错 throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { //判断释放掉releases参数后是否是0 如果 为0 设置当前排他锁的占有线程为Null free = true; setExclusiveOwnerThread(null); } setState(c);//更新state为0 return free;//返回解锁是否成功 }

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
IntelliJ IDEA2020.3预告!全新的提取方法布局,崭新的用户体验
IntelliJ IDEA是Java语言开发的集成环境,IntelliJ在业界被公认为优秀的Java开发工具之一,尤其在智能代码助手、代码自动提示、重构、J2EE支持、Ant、JUnit、CVS整合、代码审查、 创新的GUI设计等方面的功能可以说是超常的。 点击下载IntelliJ IDEA最新试用版 我已经在之前的博客文章中介绍了许多您将在IntelliJ IDEA 2020.3中使用的最主要功能。今天我们还为您准备了更多的内容。在这篇博文中,您将发现 Extract Method 重构的新用户体验,了解如何在 IntelliJ IDEA 中默认打开任何文件,并了解 IntelliJ IDEA for Kubernetes 中的新功能。 Java 新的提取方法布局 我们更新了Extract Method重构的用户体验,让它变得更快、更方便。当你选择一个你想要提取的代码片段并按下 "⌥⌘M或选择Refactor / Extract/Introduce / Method...时,IDE会立即对其进行重构,而不会显示一个对话框窗口。 在执行这个重构后,你可以点击方法名称旁边的齿轮图标,并在...
- 下一篇
Prometheus监控告警浅析
前言 最近有个新项目需要搞一套完整的监控告警系统,我们使用了开源监控告警系统Prometheus;其功能强大,可以很方便对其进行扩展,并且可以安装和使用简单;本文首先介绍Prometheus的整个监控流程;然后介绍如何收集监控数据,如何展示监控数据,如何触发告警;最后展示一个业务系统监控的demo。 监控架构 Prometheus的整个架构流程可以参考如下图片: 整个流程大致分为收集数据,存储数据,展示监控数据,监控告警;核心组件包括:Exporters,Prometheus Server,AlertManager,PushGateway; Exporters:监控数据采集器,将数据通过Http的方式暴露给Prometheus Server; Prometheus Server:负责对监控数据的获取,存储以及查询;获取的监控数据需要是指定的Metrics格式,这样才能处理监控数据;对于查询Prometheus提供了PromQL方便对数据进行查询汇总,当然Prometheus本身也提供了Web UI; AlertManager:Prometheus支持通过PromQL来创建告警规则,如果...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- CentOS8编译安装MySQL8.0.19
- CentOS8安装Docker,最新的服务器搭配容器使用
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Mario游戏-低调大师作品
- Docker安装Oracle12C,快速搭建Oracle学习环境
- CentOS7安装Docker,走上虚拟化容器引擎之路
- CentOS6,7,8上安装Nginx,支持https2.0的开启