Synchronized

原理

JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步的

代码块同步是 monitorentermonitorexit,方法的同步是依靠ACC_SYNCHRONIZED实现。方法的同步也可以通过前面的两个指令实现。

无论哪种,都是获取一个对象的 Monitor( 监视器 ),这个获取过程是排他的、独占的

sync修饰符.jpg

  • Synchronized 用的锁是与 Java 对象头息息相关的

对象头.jpg

Mark Word 中存储的数据会随着锁标志位的变化而变化

MarkWord.jpg

锁升级

JDK 1.6 后,锁有4种状态,级别从低到高:无锁、偏向锁、轻量级锁、重量级锁

锁只能升级,不能降级

偏向锁

偏向锁的获取就是登记自己的 ID,已经登记的就不用再登记了

大多数情况下,锁经常会被同一线程获取,所以为了让获取锁的代价更低,引入了偏向锁

当一个线程访问同步块并获取锁时,会在 对象头栈桢 ( 在虚拟机栈 VM Stack 里 )中的锁记录( Lock Record )存入自身的线程 ID,下次访问就不用 CAS 来加锁解锁了。

线程来,判断

如果是本线程 ID,就执行代码。

如果来的线程和记录的线程 ID 不一样

  • 可能你是第一次来,也可能锁被别人获取了。

接着看 Mark Word 里面的偏向锁标识是不是 1

如果不是就 CAS 竞争锁( 就是第一次来 )

如果标识是 1,就是另外的线程来竞争( 此时持有锁的线程可能活着,也可能没有 ),就尝试 CAS 将对象头的线程 ID改为自己的( 这时候就触发原来线程偏向锁的撤销 )

撤销

需要等到全局安全点( 在这个时间点上没有正在执行的字节码 )。会挂起原来的线程,看看线程还活着不

  • 如果不处于活动状态,设置偏向锁标识 0,设置为无锁 01,按正常获取偏向锁。CAS 替换线程 ID。( 重新偏向 )
  • 如果还活着,偏向锁 升级 成轻量级锁,新来的线程自旋获取锁。

锁撤销的开销花费还是挺大的:

  1. 在一个安全点停止拥有锁的线程。
  2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和 Markword,使其变成无锁状态。
  3. 唤醒当前线程,将当前锁升级成轻量级锁。

所以如果确定是两个及以上线程竞争,就通过 JVM 参数默认关闭偏向锁:-XX:UseBiasedLocking=false

轻量级锁

加锁

JVM 会先在当前线程的栈桢中创建用于存放锁记录( Lock Record )的空间,并将对象头中的 Mark Word 复制到锁记录,官方称为 Displaced Mark Word( 用于轻量级锁解锁 ),然后将锁记录的 Owner 指针指向锁对象

然后线程尝试 CAS 把对象头的 Mark Word 替换为指向锁记录的指针

  • 如果成功,当前线程顺利拿锁

  • 如果失败,表示还有其他线程在竞争,当前线程就 自旋 + CAS 获取锁

解锁

所谓自旋,就是指当有另外一个线程来竞争锁时,这个线程会在原地循环等待,而不是把该线程给阻塞

但自旋也得有限度,所以默认自旋的次数为10,超过就自旋失败,锁就会升级为 重量级锁

除了自旋锁,还有自适应自旋锁

所谓自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数

假如一个线程1刚刚成功获得一个锁,当它把锁释放了之后,线程 2 获得该锁,并且线程 2 在运行的过程中,此时线程 1 又想来获得该锁了,但线程 2 还没有释放该锁,所以线程 1 只能自旋等待,但是虚拟机认为,由于线程 1 刚刚获得过该锁,那么虚拟机觉得线程 1 这次自旋也是很有可能能够再次成功获得该锁的,所以会延长线程 1 自旋的次数

另外,如果对于某一个锁,一个线程自旋之后,很少成功获得该锁,那么以后这个线程要获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以免空循环等待浪费资源

重量级锁

也叫互斥锁、悲观锁

Mutex,懂的都懂

加锁:锁方法、锁代码块和锁对象


参考:

线程安全(中)–彻底搞懂synchronized(从偏向锁到重量级锁)

《Java并发编程的艺术》

在JVM中,对象在内存中分为三块区域:

  • 对象头( Header )
    • **Mark Word( 标记字段 )**:默认存储对象的 HashCode,分代年龄和锁标志位信息,包括 GC 分代年龄、哈希码、锁状态、线程持有的锁等数据,这部分的数据长度在 32 位和 64 位虚拟机中分别为 32 位和 64 位。它会根据对象的状态复用自己的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化
      • 对象在不同的状态下,Mark Word 会存储不同的内容
    • 类型指针:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例
  • 实例数据( Instance Data )
    • 存放类的数据信息、父类的信息
  • 对齐填充( Padding )
    • 由于虚拟机要求对象起始地址必须是8字节的整数倍,填充数据不是必须存在的,仅仅是为了占位置

一个空对象占 8 个字节,是因为对齐填充的关系,不到 8 个字节对其填充会帮我们自动补齐