synchronized

25 阅读5分钟
  • synchronized 是 Java 提供的保证临界区域并发安全的关键字,synchronized 有互斥性、阻塞性、可重入性等特点

1、对象头

  • 每个对象都有一个对象头,包括Mark Word和Klass Word

    • Klass Word 指向该对象的类对象
    • Mark Word

对象头.png

2、偏向锁

  • Java 6 中引入了偏向锁,第一次加锁时使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

    • 偏向锁默认是开启的,对象创建后,Mark Word 值最后 3 位为 101,它的thread、epoch、age 都为 0
    • 偏向锁默认是延迟的,不会在程序启动时立即生效,程序启动一段时间后创建的对象才会生效,如果想避免延迟,可以加 VM 参数 -XX:BiasedLockingStartupDelay=0 来禁用延迟
    • 可以加 VM 参数 -XX:-UseBiasedLocking 禁用偏向锁,对象创建后,Mark Word 值为最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值

(1)偏向撤销

  • 调用了对象的 hashCode

    • 因为偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode,MarkWord 无法同时存储线程 id 和 hashCode(存不下)
    • 轻量级锁会在锁记录中记录 hashCode
    • 重量级锁会在 Monitor 中记录 hashCode
  • 其他线程对对象加锁

    • 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁,解锁后对象是正常状态,而不是偏向状态
  • 调用 wait/notify

    • wait/notify 只有重量级锁才有

(2)批量重偏向

  • 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程1的对象仍有机会重新偏向线程2,重偏向会重置对象的 Thread ID
  • 当撤销偏向锁阈值超过 20 次后,jvm 会在给这些对象加锁时重新偏向至加锁线程

(3)批量撤销

  • 当撤销偏向锁阈值超过 40 次后,jvm 会觉得根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的

3、轻量级锁

  • 当线程执行 synchronized(obj) 加锁时,在栈针中创建一个 Lock Record 对象

    • Lock Record 对象包括指针(指针指向 obj)和 Mark Word (锁记录地址和锁状态00)
  • 使用 cas 尝试将 Lock Record 对象的 Mark Word 和 obj 的 Mark Word 替换

    • cas 成功代表加锁成功

    • cas 失败

      • 如果是其它线程已经持有了该 obj 的轻量级锁,这时表明有竞争,进入锁膨胀过程

      • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数

        • 此时的 Lock Record 指针还会指向 obj,但是 Mark Word 为 null
  • 执行完代码释放锁

    • 如果 Lock Record 对象的 Mark Word 为null,表示冲入,直接去掉(重入计数减一)

    • 如果 Lock Record 对象的 Mark Word 不为null,使用 cas 恢复 obj 的 Mark Word

      • cas 成功,解锁成功
      • cas 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程

4、重量级锁

  • Monitor 对象包括ower(当前加锁线程)、count(加锁次数)、waitSet(处于wait状态的线程)、entryList(处于等待锁状态的线程)等

    • 线程1调用 synchronized(obj) 方法时,obj 对象头的 Mark Word 设置一个指针,指向一个 Monitor 对象
    • 将 Monitor 的 ower 设置为线程1,计数器count++

      • 同一个线程可多次加锁,计数器从0,加一次锁 count 加1
    • 此时线程2(线程3、线程4 ... 线程n)调用 synchronized(obj) 方法,会发现 Monitor 对象的计数器 count 不为0,且 ower 也不是自己,然后将自己加入 entryList,此时等待锁的线程状态为 BLOCKED
    • 当线程1执行完毕,释放锁时,会将计数器 count--,ower 设置为空,唤醒 entryList 中的线程竞争锁

      • 可重入锁,每释放一次 count 减1,最后 count 为0
      • 唤醒 entryList 中的线程竞争锁是非公平的

5、自旋优化

锁竞争的时候,还可以使用自旋(循环尝试获取锁)来进行优化

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
  • Java 7 之后不能控制是否开启自旋功能

6、锁粗化

对相同对象多次加锁,导致线程发生多次重入,可以使用锁粗化方式来优化

  • 线程1循环 synchronized(obj),会优化成 synchronized(obj) 循环

7、锁消除

jvm 会对多次执行的代码会使用 JIT (即使编译器)进行优化

  • 线程1执行 synchronized(obj) ,但是 obj 对象只在线程1中使用,其他线程不会用到,此时就会消除 synchronized
  • 可以通过 -XX:-EliminateLocks 参数,取消锁消除优化

8、加锁流程

synchronized加锁流程.png