锁升级(膨胀)过程

synchronize锁升级过程:jdk高版本之后对synchronize关键字进行了很多优化,其中一项就是锁升级,以前synchronize默认就是悲观锁,是在JVM层面上加锁的,加锁解锁的开销都比较大。所以引入了偏向锁、轻量级锁、重量级锁。

那么偏向锁、轻量级锁、重量级锁又是什么
偏向锁:我认为偏向锁的关键就是“偏”,偏向于第一个访问的线程。也就是说在无竞争的环境下,有一个线程访问的同步代码块,那么这个锁就会偏向这个线程,下次有线程访问的时候就会判断是不是之前访问过的线程访问,这样就会少一次cas的开销。因为第一次有线程访问同步代码块的时候会用cas把线程id写入mark word中。偏向锁会有一个延迟,程序刚启动的5s内不会出现偏向锁,这点在博主前面jol测试对象头中证明了这点,计算过hashcode值的对象不会加偏向锁,因为对象头没有空间放线程id了。
轻量级锁:轻量级锁体现轻量的点就在于自旋,如果线程访问轻量级锁的同步代码块,会cas判断线程id是否一致,不一致会自旋一定的时间一致cas,如果cas成功就还是轻量级锁。但一般都是失败的,然后轻量级锁就会升级为重量级锁。
重量级锁:jvm层面的两个标识,加锁解锁都会阻塞其他线程。
synchronize锁升级机制总结

详细谈一下锁升级
简单的来说,在无竞争的时候sync使用偏向锁,如果偏向锁失败了(一个对象被不同的线程加锁了),就会升级为轻量级锁,如果有线程的竞争,就升级为重量级锁。

详细的说,偏向锁的标记被记录在了markwork里面,标志位为100,偏向锁的线程id也被记录在了对象头中,线程访问同步代码块的时候,就会用cas检查对象头记录的线程id是不是当前线程,如果不是,就cas把替换线程id,如果成功就获得偏向锁。如果是当前线程,也获得偏向锁。如果替换失败,就当偏向锁线程达到安全点的时候,升级为轻量级锁。偏向锁的意思就是偏向第一个访问的线程

轻量级锁的线程信息存储在当前线程的栈帧中,并将对象头中的markword复制到锁记录中,然后线程尝试使用cas将对象头中的markword替换为指向锁记录的指针,如果成功,当前线程获得锁,如果失败,表示有其他线程竞争,自旋一定的次数后就会升级为重量级锁。重量级锁使除了拥有锁的线程以外的线程都阻塞。

synchronize锁升级机制总结
再来说说synchronize的原理

synchronize锁升级机制总结
Wait Set:那些调用wait方法被阻塞的线程放置在这里

contention List:竞争队列,所有请求锁的线程首先被放在竞争队列

entry list:contention中那些有资格的会被移入contention list

ondeck:任意时刻,最多有一个线程在竞争资源,该线程为ondeck

owner:当前获取到资源的线程

  1. JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下,ContentionList 会被大量的并发线程进行 CAS 访问,为了降低对尾部元素的竞争,JVM 会将一部分线程移动到 EntryList 中作为候选竞争线程。
  2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定EntryList 中的某个线程为 OnDeck 线程(一般是最先进去的那个线程)。
  3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck,
    OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在JVM 中,也把这种选择行为称之为“竞争切换”。
  4. OnDeck 线程获取到锁资源后会变为 Owner 线程,而没有得到锁资源的仍然停留在 EntryList中。如果 Owner 线程被 wait 方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 notify或者 notifyAll 唤醒,会重新进去 EntryList 中。
  5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统来完成的(Linux 内核下采用 pthread_mutex_lock 内核函数实现的)。
  6. Synchronized 是非公平锁。 Synchronized 在线程进入 ContentionList 时,等待的线程会先尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁资源。
    参考:https://blog.csdn.net/zqz_zqz/article/details/70233767
  7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加上 monitorenter 和 monitorexit 指令来实现的,方法加锁是通过一个标记位来判断的
  8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线程加锁消耗的时间比有用操作消耗的时间更多。
  9. Java1.6,synchronized 进行了很多的优化,有适应自旋、锁消除、锁粗化、轻量级锁及偏向锁等,效率有了本质上的提高。在之后推出的 Java1.7 与 1.8 中,均对该关键字的实现机理做了优化。引入了偏向锁和轻量级锁。都是在对象头中有标记位,不需要经过操作系统加锁。
  10. 锁可以从偏向锁升级到轻量级锁,再升级到重量级锁。这种升级过程叫做锁膨胀;
  11. JDK 1.6 中默认是开启偏向锁和轻量级锁,可以通过-XX:-UseBiasedLocking 来禁用偏向锁。

相关文章: