分享

Java中锁的自我总结(同步队列、等待队列)

 liang1234_ 2020-10-23

关于锁的总结

UML图总结

中间偏下方的 d 是 ReentrantLock 类
在这里插入图片描述
.
.

Lock 接口

一个提供多个 lock();unlock() 等多个函数的接口。

队列同步器

AbstractQueuedSynchronizer 抽象队列同步器。
同时拥有 同步队列 与 等待队列
在这里插入图片描述

阻塞blocked:获取锁(对象锁或者类锁)失败,进入阻塞队列,阻塞线程当前状态被保存下来,cpu切换到其他线程任务。阻塞的任务是由 monitor ?? 唤醒
等待waiting:显示调用 wait(await) ,进入等待状态,释放当前的锁。线程一直在等待,不会引起上下文的切换。
上下文切换:cpu同步时间片分片的方式执行线程,但是如果当前线程执行一段s时间后,因被阻塞等原因需要切换到下一个任务线程,此时,需要将当前线程的状态信息等保存下来,以便再次加载这个任务的状态,从任务保存 — 再次加载就是一次线程的上下文切换。

总结:
.1. lock的实现是在一个class里面的,所以在每个class的实例化对象里面都存在对应的 同步队列 以及 等待队列。
.2. 假如在一个class里面有多个 实现 lock 的类的内部变量,针对不同的方法,但是一个对象仍然只有一把锁,凡是没有获取到锁的都进入到同步队列(synchronized 关键字是对象的同步队列 !!!but!!! lock.lock()是同步器的同步队列,但是同步器中的同步队列是自旋非阻塞的)
.
.3. 同步队列, 首先是获取锁失败,调用park阻塞线程,封装为Node ,然后CAS添加到队列的末尾,然后 在同步队列里面,有头结点的下一个节点被remove掉之后,显式的 调用了 unpark 方法唤醒后继节点去获取锁。这里锁的获取是没有 自旋 的,Node 加入尾部是在CAS循环的。
.4. 等待队列,是已经获取到锁的线程,需要用到其他线程的数据,主动调用 wait() 方法,并且释放锁,唤醒同步队列的后继节点(非公平不用唤醒),然后当前节点构造新的Node进入等待队列,当有其他 在同步队列里面 已经获得锁的线程调用 notify()(或者 signal())之后,才会重新加入同步队列,尝试获取锁。 等待队列的节点也调用了 park () 方法阻塞。
.5. 唤醒同步队列中的后继被阻塞的节点,是通过 lockSupport()(本文后面) 中的 unpark()方法唤醒被阻塞线程。park()阻塞。
.6. 在ReetrantLock 里面,有公平与非公平自旋锁,也是基于 同步器的,同步器的队列是默认阻塞的 ,ReetrantLock 里面是自旋非阻塞的。 ----- 但是 , 非公平的自旋锁,其实是真的在自旋,不会阻塞住, 但是公平的,本来就是按照 FIFO 的方式,直接使用 同步器的FIFO,同步队列里面 前一个unpark 后一个不就行了吗 ??? 应该不存在 自旋这个概念才对 。 但是实际上是,一直在自旋,但是获取的条件多了一个 判断当前节点是不是第一个节点 。 是则获取 。 ------- 所以 ReetrantLock 里面 公平与非公平的 自旋锁,都是在一直自旋没有 park(), 但是 非公平的多了判断是否是头结点 。

Condition 接口

------- 疑问 -------:Condition 接口在ReentrantLock 与 ReentrantReadWriteLock 中是实现在什么地方的? )
回答 : 1 .Condition 接口中的 await() 与 signal() 函数分别用于 是线程进行等待状态---加入到等待队列中 、唤醒等待队列中的首节点的线程。 不是用在同步队列中的。
2 . 同步队列目前分成阻塞 与 非阻塞两种,阻塞的同步队列即 synchronized 修饰的,会直接被阻塞,引起线程上下文的切换;在抽象队列同步器 里面实现的 同步队列是 阻塞的,并且存在 等待队列 。 但是在 ReetrantLock 里面的同步队列,重写了相关方法,里面的同步队列是 自旋非阻塞的,通过循环的原子性获取 state 来获取锁。

在这里插入图片描述

总结 :
1 . 一个抽象队列 同步器 的对象 , 里面存放了 同步队列 的 head tail 指针Node , 还有一种是 ConditionObject 类的对象 。
2 . 同步队列 , head tail ,同步器 只会维持一个同步队列 , head 指针是不存放 实际的 Thread node 的,只用作指针 。
3 . 等待队列 , 另外 , 还有一个或者多个 的Condition 对象 ,每一个Condition 对象 里面 的成员变量有 firstWaiter 和 lastWaiter ,通过封装成为Node 加入队列,是单向的队列 。可以创建多个 ConditionObject 对象,实现多个 等待队列。
4 . await 方法 , 首先加入到 一个 condition对象的 等待队列里面,然后释放之前的锁。再一个while循环自旋判断 是否已经在 同步队列里面,不在同步里面 进入循环,调用 park 阻塞,跳出while(只能已经在 同步队列 或者 中断),如果转移到了同步队列,现在也是阻塞的,然后继续同步器的阻塞唤醒操作、获取锁什么的。(所以这里的wait在等待队列里面也是阻塞住线程)
5 . signal 方法,首先是一个指定的 Condition 对象去调用,那么会将 指定 的 等待队列 里面的第一个 waiter 转移到 同步队列并且是非阻塞的(添加到 同步队列尾部,同样是 CAS )。
.
.
同步队列 → 总的流程 :
假如是在 一个自定义的锁的类 , 一个线程调用了lock 方法,首先调用 acquire 方法请求锁,acquire 里面首先将现场节点封装Node 加入到同步队列的队尾,然后 在 acquireQueued 的循环里面,while 的判断 是不是head ,是则直接return 说明获取到锁,如果不是 则 调用了 park 方法阻塞,但是while 循环的跳出只能是 当前node 是head 。( 但是前面也说了 head 移除之后会 unpark 后面的结点,并且将后面的setHead) ,所以对于每个在抽象队列同步器的同步队列里面的Node,都是阻塞的,但是同步器每次acquire都会有一个acquireQueued ,相当于每个Node 都会 循环的请求,但是循环是同步器发出的,类似于一个“自旋”,这个自旋是 同步器控制的,线程还是阻塞的。
.
等待队列 → 总的流程:
针对一个 ConditionObject 的对象 显式的调用 await 方法,进入这个condition的等待队列里面,释放锁,进入一个 while 循环,只有当线程回到同步队列或者被中断才可以跳出while,while里面 park 掉线程 ----- 在 condition调用 signal 之后,该condition上面的节点的 firstWaiter 首先加入到 同步队列尾部 , 会 unpark 线程,退出 while 之后,又会进入 acquireQueued 的循环里面,同样存在之前的问题。

.
问题 : 为什么不直接全部阻塞住,然后加入到同步队列,每次让head去获取不就好了吗? 为什么非要给每个node 加一个acquireQueued 里面的while循环,并且线程被阻塞了,同步器同时这么多的while循环,负担太大???并且在 等待队列 转移到 同步队列后 先unpark ,然后也会 调用 acquireQueued ??? 为什么不直接阻塞然后每次让头结点去获取???两部分重合的地方是 acquireQueued 这部分,这样算的话,等待队列里面的while 有一次 park,signal里面一次 unpark ,再 acquireQueued 里面的while 第一次判断不是head 之后又会有一次 park ,性能开销是不是太大了???

.
应用场景 : 适用于 生产者 消费者模型,首先获取 lock 锁,然后await ,并且释放锁,等到 signal 之后,再次开始抢锁。
.
可以自定义一个 MySync 继承 AQS ,但是不重写,然后 new 一个 ConditionObject 对象,先lock ,在 await 即可

.
.

可重入锁 ReetrantLock

避免当前以及获取锁的线程再次获取该锁的时候,进入阻塞状态。 进行一次判断是否是自己获取了该锁,判断正确后,再次获取该锁,每次再次获取该锁都需要对state值进行自增,在释放的时候,需要释放相应的次数。

可重入锁的 jar 包的实现中,对该锁 分为了 公平 与 非公平 两种类型。
ReetrantLock 里面的 同步队列没有被 重写,与 AQS 里面应该是一致的。

自旋锁

这是在 ReentrantLock 类下的,在内部实现了三个静态类,Sync继承抽象队列同步器,Sync下面分了 公平与非公平两类。在队列同步器中都涉及到了在同步队列中两种类型的自旋锁是如何工作的。 不同于一般的 synchronized 关键字修饰的情况----直接进入阻塞队列,有锁资源的时候,再让monitor去唤醒 unsafe.unpark()

首先关于自旋锁:在同步队列中,不阻塞线程(避免了 线程阻塞带来的 上下文切换的开销,但是会涉及到 CPU 资源的浪费)

如果一个 线程 争抢 锁失败后,会将它的各种状态信息还有自带的数据 存为一个 Node 的结点类,将该结点加入到同步队列中,非公平性与公平性也是体现在同步队列中。

非公平的自旋锁

> 同步队列 == 阻塞/非阻塞队列 != 等待队列(调用wait方法)

在同步队列中的,非公平的自旋锁,强调每个加入 队列 的线程不会进入阻塞状态,而是不断的死循环通过(volatile + CAS )的方式去获取锁(可以在死循环中设置时间)。这样就可能存在部分线程永远无法获取到锁资源,导致饥饿。所以是非公平的。

总的流程是 :一个新的线程创建 ----- 和当前所有线程进行锁的争抢 ----- 是否成功?执行:为Node节点加入同步队列 ----- 在同步队列中继续进行锁的争抢(死循环,只能通过中断停止 或者 时间)

我的疑问 : 非公平锁 好像 违背了 同步队列 FIFO 的原则,同步队列没有起到作用,而只是起到了维护所有Node的数据的作用。
回答: 非公平锁就是 不断的进行循环获取锁。

公平的自旋锁

在同步队列中,完全维护一个 FIFO 的规则,是完全公平的,它与 非公平的主要区别在于每次循环请求锁的时候,判断条件是前驱节点是否是头结点,如果是头节点,那么当头结点完成并且释放锁之后,将后面的置为head,全部都在自旋,不存在阻塞。

非公平锁 与 公平锁的 的 tryRequire()方法的区别置在于 里面除了关于 CAS 算法获取成功的判断之外,多了一个判断当前节点是否有前驱节点语句 ,这里就与队列的FIFO相关了。即每次自的时候判断一次是否是有前驱节点,如果没有前驱节点说明当前是首部head 并且CAS获取锁成功,则获取到锁。head节点和 tail 节点的引用 是存储在同步器里面的,首部节点是正在使用锁的节点。当 首部节点 完成任务后,线程释放锁,并且断开与后继节点的连接引用,此时 判断语句里面的 hasQueuedPredecessors()方法返回 false。并且CAS修改state值成功的时候,此时后继节点获取到锁,并且将自己设置为首部节点(更改同步器中的引用)。

总的流程是 : 新的线程创建 ----- 获取锁是否成功?执行:生成Node节点加入同步队列尾部并开始自旋 ----- 前驱是否存在? 无法获取锁继续自旋 :是头结点并且通过CAS获取到锁(设置当前线程的节点为头结点)
在这里插入图片描述
在这里插入图片描述

读写锁 ReetrantReadWriteLock

支持 重进入
支持 公平与非公平选择 同样非公平更优
支持 锁降级 写锁 – 读锁 – 释放写锁 的顺序,写锁可降级为读锁

在之前的可重入锁中,是根据 一个状态值 state 的原子性操作,来判断是否可以获取到锁,但是现在 需要将 一个state 变量分为 读锁和写锁 两种状态,需要将 state 的 32 bit 分为两半,根据位操作来判断 当前的锁状态是 读还是写。这一部分实现在 ReetrantReadWriiteLock 类下的 静态类 Sync 中继承 AbstractQueuedSychronizer 实现,重写了很多的方法(同步器中的 共享式 与 独占式)
32个bit 高位 16bit 识别读 地位 16bit 识别写

int C = getState();
写状态 : C & 0x0000FFFF,此时查看当前的值,如果按位与操作之后值为0,说明没有线程获取写锁。
获取写锁的时候,直接 C + 1 改变状态值。
读状态: C >> 16 ,此时值剩余高16bit,查看值,如果值为 0 ,说明没有线程获取 读锁。
获取 读锁 的时候,CAS 方法修改状态值+1,C + ( 1<<16 )。

所以,总的流程是: 首先新建的线程 查看 状态值的高16bit 与 低 16 bit,根据当前的值,判断是否有线程已经持有了该对象锁,已经判断 持有 的是 读锁 还是 写锁。如果是读锁,并且当前线程进行的也是 读 操作,那么可以直接获取读锁,并修改高 16 bit 位的值+1 ,释放的时候做减法 。但是,如果是写锁,线程的任何操作都会被阻塞,此处完全与 ReetrantLock 的运行一致,都是独占(排它)式的。

读锁的获取与释放

读取状态值,判断条件,判断自身此时的操作时读还是写,同样存在自旋的过程。这些部分与之前的部分都一致。

写锁的获取与释放

同 可重入锁 类似,都是 排它 的。

锁降级

解决的问题 : 现在有 N 个线程在运行,多数是读,少数是写。现在一个线程修改了数据,并且将一个 volatile 修饰的标识更改了状态,现在所有的这个状态对所有的线程都是可见的,但是只有一个线程 C 可以获取到写锁,获取写锁根据更新的值,更新该线程 C 需要做的事务。 但是 获取写锁 – 释放写锁 的过程存在问题,即释放写锁后,可能被其他线程 D 获取到写锁,再次更新了值,此时的最新值可能不是 C 更新后的值,是存在问题的。并且,如果线程 C 希望马上用到刚刚更新的值,而却被阻塞,那么上下文的切换开销也大。

锁降级 : 在 获取 到写锁后 ,进行数据修改,并且希望马上用到更新的值。此时 再次 请求 读锁,进行数据修改, 释放写锁。 读取使用数据,最后释放 读锁。
请求写锁 ---- 请求读锁 ---- 释放写锁 ---- 释放读锁 该过程就是 锁的降级,这样在写锁释放的时候,该线程因为持有仍然持有读锁,所以不会被其他线程获取到 写锁。

这种锁降级的方式 很适合用在 一个线程里面存在连续的读写情况 || 长时间的事务被分成部分,每完成部分就获取一次读锁共享给其他线程当前已经更新的部分数据

LockSupport 类

定义了一系列的公有静态方法,用于 阻塞&&唤醒 一个线程。

LockSupport 中的park 与 unpark 等方法,最终都是调用的 Unsafe 类中的方法,而 Unsafe 类中提供的是一系列的硬件级别的原子操作 ,有点类似于 汇编级别或者机器码级别 指令一样

在这里插入图片描述

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多