笔记主要参考《Java并发编程的艺术》并且基于JDK1.8的源码进行的刨析,此篇只分析独占模式,后续在ReentrantReadWriteLock和 CountDownLatch中 会重点分析AQS的共享模式
锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁可以防止多个线程同时访问共享资源(这种锁称为独占锁,排他锁)但是有些锁可以允许多个线程并发访问共享资源,比如读写锁
| 方法 | 作用 |
|---|---|
| void lock() | 获取锁,调用该方法的线程将会获取锁,当锁获得之后从该方法返回 |
| void lockInterruptibly() | 可中断地获取锁,该方法会响应中断,在锁的获取途中可以中断当前线程,如果在获取锁之前设置了中断标志,or获取锁的中途被中断or其他线程中断该线程则抛出InterruptedException并清除当前线程的中断标识 |
| boolean tryLock() | 尝试非阻塞的获取锁,调用方法会立即返回,如果获取到锁返回true |
| boolean tryLock(long time, TimeUnit unit) throws InterruptedException | 超时获取锁,从当前返回有三种情况 1.超时时间内获取到锁 2.当前线程在超时时间内被中断 3.超时间结束没有获取到锁,返回false |
| void unLock | 释放锁 |
| Condition newCondition() | 获取等待通知的组件,该组件和当前锁绑定,只有获取到锁调用wait方法后当前线程将放弃锁,后续被其他线程signal继续争抢锁 |
synchronized相比于Lock 更加简单,更不容易犯错,但是不够灵活

获取锁的过程不要写在try中,避免获取锁失败最后finally释放其他线程持有的锁

使用一个int成员变量state表示同步状态,内置的FIFO队列来完成资源的获取和线程的排队工作,支持独占也支持共享的获取同步状态。

三个变量(head队列头,tail队列尾,state同步状态)被volatile修饰,保证其线程可见性
| 方法 | 说明 |
|---|---|
| protected boolean tryAcquire(int arg) | 独占的获取锁,需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS改变同步状态 |
| protected boolean tryRelease(int arg) | 独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态 |
| protected int tryAcquireShared(int arg) | 共享式的获取同步状态,返回大于等于0的值表示成功,反之失败 |
| protected boolean tryReleaseShared(int arg) | 共享式释放同步状态 |
| protected boolean isHeldExclusively() | 当前队列同步器释放再独占模式下被线程占用,一般表示当前线程是否独占 |
| 方法 | 说明 |
|---|---|
| void acquire(int arg) | 独占式获取同步状态,如果获取成功那么直接返回,反之进入同步队列中等待, |
| void acquireInterruptibly(int arg) | 和acquire,但是此方法支持在获取锁的过程中响应中断,如果当前线程被中断那么抛出InterruptedException |
| boolean tryAcquireNanos(int arg, long nanosTimeout) | 在acquireInterruptibly的基础上增加了超时限制,如果在指定时间内没有获取到同步状态那么返回false反之true |
| void acquireShared(int arg) | 共享式获取同步状态,如果没有获取到那么进入等待队列等待,和acquire不同的式支持同一个时刻多个线程获取同步状态 |
| void acquireSharedInterruptibly(int arg) | 和acquireShared类似但是支持响应中断 |
| boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 在acquireSharedInterruptibly新增了超时限制 |
| boolean release(int arg) | 独占式释放同步资源,在释放同步状态后唤醒后继线程 |
| boolean releaseShared(int arg) | 共享式释放同步状态 |
Collection<Thread> getQueuedThreads() |
获取等待在同步队列上的线程们 |
| 属性 | 描述 |
|---|---|
| int waitStatus | 等待状态 |
| Node pre | 前驱节点 |
| Node next | 后继节点 |
| Node nextWaiter | 等待队列中的后继节点,如果当前节点式共享模式,那么这个节点是SHARED常量,也就是说节点类型和等待中后继节点是公用一个字段 |
| Thread thread | 获取同步状态的线程 |
等待状态是一个枚举,具备下列可选的值
AQS中包含两个节点类型引用:头节点和尾节点。当一个线程获取到同步状态的时候,其他线程无法获取,将被放入到同步队列中,加入队列这个过程为了保证线程安全而采用CAS。同步队列遵守FIFO,头节点是获取到同步状态的线程,释放同步状态将会唤醒后继线程,后继节点获取到同步状态后将被设置为头节点
支持公平和非公平和重入的独占式锁
公平锁在头节点释放同步资源的时候unpark后续节点,然后进行线程上下文切换由后继节点中的线程获取锁,导致效率并不如非公平锁(非公平锁也会唤醒后继节点,但是非公平锁接下来获取到锁的线程不一定是队列中被挂起的线程,也许是刚刚来到厕所门口的无赖小瘪三直接上来抢夺了厕所使用权,所以在当前线程唤醒后继线程的途中可能锁已经被小瘪三霸占了,提高了厕所的利用率),但是公平锁可以减少饥饿,因为非公平锁好像A在排队,A获取到共享资源需要进行唤醒和上下文切换,而导致需要更多时间,这时候流氓B刚好进厕所门,上来就是一个CAS,很快抢占了厕所这一共享资源,导致A处于饥饿——迟迟得不到厕所(共享资源)的操作资源。
实现可重入需要解决两个问题
第一个问题ReentrantLock通过获取当前线程和独占锁线程的==判断来实现,第二个问题ReentrantLock通过对AQS中的共享资源state增加和减少来实现

ReentrantLock的公平和非公平就是由于sync引用指向了的不同实现,其lock unlock等操作也是一律交由到sync
加锁的大致流程



如果nonfairTryAcquire返回true表示当前线程获取到了锁,那么皆大欢喜,当前线程可以继续运行
返回false的情况
这两种情况都需要继续执行AQS的acquire方法
独占模式获取共享资源,对中断不敏感,或者说不响应中断——获取共享资源失败的线程将会进入到同步队列,后续对此线程进行中断操作,线程不会从同步队列中移出


快速入队
下面这段代码值得品一品
Node pred = tail;
if (pred != null) {
//当前线程的前置设置为尾,多个线程执行这一步也是无关紧要的
//只是把当前节点的前置改变了,不是改变pred的next指向,所以不存在线程安全问题
node.prev = pred;
//CAS设置尾节点 为当前节点,这个自旋操作compareAndSetTail是线程安全,同一时间只有一个线程可以设置自己为尾节点
if (compareAndSetTail(pred, node)) {
//注意 如果原尾节点是S,线程A设置成功 那么尾巴被修改为了A,
//假如A执行下面一行的时候消耗完了时间片,线程B进来了,这时候线程B拿到的tail就是A,所以不会存在线程安全问题
pred.next = node;
return node;
}
}
完整入队

完整入队和快速入队差不多,就是多了一个初始化的逻辑
那么为什么不直接完整入队,也许是for循环比if多更多的字节码需要执行?也许Doug Lea测试多次后发现快速入队后完整入队,比直接完整入队效率更高

如何从自旋中退出
前继节点是头节点(头节点是当前获取到共享资源的节点)并且获取共享资源tryAcquire成功
挂起当前线程避免无休止的自旋
自旋是cpu操作,无限制的自旋是很浪费cpu资源的
是否挂起当前线程——shouldParkAfterFailedAcquire方法

如果shouldParkAfterFailedAcquire返回true 表示当前线程需要被挂起,会继续执行parkAndCheckInterrupt,这个方法很简单只有两行
private final boolean parkAndCheckInterrupt() {
//挂起当前线程
LockSupport.park(this);
//返回中断状态,并且清除中断标识
return Thread.interrupted();
}
如果parkAndCheckInterrupt 返回了true 表示当前线程被中断过,并且会让外层的acquireQueued返回true,会导致acquire执行当前线程的自我中断


理解这一段代码需要对java中断机制具备一定理解
调用Thread的interrupt方法
interrupt,interrupted,isInterrupted三个方法比较
interrupt 见上⬆
interrupted 返回当前线程的中断标识并且充值中断标识
isInterrupted返回中断标识
我们继续说为什么当前线程在获取锁的途中被中断,需要自我中断一下
acquire的"需求":
独占模式获取共享资源,对中断不敏感,
或者说不响应中断——获取共享资源失败的线程将会进入到同步队列,后续对此线程进行中断操作,线程不会从同步队列中移出
线程获取同步状态的时候被中断会发生什么——从LockSupport.park(this)中返回继续拿锁,这就是为什么说acquire的对中断不敏感。
LockSupport.park();不会抛出受检查异常,当出现被打断的情况下,线程被唤醒后,
我们可以通过Interrupt的状态来判断,我们的线程是不是被interrupt的还是被unpark或者到达指定休眠时间
哪为什么线程拿到锁退出后发现曾经被中断过后,会进行自我中断昵————假如我们写如下这样的代码执行

如果拿到锁返回的时候不进行自我中断,那么中断标识将会被复位,假如存在一个调度线程中断了上面的线程,但是上面的线程还在抢夺锁,并且被park了,这时候上面线程的park会返回,并且清除中断标识,如果不进行自我中断,那么下面while内容还是会进行,那么我们调度线程的中断就无效了。哪为什么doug lea在线程获取锁被中断之后要复位线程的中断标识昵————因为被中断的线程无法再次被park方法挂起,如果不复位那么线程将一直自旋尝试获取锁,浪费cpu资源。也就是说获取锁之后如果获取锁的途中被中断了进行自我中断的这个操作是在模拟中断是在获取到锁之后来的(无论是在抢锁之前,还是抢锁之后的中断,对于acquire来说都是如此)

使用此构造方法,传入true获取一个公平锁

公平锁的lock方法直接调用AQS的acquire方法,上面我们分析的acquire方法它会先去调用tryAcquire,这个tryAcquire被FairSync重写
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//共享状态当前空闲
if (c == 0) {
//前面没有节点 这就是公平是怎么实现的
//且cas成功 那么拿到锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//实现重入 和 公平锁一样
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
源码没什么很难的点,就是通过判断前面时候还有节点(标识是否由线程比当前线程先到)如果没有那么再去拿锁,如果共享状态不是0且当前线程不是独占的线程那么就会执行acquireQueued方法,在acquireQueued里面自选获取锁会判断前一个节点是否是头节点且调用tryAcquire
释放锁直接调用AQS的release方法,其中tryRelease方法由ReentrantLock中Sync自己实现(公平or非公平都一样)
public final boolean release(int arg) {
//完全的释放资源
if (tryRelease(arg)) {
Node h = head;
//头节点初始化的时候才为0,但是后面如果由节点加入到同步队列会把前置节点的状态设置为Singnal
if (h != null && h.waitStatus != 0)
//唤醒后继节点
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//重入了n次,当前释放m次 c=n-m
int c = getState() - releases;
//如果不是独占锁的线程 那么抛出异常
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
//是否完全的释放了锁
boolean free = false;
//只有剩下的为0 才是完全释放锁
if (c == 0) {
//置为true
free = true;
//独占线程设置为null
setExclusiveOwnerThread(null);
}
//修改state
setState(c);
return free;
}
需要注意的是只有完全的释放了共享资源(在ReentrantLock里就是加锁n次解锁n次)才返回true,才会去唤醒后继节点
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(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)//大于0代表放弃了
s = t;
}
if (s != null)
//唤醒
LockSupport.unpark(s.thread);
}
使用 LockSupport.unpark(s.thread)唤醒线程,这里需要品一品 Doug Lea 他为什么要从尾部开始唤醒
再品入队
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)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
快速入队
快速入队要求尾节点不为空,如果尾节点为空那么说明
完整入队
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//假设AB两个线程现在正在抢锁
node.prev = t;
//CAS设置尾 当前线程A被设置为了尾
if (compareAndSetTail(t, node)) {
//假如A执行这一行的时候用完了时间片,轮到了B
//B把自己设置了尾并且B的前置是A,此时A的前置还没来得及设置
//如果这个时候进行唤醒,从头开始遍历的话会发现没有后面的节点了
//所以需要从尾开始,找到B,B继续往前找到A
//Doug Lea 永远的神
t.next = node;
return t;
}
}
}
}
为什么要从尾开始遍历

这部分都是调用的nonfairTryAcquire方法,也就是是说无论是公平还是非公平都是直接不公平的获取资源。tryLock方法是直接尝试,只有当前共享资源没有被占用的时候返回true,否则false 并且是立即返回所以无论是公平还是非公平,调用这个方法都是一样的逻辑——有人占着厕所那就直接回去继续工作
这个方法直接调用了AQS的acquireInterruptibly(1)
public final void acquireInterruptibly(int arg)
throws InterruptedException {
//如果已经中断了那么抛出中断异常
//Thread.interrupted() 会清除中断标识,因为抛出InterruptedException就是响应了中断,
if (Thread.interrupted())
throw new InterruptedException();
//调用公平or非公平自己复写的方法
if (!tryAcquire(arg))
//如果尝试获取共享资源失败了 那么入队,自旋的共享资源
doAcquireInterruptibly(arg);
}

基本上和acquireQueued差不多,就是自旋时发现中断了那么抛出中断异常,注意parkAndCheckInterrupt是调用的Thread.interrupted(),会清除中断标识
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
//不是interrupted 而是 isInterrupted
return Thread.isInterrupted();
}
使用interrupted会重置中断标识,因为若中断表示没有重置,
线程再次获取锁进行自旋将无法被park方法挂起导致无限自选耗费cpu资源,
且如果不重置中断那么用户的业务逻辑会收到影响(while(当前线程没有中断){执行业务代码})
【2022-7-11 评论区一名博主指出,这是他的主页https://www.cnblogs.com/sunankang/】
放弃共享资源的争抢,一般是等待超时,或者被中断后响应中断

private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// 删除放弃的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
//当前节点的前置节点
Node predNext = pred.next;
//设置当前节点状态为取消
node.waitStatus = Node.CANCELLED;
// 如果当前节点是尾节点,且CAS设置前置节点为尾节点成功
//在共享模式中可能存在当前线程放弃,但是后续存在线程入队的情况
//所以这里的CAS可能失败
if (node == tail && compareAndSetTail(node, pred)) {
//CAS设置前置节点的next为null 相当于删除自己
compareAndSetNext(pred, predNext, null);
} else {
//当前节点不是尾节点,
//或者说在共享模式下,上面一步的compareAndSetTail失败
int ws;
//前置不是头,头节点释放后自然会唤醒后继
//且 前置节点已经是SIGNAL 或者CAS设置前置节点状态为SIGNAL成功
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
//CAS设置前置的next是当前节点的后继 删除自己
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
//前置是头 或者前置不是SIGNAL或者CAS设置SIGNAL(说明前置也放弃)
//直接唤醒后继节点,后继节点会继续自旋获取锁
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
总体上就是,如果当前节点前面存在节点可以唤醒当前节点的后继节点,那么为二者建立联系,否则直接唤醒后面的节点,最终都会把自己从队列移除
直接调用了AQS的tryAcquireNanos方法
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
//上来就判断下是否中断了
if (Thread.interrupted())
throw new InterruptedException();
//尝试获取锁 (调用对应公平和非公平的方法)or doAcquireNanos
//所以返回true的情况是 上来tryAcquire尝试CAS成功 或者调用doAcquireNanos超时等待获取锁成功
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}

大致逻辑还是那些,需要注意的是,nanosTimeout > spinForTimeoutThreshold,剩余时间大于阈值(1000)才会挂起,如果小于那么还是进行自旋,因为非常短的超时时间无法做到十分精确(挂起和唤醒也是需要时间的)进行超时等待反而会表现得不精确
我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co
我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i
鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende
给定一个复杂的对象层次结构,幸运的是它不包含循环引用,我如何实现支持各种格式的序列化?我不是来讨论实际实现的。相反,我正在寻找可能会派上用场的设计模式提示。更准确地说:我正在使用Ruby,我想解析XML和JSON数据以构建复杂的对象层次结构。此外,应该可以将该层次结构序列化为JSON、XML和可能的HTML。我可以为此使用Builder模式吗?在任何提到的情况下,我都有某种结构化数据-无论是在内存中还是文本中-我想用它来构建其他东西。我认为将序列化逻辑与实际业务逻辑分开会很好,这样我以后就可以轻松支持多种XML格式。 最佳答案 我最
一、引擎主循环UE版本:4.27一、引擎主循环的位置:Launch.cpp:GuardedMain函数二、、GuardedMain函数执行逻辑:1、EnginePreInit:加载大多数模块int32ErrorLevel=EnginePreInit(CmdLine);PreInit模块加载顺序:模块加载过程:(1)注册模块中定义的UObject,同时为每个类构造一个类默认对象(CDO,记录类的默认状态,作为模板用于子类实例创建)(2)调用模块的StartUpModule方法2、FEngineLoop::Init()1、检查Engine的配置文件找出使用了哪一个GameEngine类(UGame
目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称
最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总
深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal
了解Rails缓存如何工作的人可以真正帮助我。这是嵌套在Rails::Initializer.runblock中的代码:config.after_initializedoSomeClass.const_set'SOME_CONST','SOME_VAL'end现在,如果我运行script/server并发出请求,一切都很好。然而,在我的Rails应用程序的第二个请求中,一切都因单元化常量错误而变得糟糕。在生产模式下,我可以成功发出第二个请求,这意味着常量仍然存在。我已通过将以上内容更改为以下内容来解决问题:config.after_initializedorequire'some_cl
我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or