JVM基础 -- 浅谈synchronized
抽象算法
synchronized代码块
1 | public void foo(Object lock) { |
1 | public void foo(java.lang.Object); |
- monitorenter指令和monitorexit指令均会消耗操作数栈上的一个引用类型元素,作为所要加锁解锁的锁对象
- 上面的字节码包含一个monitorenter指令和多个monitorexit指令
- JVM需要保证所获得的锁在正常执行路径以及异常执行路径上都能够被解锁
- 具体的执行路径请参照Exception table
实例方法 + 静态方法
1 | public synchronized void eoo(Object lock) { |
1 | public synchronized void eoo(java.lang.Object); |
- 字节码中方法的访问标记(flags)包括ACC_SYNCHRONIZED
- 进入该方法时,JVM需要执行monitorenter操作
- 不管正常返回还是向调用者抛出异常,JVM均需要执行monitorexit操作
- 这里的monitorenter操作和monitorexit操作所对应的锁对象是隐式的
- 对于实例方法来说,锁对象为this
- 对于静态方法来说,锁对象为所在类的Class实例
monitorenter + monitorexit
- 抽象理解:每个锁对象拥有一个锁计数器和指向持有该锁的线程的指针
- 当执行monitorenter时,如果锁对象的计数器为0
- 那么说明锁对象还没有被其他线程所持有
- JVM会将锁对象的持有线程设置为当前线程,并将计数器+1
- 当执行monitorenter时,如果锁对象的计数器不为0
- 如果锁对象的持有线程是当前线程,那么JVM会将其计数器+1
- 否则需要等待,直到持有该锁对象的线程释放该锁
- 当执行monitorexit时,JVM则需要将该锁对象的计数器-1
- 当计数器减为0时,那么代表该锁已经被释放掉了
- 采用计数器的方式,是为了允许同一个线程重复获取同一把锁,可重入
锁优化
对象状态图
针对一个对象的整个生命周期,锁升级是单向不可逆:偏向锁 -> 轻量级锁 -> 重量级锁
重量级锁
- 重量级锁是JVM中最为基础的锁实现
- JVM会阻塞加锁失败的线程,并在目标锁被释放掉的时候,唤醒这些线程
- Java线程的阻塞和唤醒,都依赖于操作系统完成
- 涉及系统调用,需要从操作系统的用户态切换至内核态,开销很大
- 每个Java对象都存在一个与之关联的monitor对象
- 为了尽量避免昂贵的线程阻塞和唤醒操作,采用自旋
- 自旋:在处理器上空跑并且轮询锁是否被释放
- 线程会进入自旋的两种情况(睡前醒后)
- 线程即将进入阻塞状态之前
- 线程被唤醒后竞争不到锁
- 与线程阻塞相比,自旋可能会浪费大量的处理器资源
- JVM采取的是自适应自旋
- 根据以往自旋等待时间是否能够获得锁来动态调整自旋的时间(循环次数)
- 自旋还会带来不公平锁
- 处于阻塞状态的线程,没有办法立即竞争被释放的锁
- 而处于自旋状态的线程,则很有可能优先获得这把锁
轻量级锁
- 场景:多个线程在不同的时间段请求同一把锁,没有锁竞争
- JVM采用轻量级锁,可以避免避免重量级锁的阻塞和唤醒
加锁
- 如果锁对象标记字段的最后两位为01(无锁或偏向锁)
- JVM会在当前栈帧中建立一个名为锁记录(Lock Record)的内存空间
- 用于存储锁对象当前的标记字段(Mark Word)的拷贝,叫作Displaced Mark Word
- 拷贝锁对象的标记字段到锁记录中,见下图:轻量级锁CAS操作之前
- JVM使用CAS操作
- 将锁对象的标记字段更新为指向锁记录的指针
- 将锁记录里面的owner指针指向锁对象
- 如果CAS更新成功,那么当前线程拥有了该锁对象的锁,并将锁对象标记字段的锁标志位设置为00
- 此时该锁对象处于轻量级锁的状态,见下图:轻量级锁CAS操作之后
- 如果CAS更新失败,检查锁对象的标记字段是否指向当前线程的栈帧
- 如果是,说明锁重入,继续执行同步代码
- 如果不是,说明出现多线程竞争,膨胀为重量级锁
- 锁标记位变为10,锁对象的标记字段存储的是指向monitor对象的指针
- 后面等待锁的线程进入阻塞状态,当前线程则尝试使用自旋来获取锁
轻量级锁CAS操作之前
轻量级锁CAS操作之后
解锁
- JVM通过CAS操作,把线程中的Displaced Mark Word复制回锁对象的标记字段
- 如果CAS更新成功,同步过程结束
- 如果CAS更新失败,说明其他线程尝试获取过该锁(现已膨胀),在释放锁的同时,唤醒被挂起的线程
偏向锁
- 在无多线程竞争的情况下,仍然需要尽量减少不必要的轻量级锁的执行路径
- 轻量级锁的获取和释放依赖多次CAS原子指令
- 由此引入了偏向锁
- 偏向锁乐观地认为:从始至终只有一个线程请求某一把锁
- 只需要在置换ThreadID时需要依赖一次CAS操作
- 一旦出现多线程竞争,必须撤销偏向锁
- 轻量级锁是为了在线程交替执行同步块时提高性能
- 偏向锁是为了在只有一个线程执行同步块时进一步提高性能
加锁
- 偏向锁只会在第1次加锁时采用CAS操作
- 如果锁对象的标记字段最后三位为101,即可偏向状态
- 锁对象处于未偏向状态(Thread ID == 0)
- 那么JVM会通过CAS操作,将当前线程的ID记录在该锁对象的标记字段中
- 如果CAS成功,则认为当前线程获得该锁对象的偏向锁
- 如果CAS失败,说明另外一个线程抢先获取了偏向锁
- 此时需要撤销偏向锁,使得锁对象进入轻量级锁状态
- 锁对象处于已偏向状态(Thread ID != 0)
- 检测:标记字段中的线程ID == 当前线程的ID?
- 如果是,说明锁重入,直接返回
- 如果不是,说明该锁对象目前偏向于其他线程,需要撤销偏向锁
- 一个线程执行完同步代码后,不会将标记字段的线程ID清空,最小化开销
撤销
- 偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程本身不会主动释放偏向锁
- 偏向锁的撤销,需要等待全局安全点,暂停拥有偏向锁的线程,判断锁对象当前是否处于被锁定的状态
- 如果是,撤销偏向锁后升级为轻量级锁的状态
- 如果不是,撤销偏向锁后恢复为无锁状态
- 如果某个类的总撤销数超过-XX:BiasedLockingBulkRevokeThreshold=40
- Threshold of number of revocations per type to permanently revoke biases of all objects in the heap of that type
- JVM会撤销该类实例的偏向锁,并且在之后的加锁过程中直接为该类实例设置为轻量级锁
参考资料
All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.