synchronized

104 阅读4分钟

对临界资源加锁一定要保证所有线程看到的是同一把锁,锁 new 对象是无效的
同步方法:用synchronized修饰的方法,静态同步方法锁的是当前类 .class,非静态同步方法锁的是 this
synchronized 必须是可重入锁。

可重入锁:当一个对象获取到锁之后,再次需要这个锁,是自己持有的,就可以直接使用,不会造成死锁。
子类也是可以通过可重入锁调用父类的同步方法。

首先介绍一下用户态和内核态:

内核态(Kernel Mode):运行操作系统程序,操作硬件
用户态(User Mode):运行用户程序

而用户态和内核态的转换成本较高,JVM 也是运行在用户空间的,在 JDK 早期,synchronized 被称为重量级锁,因为申请锁资源必须从用户空间切换到内核空间拿到锁,再把锁返回给用户空间。

对象在内存中的布局

锁升级

首先看锁升级整体流程图与 64 位系统下的锁状态对应 makeword 的变化图:

  每次进入同步块(即执行 monitorenter )的时候都会以从高往低的顺序在栈中找到第一个可用的 Lock Record,并设置偏向线程 ID ;每次解锁(即执行 monitorexit )的时候都会从最低的一个 Lock Record 移除。所以如果能找到对应的 Lock Record 说明偏向的线程还在执行同步代码块中的代码。

  • 偏向锁:据 JDK 团队发现,大多情况下锁不仅不存在多线程竞争,还总是由同一线程重复获取,为了让线程获得锁的代价更低而引入了偏向锁
  1. 访问 MarkWord 中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态。
    1. 如果为可偏向状态,则测试线程ID是否指向当前线程,如果是,进入步骤 e,否则进入步骤 c。
    2. 如果线程ID并未指向当前线程,则通过 CAS 操作竞争锁。如果竞争成功,则将 MarkWord 中线程 ID 设置为当前线程 ID ,然后执行 e;如果竞争失败,执行 d。
    3. 如果 CAS 获取偏向锁失败,则表示有竞争,会撤销偏向锁
      • 撤销偏向锁的步骤:
        1. 等待全局安全点(在这个时间点上没有正在执行的字节码)。
        2. 先暂停拥有偏向锁的线程
        3. 检查持有偏向锁的线程是否活着
          1. 线程还存活,则检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,进行CAS竞争锁
          2. 未存活,或者持有偏向锁的线程未在执行同步代码块中的代码,则进行校验是否允许重偏向,如果不允许重偏向,则撤销偏向锁,将Mark Word 设置为无锁状态(未锁定不可偏向状态),然后升级为轻量级锁,进行CAS竞争锁
        4. 恢复暂停的线程,继续执行代码
        5. 执行同步代码。
  • 轻量级锁:如果存在其他线程竞争锁,那么就会膨胀为轻量级锁。这时候锁并不会去经过内核,仅仅是将 makeword 中的部分字节 CAS 更新指向线程栈中的 Lock Record 如果更新成功,则说明锁获取成功,否则说明已经有线程获取到了锁,CAS 更新到一定时候膨胀为重量级锁。
  • 重量级锁:JDK 1.6 的时候,CAS 自选的次数超过 10 次或者等待自旋的线程超过了内核数的 1/2 就会转化为重量级锁;1.6 之后增加了自适应自旋,JDK 根据每个线程的情况判断需不需要升级。

Lock Record:当轻量的锁住一个对象时,就会在获取锁的线程的栈上隐式或显式的创建一个 Lock Record。

  上面看锁升级的流程图上,偏向锁有个是否启动的判断,在 JDK 中这个配置默认是开启的,当然可以通过配置修改:

启用参数: -XX:+UseBiasedLocking
关闭延迟: -XX:BiasedLockingStartupDelay=0
禁用参数: -XX:-UseBiasedLocking

  在知道肯定是多并发的情况下,可以把偏向锁关闭,减少消除偏向锁的开销。
  还有一个开启的延迟,就是 JVM 启动几秒之后才会开启偏向锁,有这个延迟的原因是 JVM 启动的时候内会有一些多线程进行争抢,所以有一个延迟的操作。

实现

字节码层面

  • 如果是给对象加锁:monitorenter monitorexit 组合使用;
  • 如果是同步方法:加了一个 ACC_SYNCHRONIZED 修饰符,方法的同步同样可以使用这两个指令来实现,monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。