深入探究 synchronized


前情

如何使用这个关键字和类锁对象锁什么的,老生常谈了,就不在这里说了,先声明,这篇博客的内容只能算是我从很多博客中总结出的自己的观点,无法保证全部是自己的话,也无法保证绝对正确性,望以谨慎

正文

存在

  • synchronized在一个程序中以字节码中的指令的形式存在,使用javap工具可以看到synchronized关键字在字节码文件中的存在形式,如果是同步代码块,是monitorentermonitorexit两个指令来划分一段临界区,锁定的是一个对象的monitor,即对象监视器.而同步方法是依靠方法修饰符ACC_SYNCHRONIZED实现
  • 以上是字节码中的存在,从设计逻辑来看,synchronized用的锁存在与对象头中,所谓Java对象头,就是Java堆中的对象的头部,详情不在这里说了,对象头主要包括两部分,即: 标记字段和类型指针,类型指针是用来指向这个对象的类型,而标记字段就很多了,有这个对象的hashcode,cg年龄,锁状态标记,线程持有锁,偏向所ID,偏向时间戳等,一般的对象头两个机器码,数组类型为3个机器码.

Monitor

Monitor Record 是线程私有的数据结构,每一个线程都有一个可用 Monitor Record 列表,同时还有一个全局的可用列表。
每一个被锁住的对象都会和一个 Monitor Record 关联(对象头的 MarkWord 中的 LockWord 指向 Monitor 的起始地址),Monitor Record 中有一个 Owner 字段,存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下:
Owner
EntryQ
RcTHis
Nest
HashCode
Candidate
Owner : 初始时为Null表示没有任何线程有所该Monitor,当线程拥有该锁后保存线程为唯一标识,当锁被释放时有设置为Null
EntryQ: 关联一个系统互斥锁,阻塞所有试图锁住Monitor Record失败的线程
RcThis:表示blocked或waiting在该Monitor Record上的所有线程的个数
Nest:用来实现重入锁的计数
HashCode:从对象头拷贝而来的
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功获得锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换,,candidate只有两种可能的值:0:表示没有需要唤醒的线程1:表示要唤醒一个继任线程来竞争锁

synchronized优化和锁分类

synchronized在1.6以后进行了很多优化,使得它和ReentrantLock相比也不落下风.

  • 锁分类
    • 偏向锁,想象一下,如果在一个单线程的情况下使用了synchronized关键字,每一次进入相关临界区时,其实并没有必要加锁,但是由于synchronized关键字的存在,造成了多余的操作和消耗.偏向锁就是解决这个问题的,如果这是一个单线程的情况,在获取锁前会检查对应的对象头部信息,查看偏向状态,如果是可偏向状态,就直接执行代码块内容,如果不是,就去修改这个标记,这个过程是一个CAS,如果成功,也就验证了这是个无竞争状态,如果失败,就代表存在多线程竞争.这时,会执行撤销偏向锁的操作,进入轻量级锁
      总的来说,偏向锁就是为了没有多线程竞争的情况下,减少加锁的损耗
    • 轻量级锁
      • 轻量级锁的存在意义在于”对于绝大部分的锁,在整个生命周期内是不会有竞争的”
      • 轻量级锁主要的实现还是CAS和自旋锁(自适应自旋锁),也就是在获取锁失败时,不是马上转入内核态将线程挂起,而是通过一个自旋的过程,如果获取锁的线程马上释放了锁,那么这个自旋的过程就是很有意义的,
      • 轻量级锁的实现过程就是通过一系列的对上面说到的Minitor Record中字段的CAS修改来实现的
      • 自旋锁的次数是可以通过JVM参数设置的,’-xx:preBlockSpin`来调整,默认是10次,但是这个并不常用,因为无法用一个统一的次数衡量所有程序,所有又有了自适应的自旋锁,通过一个自适应算法来智能的计算自旋次数,一般是由前一次在同一个锁上的自旋次数及拥有者的状态来决定.
    • 重量级锁
      • 这里说的重量级锁主要针对那些没有获得锁的线程,如果超过了轻量级锁的自旋次数,就会换到重量级锁,这是一个硬件级别的操作,也就是将线程由用户态切换到内核态,执行挂起等操作,这是个成本很高的操作

锁消除和锁粗化

  • 锁消除,其实是类似偏向锁的设计思路,不过应该要更高一些,就是如果JVM分析出一个同步操作并不会发生共享数据竞争的情况,就会将锁消除掉,比如我们在单线程情况下使用StringBuffer或者Vector,因为其内部是有锁的,但是我们并不需要,所以就会将这个锁消除掉
  • 锁粗化, 如果我们的锁粒度没有控制好,导致有一段连续的加锁解锁的代码,这样频繁的加解锁操作并没有带来更好的效率,反而降低了效率,jvm会将这些锁连接起来,编程一个范围更大的锁.
  • 这一部分的内容其实并没有看上去那么简单,其内部的实现其实和JIT相关,JIT就是Java即时编译,会再开一个博客说说,锁消除和其中的一个逃逸分析相关,想深入了解的可以看我下一篇博客或者自行寻找

  目录