0%

揭秘`Synchronized`

Synchronized

基本认知

  • 被用来解决数据共享访问的安全性问题最常见的应用
用法

不同的修饰类型,代表锁的控制粒度

  • 修饰实例方法
    • 作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 静态方法
    • 作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,
    • 指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

锁的优化

旧时代的重量级锁,1.6版本开始为了减少锁竞争的性能开销,引入了偏向锁、轻量级锁的概念;根据竞争的激烈程度锁状态从低到高不断升级。
既然是通过锁的状态来进行锁的升级,那么锁是如何存储的呢?

锁的存储
  • 从synchronized的语法来看是基于lock这个对象的生命周期来控制锁粒度的。

    JVM源码分析能得到MarkwordmarkOop类型),JVM源码自己翻哈

  • Mark word记录了对象和锁有关的信息,Mark Word在32位JVM的长度是32bit、在64位JVM的长度是64bit
    Mark Word里面存储的数据会随着锁标志位的变化而变化,可分为以下5种情况:

锁的升级

锁存在四种状态分别是:无锁、偏向锁、轻量级锁、重量级锁

偏向锁

当访问有同步锁的代码块时,会在对象头中存储当前线程的ID,后续该线程再次访问该段同步锁代码时不需要再次加锁和释放锁,而是直接比较对象头里中的线程id。若相等则表示锁是偏向于当前线程的,就不需要再尝试获得锁了

  • 锁获取

    • 先获取锁对象的Markword ,判断是否处于可偏向状态。(biased_lock=1、且 ThreadId 为空)
    • 若是可偏向状态,则通过CAS操作,把当前线程的ID写入到Markword
      • CAS成功,则表示已经获得了偏向锁,接着执行同步代码块
      • CAS失败,则有其他线程已经获得了偏向锁存在锁竞争;
        要撤销已获得偏向锁的线程且把它持有的锁升级为轻量级锁(在全局安全点才能操作,也就是没有线程在执行字节码)
    • 若是已偏向状态,则判断Markword 中存储的线程ID是否与当前线程ID相等
      • 若相等,则不需要再次获得锁,可直接执行同步代码块
      • 若不相等,则当前锁偏向于其他线程,需要撤销偏向锁并升级到轻量级锁
  • 锁撤销

    • 本质并不是把对象恢复到无锁可偏向状态,而是在获取过程中存在竞争时是直接把被偏向的锁对象升级到被加了轻量级锁的状态。
      那么已获得偏向锁的线程有两种情况:
      • 若已获得偏向锁的线程已退出临界区则此时会将对象头设置成无锁状态并且竞争锁的线程可以基于CAS重新偏向
      • 若已获得偏向锁的线程还未退出临界区则此时会把原获得偏向锁的线程升级为轻量级锁后继续执行同步代码块

        临界区可以理解成要同步代码块的区段,进入就是开始执行同步代码块,退出就代码同步代码块执行完毕

        JVM设置开启或关闭偏向锁:UseBiasedLocking

轻量级锁
  • 锁升级为轻量级锁之后,对象的 Markword 也会进行相应的变化。过程如下:

    • 线程在自己的栈桢中创建锁记录 LockRecord
    • 将锁对象的对象头中的 MarkWord 复制到线程的刚刚创建的锁记录中
    • 将锁记录中的 Owner 指针指向锁对象
    • 将锁对象的对象头的 MarkWord 替换为指向锁记录的指针
  • 锁获取-自旋锁

    • 所谓自旋就是指当有竞争锁时,这个线程会在原地循环等待(会消耗cpu),而不是阻塞,直到那个
      获得锁的线程释放锁之后,这个线程就可以马上获得锁的。所以轻量级锁适用于那些同步代码块执行的很快的场景可以缩短锁的获取时间。

      自旋锁其实就是空间换时间的操作,
      但是自旋必须有次数限制否则不断的循环反而会消耗CPU资源。 默认情况下自旋的次数是 10 次,
      可以通过 preBlockSpin 来修改

    JDK1.6 之后,引入了自适应自旋锁,即自旋的次数是动态变化的;会根据前一次在同一个锁上自旋的时间以及锁的拥有者的状态来决定。
    如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并
    且持有锁的线程正在运行中,那么虚拟机就会认为这次自
    旋也是很有可能再次成功,进而它将允许自旋等待持续相
    对更长的时间。如果对于某个锁,自旋很少成功获得过,
    那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源

  • 锁释放

    • 锁释放逻辑本质就是锁获取的逆向逻辑,通过
      CAS 操作把线程栈帧中的 LockRecord 替换回到锁对象的
      MarkWord 中,若成功表示没有竞争,若失败表示当前锁存在竞争,那么轻量级锁就会升级成为重量级锁

重量级锁

到这个阶段就意味着线程只能被挂起阻塞来等待被唤醒了

  • 在class字节码中可以看到加入同步代码块后会有monitorentermonitorexit

    可以使用以下命令查看: javap -v xxx.class

  • monitorenter 表示尝试获得一个对象监视器。 monitorexit 表示释放monitor监视器的所有权。monitor依赖操作系统的 MutexLock(互斥锁)实现的,当线程被阻塞后便进入内核态从而导致系统调度要在用户态与内核态之间来回切换,所以性能相对较低

    每一个 JAVA 对象都会与一个监视器 monitor 关联(可以理解成一把锁),当一个线程想要执行一段被
    synchronized 修饰的同步方法或者代码块时,该线程得先获取到 synchronized 修饰的对象对应的 monitor

引用一张别人的锁升级流程图–非常完善