Synchronized关键字深度解析

Synchronized的原理

monitorenter

每一个对象都会和一个监视器monitor关联,监视器被占用时会被锁住,其他线程无法来获取到该monitor,当JVM执行某个线程的某个方法内部的monitorenter时,它会尝试去获取当前对象的monitor的所有权。
过程如下:

  1. 若monitor的进入数为0,线程可以进入monitor,并将monitor的进入数置为1,当前线程成为monitor的owner
  2. 若线程已拥有monitor的所有权,允许它重入monitor,则进入monitor的进入数加1
  3. 若其他线程占有monitor的所有权,那么当前尝试获取monitor的所有权的线程会被阻塞,直到monitor的进入数变为0,才能重新尝试获取monitor的所有权
    monitorenter小结:
    synchronized的锁对象会关联一个monitor,这个monitor不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有monitor就会创建monitor,monitor内部有两个重要的成员变量,owner拥有这把锁的线程,recursions会记录线程拥有锁的次数,当一个线程拥有monitor后其他线程只能等待。monitor才是真正的锁,是一个c++对象,owner拥有锁的线程 recursion记录获取锁的次数

monitorexit

  1. 能执行mointorexit指令的线程一定是拥有当前对象的monitor的所有权的线程。
  2. 执行monitorexit时会将monitor的进入数减1.当monitor的进入数减为0时,当前线程退出montior,不再拥有monitor的所有权,此时其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
  3. monitorexit插入在方法结束处和异常处,JVM保证每个monitorenter必须有对应的monitorexit

存在两个monitorexit

因为synchronzied锁的同步代码块有可能会抛出异常,则需要释放锁,还有正常执行完方法也会释放锁

站在虚拟机源码分析

锁池

假设线程A已经拥有了某个对象的锁,而其它的线程想要调用这个对象的某个synchronized方法(或者synchronized块),由于这些线程在进入对象的synchronized方法之前必须先获得该对象的锁的拥有权,但是该对象的锁目前正被线程A拥有,所以这些线程就进入了该对象的锁池中。

等待池

假设一个线程A调用了某个对象的wait()方法,线程A就会释放该对象的锁(因为wait()方法必须出现在synchronized中,这样自然在执行wait()方法之前线程A就已经拥有了该对象的锁),同时线程A就进入到了该对象的等待池中。如果另外的一个线程调用了相同对象的notifyAll()方法,那么处于该对象的等待池中的线程就会全部进入该对象的锁池中,准备争夺锁的拥有权。如果另外的一个线程调用了相同对象的notify()方法,那么仅仅有一个处于该对象的等待池中的线程(随机)会进入该对象的锁池.
规则如下
如果线程调用了对象的wait()方法,那么线程便会处于该对象的等待池中,等待池中的线程不会去竞争该对象的锁
当有线程调用了对象的notifyAll()方法或者notify()方法,被唤醒的线程便会进入该对象的锁池中,锁池中的线程会去竞争该对象锁。
优先级高的线程竞争到对象锁的概率大,如果某线程没有竞争到该对象锁,它还留在锁池中,只有线程再次调用wait()方法,它才会重新回到等待池中,而竞争到对象锁的线程则继续往下执行,直到执行完了synchronized代码块,它会释放掉该对象锁,这时锁池中的线程会继续竞争该对象锁。

 // are simple integers or pointers
  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  // 记录个数
    _waiters      = 0,
    _recursions   = 0;   // 递归次数/重入次数
    _object       = NULL;
    _owner        = NULL; // 记录当前持有锁的线程ID
    _WaitSet      = NULL;  // 等待池:处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 锁池:处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

Synchronized对象布局

在这里插入图片描述

对齐填充

对齐填充并不是必然存在的,也没有特定的含义,仅仅起着占位符的作用。
由于HotSpot虚拟机的自动内存管理系统要求对象的起始地址必须是8字节的整数倍,也就是对象的大小必须是8字节的整数倍。而对象头部分正好是8字节的倍数(1倍或者2倍),因此,当对象实例数据部分没有对齐的时候,就需要通过对齐填充来补全。

new一个对象占用的字节

对象头(16字节)没有压缩+实例属性+填充数据

Synchronized锁升级

偏向锁

当一个线程获取到锁之后,会再锁的对象头mark word中记录该线程的id,下次再进入到该同步代码块的时候,不需要重复的加锁cas和解送锁,从而提高效率,整个过程是只有一个线程取竞争那把锁,这个就可以理解为偏向锁。
偏向锁的来源是因为Hotsopt的作者研究发现大多数情况下,锁不仅不存在多线程竞争,而是总是由同一个线程多次获得,而线程的阻塞和唤醒需要cpu从用户态转为核心态,频繁的阻塞和唤醒对cpu来说是一件负担很重的工作,为了让线程获得锁的代码更低而引入了偏向锁。
在实际应用运行过程中,锁总是同一个线程持有,很少发生竞争,也就是说锁总是被第一个占用他的线程拥有,只需要在锁第一次拥有的时候,记录下偏向线程id,这样偏向线程就一直持有锁,直到竞争发生才释放锁,这样每次同步的时候,只需要检查锁的偏向线程id与当前线程id是否一致,如果一致直接进入同步,无需每次加锁解锁都去更新cas更新对象头,如果不一致意味着发生了竞争,锁已经不是总偏向于同一个线程,这个时候锁就需要升级为轻量级锁,才能保证线程间公平竞争锁。
在这里插入图片描述

轻量级锁

当有多个线程在间隔的方式竞争锁的对象时,会短暂结合自旋控制,线程不会阻塞,但是会消耗cpu资源

重量级锁

线程的竞争不会使用自旋,线程会阻塞,不会消耗cpu资源,适合于同步代码执行比较长的时间

总结:

  1. 偏向锁:只有一个线程的情况下,可以使用轻量级减少cas加锁和释放锁的操作,如果是多个线程同时访问锁的情况下,偏向锁撤销为轻量级或者是重量级锁
  2. 多个线程间隔或者短暂同时竞争锁的情况下,不会导致当前的线程阻塞,如果没有获取到锁的情况下会采用自旋的形式重复的获取锁,但是非常消耗cpu的资源,如果多次重复获取锁失败,则变为重量级锁。场景:同步代码块里面的代码执行时间是非常快的情况下。
  3. 没有获取到锁的线程会变为阻塞的状态,效率是极低的 场景:线程不会采用自旋的形式,不会消耗cpu资源,释放cpu的执行权,同步代码块执行可能比较耗时

锁的消除

锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到某一段代码中,在堆上的所有数据不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁无须进行
参考:蚂蚁课堂

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值