浅析 synchronized 底层实现与锁相关 | Java(下)

简介: 一切的最开始都是源自为什么?

常见的锁

自旋锁

如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

但是线程自旋是需要消耗 CPU 的,说白了就是让 CPU 在做无用功,线程不能一直占用 CPU 自旋做无用功,所以需要设定一个自旋等待的最大时间。

如果持有锁的线程时间超过自旋等待的最大时间扔没有释放锁,就会导致其他争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

优缺点

自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起操作的消耗。

但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行代码块,这时候就不适合使用自旋锁,因为自旋锁在获取锁前一直都是占用cpu不断做尝试,线程自旋产生的消耗大于线程阻塞挂起操作的消耗。导致其他需要cpu的线程无法获取到cpu,从而造成了cpu的浪费。

自旋锁时间阈值

自旋锁的目的是为了占着 CPU 的资源不释放,等到获取到锁立即进行处理。

如何选择自旋的执行时间呢?如果自旋执行时间太长,会有大量的线程处于自旋状态占用 CPU 资源,进而会影响整体系统的性能,因此自旋次数非常重要。

自适应自旋锁

JVM 对于自旋次数的选择, jdk 1.5 默认为 10次 ,在1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不再是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本上认为是一个线程上下文切换的时间是最佳的时间。

比如线程A 获取了一把锁后,当它释放这把锁之后,线程B成功获得了这把锁后,此时,线程A再次申请获取锁,由于此时线程B还没有释放锁,所以线程A只能自旋等待,但是虚拟机认为:由于线程A刚刚获得过这把锁,那么虚拟机会认为线程A这次自旋也是有可能会再次成功获得该把锁,所以会延长线程A的自旋次数。

对于一个锁,一个线程自旋之后,获取锁成功概率不大,那么以后这个线程要获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁,以免空循环等待浪费资源。

偏向锁

背景

实际开发中,大多数情况下不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入偏向锁,从而减少不必要的 CAS 操作。

概括

偏向锁,顾名思义,它会偏向第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁,从而减少加锁/解锁 的一些 CAS 操作(比如等待队列中的 CAS 操作(CLH队列锁) ) 。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会挂起,JVM 会消除它身上的偏向锁,并将锁恢复到标准的轻量级锁。它通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。

偏向锁升级过程

网络异常,图片无法展示
|

1.访问 Mark Word 中偏向锁的标识是否被设置成1  ,锁标志位 是否为 01 ,确认其为可偏向状态;

2.如果是可偏向状态,则测试 线程ID 是否指向当前线程,如果是,进入步骤5,否则进入步骤3;

3.如果 线程ID 并未指向当前线程,则通过 CAS 操作竞争锁。如果竞争成功,则将 Mark Word 中 线程ID 设置为当前线程ID ,然后执行 步骤5 ; 如果竞争失败,则执行步骤 4;

4.如果 CAS 获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。

5.执行同步代码。

偏向锁的释放

偏向锁的撤销在步骤4中已经提过。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放偏向锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(即在这个时间点上没有字节码正在执行),它会首先暂停拥有锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为"01") 或轻量级锁(标志位为"00")的状态。

适用场景

始终只有一个线程在执行代码块,在它没有执行完释放锁之前,没有其他线程去执行同步块,在锁无竞争的情况下使用,一旦有了竞争就升级为轻量级锁,升级为轻量级锁的时候需要撤销偏向锁,撤销偏量锁的时候会导致 stop the word(stw) 操作。

在有锁的竞争时,偏向锁会做很多额外操作,尤其是撤销偏向锁的时候会导致进入安全点,而安全点会导致 stw,导致性能下降。

Stop the word是什么?

指在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外),是Java中一种全局暂停现象,类似于应用程序发生了停顿,没有任何响应。

轻量级锁

轻量级锁是由偏向锁升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁;轻量级锁的目的是减少无实际竞争情况下,使用重量级锁的性能消耗。比如系统调用引起的内核态与用户态切换,线程阻塞造成的线程切换等。

轻量级锁的加锁过程:

在代码进入同步块的时候,如果同步对象锁状态无锁状态且不允许进行偏向(锁标志位为"01"状态,是否为偏量锁为"0"),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录 (Lock Record) 的空间,用于存储锁对象目前的 Mark Word 的拷贝,官方称之为 Displaced Mark Word 。

拷贝成功后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock record 里的 owner 指针指向 object mark word 。 如果更新成功,则执行上述偏向锁中的步骤4,否则执行步骤5。

如果这个更新操作成功,那么这个线程就拥有了该对象的锁,并且对象MarkWord 的锁标志设置为 “00” ,即表示此对象处于轻量级锁锁定状态。

如果这个更新操作失败了,虚拟机首先会检查对象Mark Word是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,当竞争线程尝试占用轻量级失败多次之后,轻量级锁就会膨胀为重量级锁,重量级线程指针指向竞争线程,竞争线程也会阻塞,等待轻量级线程释放锁后唤醒它。同时锁标志的状态值变为 “10” ,如图下 Mark Word 中存储的就是指向重量级锁(互斥量) 的指针,后面等待锁的线程也要进入阻塞状态。


网络异常,图片无法展示
|

重量级锁

轻量级锁不断自旋膨胀之后,就会升级为重量级锁。重量级锁时依赖对象内部的 monitor 锁来实现,而 moitor 又依赖系统的 MutexLock(互斥锁) 来实现,所以重量级锁也被称为 互斥锁

为什么说重量级锁开销比较大?

当系统检查到锁时重量级锁时,会把正在等待获取锁的线程进行阻塞,被阻塞的线程不会消耗 cpu ,但是阻塞和唤醒线程,都需要操作系统来处理,这就需要从用户态转换到内核态,而从用户态到内核态的切换吗,需要通过系统调用来完成。

系统调用的过程中会发生 cpu 上下文切换,一次系统调用的过程,需要发生 两次 上下文切换。而这个过程很多时候比同步代码块所需时间还长。

不同锁之间的比较

image.png

总结

到了这里,我们知道了为什么 synchronized 关键字的底层实现以及锁的状态变化过程。说实话,这些对于一个Android 开发而言,可能很难有应用场景,但是于我个人而言,终于解释了曾经哪些隐晦的为什么,以及一些边界概念。知道的越多,不知道的越多。

目录
相关文章
|
1月前
|
缓存 Java
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
本文介绍了几种常见的锁机制,包括公平锁与非公平锁、可重入锁与不可重入锁、自旋锁以及读写锁和互斥锁。公平锁按申请顺序分配锁,而非公平锁允许插队。可重入锁允许线程多次获取同一锁,避免死锁。自旋锁通过循环尝试获取锁,减少上下文切换开销。读写锁区分读锁和写锁,提高并发性能。文章还提供了相关代码示例,帮助理解这些锁的实现和使用场景。
java中的公平锁、非公平锁、可重入锁、递归锁、自旋锁、独占锁和共享锁
|
1月前
|
Java 开发者
Java 中的锁是什么意思,有哪些分类?
在Java多线程编程中,锁用于控制多个线程对共享资源的访问,确保数据一致性和正确性。本文探讨锁的概念、作用及分类,包括乐观锁与悲观锁、自旋锁与适应性自旋锁、公平锁与非公平锁、可重入锁和读写锁,同时提供使用锁时的注意事项,帮助开发者提高程序性能和稳定性。
52 3
|
1月前
|
Java 开发者
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
在Java多线程编程的世界里,Lock接口正逐渐成为高手们的首选,取代了传统的synchronized关键字
46 4
|
2月前
|
Java
Java 中锁的主要类型
【10月更文挑战第10天】
|
2月前
|
安全 Java 开发者
java的synchronized有几种加锁方式
Java的 `synchronized`通过上述三种加锁方式,为开发者提供了从粗粒度到细粒度的并发控制能力,满足了不同场景下的线程安全需求。合理选择加锁方式对于提升程序的并发性能和正确性至关重要,开发者应根据实际应用场景的特性和性能要求来决定使用哪种加锁策略。
33 0
|
2月前
|
Java 应用服务中间件 测试技术
Java21虚拟线程:我的锁去哪儿了?
【10月更文挑战第8天】
46 0
|
5天前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
35 6
|
20天前
|
设计模式 Java 开发者
Java多线程编程的陷阱与解决方案####
本文深入探讨了Java多线程编程中常见的问题及其解决策略。通过分析竞态条件、死锁、活锁等典型场景,并结合代码示例和实用技巧,帮助开发者有效避免这些陷阱,提升并发程序的稳定性和性能。 ####
|
18天前
|
存储 监控 小程序
Java中的线程池优化实践####
本文深入探讨了Java中线程池的工作原理,分析了常见的线程池类型及其适用场景,并通过实际案例展示了如何根据应用需求进行线程池的优化配置。文章首先介绍了线程池的基本概念和核心参数,随后详细阐述了几种常见的线程池实现(如FixedThreadPool、CachedThreadPool、ScheduledThreadPool等)的特点及使用场景。接着,通过一个电商系统订单处理的实际案例,分析了线程池参数设置不当导致的性能问题,并提出了相应的优化策略。最终,总结了线程池优化的最佳实践,旨在帮助开发者更好地利用Java线程池提升应用性能和稳定性。 ####
|
20天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####