【Java并发】synchronized关键字的底层原理

1.synchronized作用

synchronized是Java提供一种隐式锁无需开发者手动加锁释放锁。保证多线程并发情况下数据的安全性,实现了同一个时刻只有一个线程能访问资源,其他线程只能阻塞等待,简单说就是互斥同步。

2.synchronized加锁原理

代码块加锁:
例如下面一段代码就是加上了对象锁
在这里插入图片描述
这个时候通过反编译查看class字节码信息:
在这里插入图片描述
可以看到,底层是通过monitorentermonitorexit两个关键字实现的加锁释放锁,执行同步代码之前使用monitorenter加锁,执行完同步代码使用monitorexit释放锁,抛出异常的时候也是用monitorexit释放锁。

在方法上加锁
在这里插入图片描述
反编译看一下底层实现
在这里插入图片描述
这次只使用了一个ACC_SYNCHRONIZED关键字,实现了隐式的加锁与释放锁。其实无论是ACC_SYNCHRONIZED关键字,还是monitorenter和monitorexit,底层都是通过获取monitor锁来实现的加锁释放锁

3.monitor锁

Monitor 被翻译为监视器,是由jvm提供,c++语言实现

monitor锁是通过ObjectMonitor来实现的,虚拟机中ObjectMonitor数据结构如下(C++实现的):
在这里插入图片描述
其中:

  1. Owner(持有锁的线程-只能有一个):存储当前获取锁的线程的,只能有一个线程可以获取
  2. EntryList(保存竞争的线程):关联没有抢到锁的线程,处于Blocked状态的线程
  3. WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程

执行逻辑:
在这里插入图片描述
在这里插入图片描述
图上展示了ObjectMonitor的基本工作机制:

  1. 当多个线程同时访问一段同步代码时,首先会进入 _EntryList 队列中等待。
  2. 当某个线程获取到对象的Monitor锁后进入临界区域,并把Monitor中的 _owner变量设置为当前线程,同时Monitor中的计数器 _count 加1。即获得对象锁。
  3. 若持有Monitor的线程调用 wait()方法,将释放当前持有的Monitor锁,_owner变量恢复为null_count减1,同时该线程进入 _WaitSet集合等待被唤醒。
  4. 在_WaitSet 集合中的线程唤醒后会被再次放到_EntryList 队列中,重新竞争获取锁。
  5. 若当前线程执行完毕也将释放Monitor并复位_owner变量的值,以便其他线程进入获取锁。

4.synchronized锁的优化

JDK1.5之前,synchronized是属于重量级锁(Monitor实现的锁属于重量级锁),涉及到了用户态和内核态的切换进程的上下文切换成本较高性能比较低。
在JDK 1.6引入了两种新型锁机制:偏向锁轻量级锁,它们的引入是为了解决在没有多线程竞争基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。

4.1.自适应性自旋锁

自旋锁:在没有拿到锁的时候,当前线程会进入阻塞状态,当持有锁的线程释放了锁,当前线程才可以再去竞争锁。在线程占用锁的时间很短的话。会浪费大量的性能阻塞和唤醒的切换上。

为了避免阻塞和唤醒的切换,在没有获得锁的时候就不进入阻塞,而是不断地循环检测锁是否被释放,这就是自旋。在占用锁的时间短的情况下,自旋锁表现的性能是很高的。

自适应性自旋锁:是对自旋锁的一次升级,自适应性自旋锁的意思是,自旋的次数不是固定的,而是由前一次在同一个锁上的自旋时间锁的拥有者的状态来决定

举例就是此次自旋成功了,很有可能下一次也能成功,于是允许自旋的次数就会更多,反过来说,如果很少有线程能够自旋成功,很有可能下一次也是失败,则自旋次数就更少。这样能最大化利用资源

4.2.偏向锁

不存在多线程竞争,而且总是由同一线程多次获得锁(同一线程可重入锁)
例如如下代码:
同一个线程多次获得锁
加锁m1—>m2—>m3
释放锁m3—>m2—>m1
期间并不存在竞争,不存在阻塞等待,也不存在唤醒。
在这里插入图片描述
在这里插入图片描述
原理:
锁的争夺实际上是Monitor对象的争夺,还有每个对象都有一个对象头,对象头是由Mark Word和Klass pointer 组成的。一旦有线程持有了这个锁对象,标志位修改为1,就进入偏向模式,同时会把这个线程的ID记录在对象的Mark Word中,当同一个线程再次进入时,就不再进行同步操作,这样就省去了大量的锁申请的操作,从而提高了性能。

只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现
这个线程 ID自己的表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

一旦不同的线程来获取锁的时候,那么偏向锁发现Mark Word中线程id不一样了,就会向上升级为轻量级锁(不会直接升级到重量级锁)

4.3.轻量级锁

在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的不同的线程交替的执行同步块中的代码。(交替执行自然不存在线程竞争)
在这里插入图片描述
原理:
执行同步代码块之前,JVM会在线程的栈帧中创建一个锁记录(Lock Record),并将Mark Word拷贝复制到锁记录中。然后尝试通过CAS操作将Mark Word中的锁记录的指针,指向创建的Lock Record。如果成功表示获取锁状态成功,如果失败,则进入自旋获取锁状态。

在这里插入图片描述

自旋获取锁也失败了,则升级为重量级锁,也就是把线程阻塞起来,等待唤醒。

4.3.重量级锁

也就是上述的synchronized锁,就是一个重量级锁

重量级锁就是一个悲观锁了,但是其实不是最坏的锁,因为升级到重量级锁,是因为线程占用锁的时间长(自旋获取失败),锁竞争激烈的场景,在这种情况下,让线程进入阻塞状态,进入阻塞队列,能减少cpu消耗。所以说在不同的场景使用最佳的解决方案才是最好的技术。synchronized在不同的场景会自动选择不同的锁,这样一个升级锁的策略就体现出了这点。

5.总结

Java中的synchronized有偏向锁轻量级锁重量级锁三种形式,分别对应了锁只被一个线程持有不同线程交替持有锁多线程竞争锁三种情况。

对应情况
偏向锁只被一个线程持有
轻量级锁不同线程交替持有锁
重量级锁多线程竞争锁
描述
重量级锁底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低
轻量级锁线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
偏向锁一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令

一旦锁发生了竞争,都会升级为重量级锁

参考来自:黑马程序员,公众号:java技术爱好者、一灯架构

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值