浅谈 synchronized 锁机制原理 与 Lock 锁机制
阅读本文大概需要 10 分钟。
来自:blog.csdn.net/a745233700/article/details/119923661
前言
一、synchronized锁机制
1、synchronized 的作用:
修饰实例方法:作用于当前实例加锁,进入同步代码前要获得当前实例的锁 修饰静态方法:作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 修饰代码块:指定加锁对象,进入同步代码库前要获得给定对象的锁
2、synchronized 底层语义原理:
monitor
实现的(无论是显示同步还是隐式同步都是如此),每个对象的对象头都关联着一个 monitor
对象,当一个 monitor
被某个线程持有后,它便处于锁定状态。在 HotSpot
虚拟机中,monitor 是由 ObjectMonitor
实现的,每个等待锁的线程都会被封装成 ObjectWaiter
对象,ObjectMonitor
中有两个集合,_WaitSet
和 _EntryList
,用来保存 ObjectWaiter
对象列表 ,owner 区域指向持有 ObjectMonitor
对象的线程。_EntryList
集合尝试获取 moniter,当线程获取到对象的 monitor
后进入 _Owner
区域并把 _owner
变量设置为当前线程,同时 monitor
中的计数器 count 加1;若线程调用 wait()
方法,将释放当前持有的 monitor
,count自减1,owner 变量恢复为 null,同时该线程进入 _WaitSet
集合中等待被唤醒。若当前线程执行完毕也将释放 monitor
并复位变量的值,以便其他线程获取 monitor
。如下图所示:_EntryList
:存储处于Blocked
状态的ObjectWaiter
对象列表。_WaitSet
:存储处于wait
状态的ObjectWaiter
对象列表。
3、 synchronized 的显式同步与隐式同步:
monitorenter
和 monitorexit
指令,而隐式同步并不是由 monitorenter
和 monitorexit
指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED
标志来隐式实现的。3.1、synchronized 代码块底层原理:
synchronized
同步语句块的实现是显式同步的,通过 monitorenter
和 monitorexit
指令实现,其中 monitorenter
指令指向同步代码块的开始位置,monitorexit
指令则指明同步代码块的结束位置,当执行 monitorenter
指令时,当前线程将尝试获取 objectref
(即对象锁)所对应的 monitor
的持有权:当对象锁的
monitor
的进入计数器为 0,那线程可以成功取得monitor
,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有对象锁的
monitor
的持有权,那它可以重入这个monitor
,重入时计数器的值也会加1。若其他线程已经拥有对象锁的
monitor
的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit
指令被执行,执行线程将释放monitor
并设置计数器值为0,其他线程将有机会持有monitor
。
monitorenter
指令都有执行其对应 monitorexit
指令。为了保证在方法异常完成时,monitorenter
和 monitorexit
指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器可处理所有的异常,它的目的就是用来执行 monitorexit
指令。3.2、synchronized 方法底层原理:
synchronized
同步方法的实现是隐式的,无需通过字节码指令来控制,它是在方法调用和返回操作之中实现。JVM 可以通过方法常量池中的方法表结构(method_info Structure
)中的 ACC_SYNCHRONIZED
访问标志 判断一个方法是否同步方法。ACC_SYNCHRONIZED
访问标志是否被设置,如果设置了,标识该方法是一个同步方法,执行线程将先持有 monitor
, 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放 monitor
。在方法执行期间,执行线程持有了 monitor
,其他任何线程都无法再获得同一个 monitor
。monitor
将在异常抛到同步方法之外时自动释放。4、JVM 对 synchronized 锁的优化:
monitor
是依赖于操作系统的 Mutex
互斥量来实现的,操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。在 JDK6 之后,synchronized 在 JVM 层面做了优化,减少锁的获取和释放所带来的性能消耗,主要优化方向有以下几点:4.1、锁升级:偏向锁->轻量级锁->自旋锁->重量级锁
CAS
并配合 Mark Word
一起实现的。对象头 Mark Word 指向类的指针 数组长度 实例数据 对齐填充
hashcode
、分代年龄、锁标记位相关的信息,由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到 JVM 的空间效率,Mark Word
被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,在 32位 JVM 中的长度是 32 位,具体信息如下图所示:4.1.2、锁升级过程:
如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。
相反的,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。
Monitor
中并阻塞在 _EntryList
集合中(具体的争夺锁的原理见该部分的第2点:synchronized 底层语义原理)。4.2、锁消除:
4.3、锁粗化:
5、偏向锁的废除:
为了支持偏向锁使得代码复杂度大幅度提升,并且对 HotSpot 的其他组件产生了影响,这种复杂性已成为理解代码的障碍,也阻碍了对同步系统进行重构
在更高的 JDK 版本中针对多线程场景推出了性能更高的并发数据结构,所以过去看到的性能提升,在现在看来已经不那么明显了。
围绕线程池队列和工作线程构建的应用程序,性能通常在禁用偏向锁的情况下变得更好。
二、Lock 锁机制
1、Lock 锁是什么:
lock()
方法与 unlock()
对临界区进行加锁与释放锁,当前线程获取到锁之后,其他线程由于无法持有锁将无法进入临界区,直到当前线程释放锁,unlock()
操作必须在 finally 代码块中,这样可以确保即使临界区执行抛出异常,线程最终也能正常释放锁。2、ReentrantLock 重入锁:
ReentrantLock
重入锁是基于 AQS 框架并实现了 Lock
接口,支持一个线程对资源重复加锁,作用与 synchronized
锁机制相当,但比 synchronized
更加灵活,同时也支持公平锁和非公平锁。2.1、ReentrantLock 与 synchronized 的区别:
ReentrantLock
依赖于 API,是显式锁,需要 lock()
和 unlock()
方法配合 try/finally
语句块来完成。ReentrantLock
在发生异常时,如果没有主动通过 unLock()
去释放锁,则很可能造成死锁现象,这也是 unLock()
语句必须写在 finally 语句块的原因。ReentrantLock
相比于 synchronzied
更加灵活, 除了拥有 synchronzied
的所有功能外,还提供了其他特性:ReentrantLock
可以实现公平锁,而synchronized
不能保证公平性。ReentrantLock
可以知道有没有成功获取锁(tryLock),而synchronized
不支持该功能ReentrantLock
可以让等待锁的线程响应中断,而使用synchronized
时,等待的线程不能够响应中断,会一直等待下去;ReentrantLock
可以基于Condition
实现多条件的等待唤醒机制,而如果使用synchronized
,则只能有一个等待队列
ReentrantLock
的性能要远远优于 synchronizsed
。但是在 JDK6 及以后的版本,JVM 对 synchronized
进行了优化,所以两者的性能变得差不多了synchronizsed
和 ReentrantLock
都是可重入锁,在使用选择上需要根据具体场景而定,大部分情况下依然建议使用 synchronized
关键字,原因之一是使用方便语义清晰,二是性能上虚拟机已为我们自动优化。如果确实需要使用到 ReentrantLock
提供的多样化特性时,我们可以选择ReentrantLock
3、ReadWriteLock 读写锁:
ReentrantLock
某些时候有局限,如果使用 ReentrantLock
,主要是为了防止线程A在写数据、线程B在读数据造成的数据不一致,但如果线程C在读数据、线程D也在读数据,由于读数据是不会改变数据内容的,所以就没有必要加锁,但如果使用了 ReentrantLock
,那么还是加锁了,反而降低了程序的性能,因此诞生了读写锁 ReadWriteLock
。ReadWriteLock
是一个接口,而 ReentrantReadWriteLock
是 ReadWriteLock
接口的具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和写之间才会互斥,提升了读写的性能。推荐阅读:
互联网初中高级大厂面试题(9个G) 内容包含Java基础、JavaWeb、MySQL性能优化、JVM、锁、百万并发、消息队列、高性能缓存、反射、Spring全家桶原理、微服务、Zookeeper......等技术栈!
⬇戳阅读原文领取! 朕已阅