我所理解的Java锁

随着程序的运行,有新的线程要进入临界区,通过CAS竞争锁失败。Mard Work立即将偏向锁标记锁为轻量级锁,因为已经发生了竞争条件。紧接着,会反复同通过CAS为线程获取锁,如果占有锁的线程在临界区待的时间很短,那么申请锁的线程将很快拿到锁。

因此,轻量级锁意味着,有竞争条件,但是大家能很快地被分配到锁。

重量级锁

当然,申请锁的线程并不总是能很快地获取到锁,与其反复地CAS重试而浪费CPU时间,不如直接将线程阻塞住。那么,在轻量级锁的情况下,如果有线程超过一定次数的重试还是获取不到锁,Mard Work立即将轻量级锁标记为重量级锁,此后所有获取不到锁的线程将被阻塞,需要Monitor的参与。

因此,重量级锁意味着,在有竞争条件的情况下,线程不能很快地被分配到锁。

Synchronized的锁只能膨胀,不能收缩。偏向锁和轻量锁为乐观锁,重量级锁为悲观锁。

Synchronized的好处在于,它的优化、锁申请释放、锁的分配都是自动的,开发者能快速地使用。

Lock语义

Synchronized虽然能完成大多的并发场景,但是却可能造成线程阻塞且时长不可知。“如果去餐厅吃饭,客满了我想离开而不是等待”,Synchronized就满足不了这样的场景。并且,有时候我们想控制锁的分配过程,更甚地,我们喜欢VIP通道,希望让一些线程更优先地获取到锁。

Lock也就有了它的舞台:

public interface Lock {
void lock(); // 获取锁,获取不到会被阻塞
void lockInterruptibly() throws InterruptedException; // 获取锁,可被中断,获取不到会被阻塞
boolean tryLock(); // 获取锁,无论结果如何不会被阻塞
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 获取锁,最多在unit时间内返回结果,可被中断
void unlock(); // 释放锁
Condition newCondition(); // 支持满足一定条件后,再去获取锁
}

Lock接口提供了一套实现一种锁,所应具有的方法语义,实现一种锁时,应当考虑如何满足Lock所表达的功能,并具备本身的特点。

一种Lock锁所应具有的特点为:

  • 可以像Synchronized一样,获取不到就阻塞,以lock()表达语义
  • 也可以在获取锁的过程,对中断进行响应,以lockInterruptibly()和tryLock()表达表达
  • 还可以在获取不到锁时,自行抉择等待多久,然后做进一步打算,以tryLock()表达语义
  • 并支持了一种条件锁,让线程等待时机,等一种事件达成,然后再去获取锁,看起来就如栅栏一样,以Condition表达语义

Lock与Synchronized最鲜明的对比为可中断,不强制阻塞,并表达了Synchronized所不支持的条件锁特性。

AQS基础

锁的处理分为了两部分,一部分为如何加解锁,另一部分为把锁分配给谁。在Synchronized时,这两部分都是透明的,只是以关键字进行了标记。而当要实现一种锁时,就不得不周全这两部分的内容,其中将有种种需要注意的细节。

为了将更多的精力放在“如何加解锁”上,以表达不同的锁的特性,Java抽象出了AQS(AbstractQueuedSynchronizer)来协助实现Lock。AQS解决了“将锁分配给谁”的问题。

以下,就为AQS的运行机制的概要,更具体的可以参考:一文了解AQS(AbstractQueuedSynchronizer)

AQS运行概要.png

  1. 当申请锁,即调用了与acquire()类似语义的方法时,AQS将询问子类是否上锁成功,成功则继续运行。否则,AQS将以Node为粒度,记录这个申请锁的请求,将其插入自身维护的CLH队里中并挂起这个线程。
  2. 在CLH队列中,只有最靠近头节点的未取消申请锁的节点,才有资格申请锁。
  3. 当线程被唤醒时,会尝试获取锁,如果获取不到继续挂起;获取得到则继续运行。
  4. 当一个线程释放锁,即调用release()类似语义的方法时,AQS将询问子类是否解锁成功,有锁可以分配,如果有,AQS从CLH队列中主动唤起合适的线程,过程为2、3。
  5. 如果需要等待条件满足再去申请锁,即调用了wait()类似语义的方法时,在AQS中表现为,以Node为粒度,维护一个单向等待条件队列,把Node所代表的线程挂起。
  6. 当条件满足时,即调用了signal()类似语义的方法时,唤醒等待条件队列最前面的未取消等待的Node,执行1。
  7. 子类可以维护AQS的state属性来记录加解锁状态,AQS也提供了CAS的方法compareAndSetState()抢占更新state。

关键点在于,通过AQS申请锁的线程,都可通过CAS进行锁竞争,state表达分配了多少把锁,CAS能保证代表锁状态的state的原子性,那么,就可以在有必要的时候将线程挂起。当线程被唤醒时,再次参与锁竞争流程。从外部看,就如入口方法被阻塞住并在合适的未来被恢复了一样。

有了AQS,可以看其他锁,是如何实现Lock语义并具有哪些特性。

ReentranLock(可重入锁)

ReentranLock实现了Lock语义,并具AQS的特性,是悲观锁、独占锁、可重入锁,是否公平与是否可中断则取决于使用者。

ReentranLock以其内部类Sync继承AQS特性,在实例化时,可以通过参数决定是否公平。ReentranLock只允许一个线程持有锁,因此它是独占锁,其他申请锁的线程将因此而挂起等待。

ReentranLock的可重入性表现在,当锁被线程持有,AQS询问是否加锁成功时,Sync如果发现申请的线程与持有锁的线程是同一个,它将通过CAS更新state状态再次分配锁,并回复加锁成功。也就实现了重入。

是否公平体现在,在向AQS申请分配锁时,有一次询问是否加锁成功的机会,在此时是否忽略CLH队列中等待的线程,就代表了是否给予插队的机会。

具体的实现原理可见:ReentranLock

ReentrantReadWriteLock(读写锁)

ReentrantReadWriteLock也实现了Lock语义,具备了AQS的特性,ReentrantReadWriteLock是可重入锁。

ReentrantReadWriteLock即是悲观锁,也是乐观锁;即是独占锁,也是共享锁。何出此言?

ReentrantReadWriteLock的应用场景,是针对于读操作远多于写操作的场景,以读锁写锁共同协作。整体来看,ReentrantReadWriteLock锁具有的特性,就取决择于观察的时间段。

只有读锁

在一段时间里,如果只有读锁,那么ReentrantReadWriteLock是共享锁,是乐观锁。这是容易理解的,读操作并不会改变数据的状态,也就没有竞争条件,此时,大家都能获取到锁,通过临界区,CLH队列里没有线程在排队。

只有写锁

只有写锁.png

在一段时间里,如果只有写锁那么ReentrantReadWriteLock是悲观锁,是独占锁。在这种情况下ReentrantReadWriteLock表现得与ReentranLock一样。因为此时竞争条件激烈,只能让线程逐个通过临界区。

读写锁都有

读写锁都有.png

在一段时间里,如果读写锁都有,那么ReentrantReadWriteLock是悲观锁。虽然读锁不会有竞争条件,但因会读到过期的数据,因此需要等写锁完成后才进行分配,大家都需要进入CLH队列排队。

值得注意的是,如果写锁前面有读锁没有释放,写锁就要进行等待,在读锁处理的过程中,数据也不应当过期,这样,就提供了一个时间窗口让读锁安心处理,也让写锁更具独占的意义。

可重入性与是否公平

是否公平与ReentranLock一样,借助AQS解决把锁分配给谁的实现类,都可通过在首次请求锁时,选择是否忽略CLH队列中的情况,实现是否插队。

在实现可重入性时,写锁因是独占的,可以直接通过state维护,而当是读锁,是分享锁时,就需要借助其他内容记录每一个线程的重入情况。ReentrantReadWriteLock就通过ThreadLocal在各个线程内部维护了类型为HoldCounter的对象记录此信息。

特别的,拥有都读锁的线程可以继续申请写锁,反之则不行。

具体实现原理可见:ReentrantReadWriteLock

Semaphore(信号量)

Semaphore的内部类Sync继承了AQS的特性,实现了除条件锁外的Lock语义(但没有直接声明implementation)。

Semaphore是具有不可重入的特性,特点为一次可申请多个锁,是所看到的锁方案中难见到的不支持重入的锁。

Semaphore的场景为,如何并发地占用有限的共享资源。比如餐位,如果没有餐位了,就不会接待新一批的客人。 Semaphore不支持重入.png

Semaphore不支持重入的原因在于,因为资源的有限性,重入可能引起死锁。以一个极端的餐位例子举例:如果正在进食的客人,都要求申请更多的餐位,但此时已没有更多的餐位,那么,申请不到餐位引起等待,而等待的客人不愿完成进食放出餐位。

Semaphore公平与不公平的特性,也是取决于首次去向AQS申请锁时,是否考虑CLH队列的情况。

具体实现可参考:Semaphore

其他特性

除了以上的,锁应考虑具有的特性之外,还有其他的一些,锁所具有的独特特性,代表一种具体实现。

条件锁

条件锁意味着,等待条件达成的线程,在条件满足前,都将被挂起。当条件满足后,放过一些线程去申请锁,这使得条件锁很像栅栏。 条件锁.png

Java提供了Condition作为条件锁的方法语义模板,以await()表达等待条件,以signal()表达条件达成信号。

借助AQS实现的条件锁亦是如此。其中维护了一个条件等待队列,所有await()的线程以Node的形式进入队列,并在signal()信号到来后,让某些Node进入到CLH队列。

自旋锁

自旋锁属于无锁状态,得益于CAS能保证单一变量的原子性,那么其他仅依赖单一变量的临界区就可以使用CAS加解锁。其操作为,通过不断循环地尝试CAS,直到成功,也称为自旋

自旋锁基于一种假设,线程处于临界区足够短,通过不断地浪费CPU时间自旋至获取锁成功更有效率。因为在自旋锁的要针对的场景里,比起阻塞、唤起线程的上下文切换所引起的性能消耗,自旋浪费CPU时间的消耗反而更小。

分段锁

有时候,没必要把所有的共享资源都放在同一个位置,如同去银行办理业务,可以选择不同的柜台。这也是分段锁的意义:将共享资源存于不同的区域,细化锁的粒度,使得对一部分资源的竞争,不会影响到另一部分资源。

以ConccurrentHashMap在JDK7中的实现为例,就以Segment为类型的数据结构对数据分段,并且每个Segment是一个ReentrantLock。如此,不同的数据分布在不同的区域,相应的访问者到对应的位置进行竞争。

总结

文章以为什么需要锁开始,陈述了synchronizedLock的实现,窥探了Java中锁存在的形式。

在考虑实现一种锁时,需要考虑悲观与乐观、独占与共享、公平与否、是否重入、是否可中断的特性,还可以进一步考虑是否支持条件锁的语义。

synchronized通过 偏向锁 -> 轻量级锁 -> 重量级锁 的锁膨胀过程提升了效率,因其为底层实现,将有更多的想象空间。
实现Lock语义的锁,通过AQS解决了将锁分配给谁的问题,得以聚焦于自身的加解锁方式上,满足并形成了各种锁之间的不同特性。掌握了AQS之后,大可以借助其实现具有业务特性的锁。
当然,还会看到各式各样对于锁的称呼,那么就需要考虑这种锁的特性,是作为锁要考虑的共同特性之一,还是它仅有的独特特点。
在提升锁的效率的方案中,处处可以见到CAS的身影,借以说明了没有锁才是期望的锁。那么,在面对并发时,可以从 是否需要锁 -> 是否CAS可以解决 -> 是否可以不阻塞 -> 是否需要某种特性的阻塞锁,这样的选择路径寻找更合适的方案。
以上,错误之处,不吝赐教。
参考

Synchronized原理
java CAS原理
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

最后

经过日积月累, 以下是小编归纳整理的深入了解Java虚拟机文档,希望可以帮助大家过关斩将顺利通过面试。
由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。







由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
12925181467)]

由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 15
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值