Java 1.6 到底对synchronized做了什么?

1. 概述

在多线程并发编程中 synchronized 一直是元老级角色,很多人都会称呼它为 重量级锁 ,但是,随着 Java SE 1.6 版本对 synchronzied 进行了各种优化之后,有些情况它并不那么重了。本文将详细介绍 Java SE 1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁, 以及锁的存储结构和升级过程.

2. 实现同步的基础

Java中的每个对象都可以作为锁,具体变现为以下3中形式:

  1. 对于普通同步方法, 锁是当前实例对象。
  2. 对于静态同步方法, 锁是当前类的Class对象。
  3. 对于同步方法块, 锁是synchronized括号里配置的对象。

一个线程试图访问同步代码块时,,必须获取锁。在退出或者抛出异常时, 必须释放锁。

3. 实现方式

JVM 基于进入和退出Monitor 对象来实现方法同步 和 代码块同步,但是两者的实现细节不一样:

  1. 代码块同步 :通过 monitorrentermontorexit 指令实现的。
  2. 同步方法 :通过 ACC_SYNCHRONIZED 修饰。

4. 对象头

HotSpot 虚拟机 中,对象在内存中的布局可以分为三块区域:对象头、实例数据 和 对其填充。

对象头 包括两部分:MarkWord类型指针

如果是数组对象的话,对象头还有一部分是存储数组的长度。

多线程下 synchronized 的加锁就是对同一个对象的对象头中的 MarkWord 中的变量进行 CAS 操作

4.1. MarkWord

MarkWord :用于存储对象自身的运行时数据,如 HashCode、GC 分代年龄、锁状态标志、线程持有的锁、偏向锁ID 等等。

占用内存大小与虚拟机位长一致(32位 JVM --> MarkWord 是32位,64位 JVM --> MarkWord 是64位)。

4.2. 类型指针

类型指针指向对象的类元数据,虚拟机通过这个指针确定该对象是哪个类的实例。

4.3. 对象头的长度

长度内容说明
32 / 64 bitMarkWord存储对象的HashCode 或 锁信息 等
32 / 64 bitClass MetaData Address存储对象类型数据的指针
32 / 64 bitArray Length数组的长度(如果当前对象是数组)

如果是数组对象的话,虚拟机用3 个字宽(32/64bit + 32/64bit + 32/64bit)存储对象头;如果是普通对象的话,虚拟机用2个字宽存储对象头(32/64bit + 32/64bit)。

5.优化后synchronized锁的分类

锁级别从低到高依次是:

  1. 无锁状态
  2. 偏向锁状态
  3. 轻量级锁状态
  4. 重量级锁状态

锁可以升级,但是不能降级。即:无所 --> 偏向锁 --> 轻量级锁 --> 重量级锁 方向是单向的。

6. 锁的升级

6.1. 偏向锁

偏向锁是针对于一个线程而言的,线程获得锁之后就不会再有解锁等操作了,,这样可以省略很多开销.。假如有两个线程来竞争该锁话,那么偏向锁就失效了,,进而升级成轻量级锁了。

为什么要这样做呢?

因为经验表明,其实大部分情况下, 都会是同一个线程进入同一块同步代码块的。 这也是为什么会有偏向锁出现的原因。

在 JDK 1.6 中,偏向苏的开关是默认开启的,适用于只有一个线程访问同步块的场景。

偏向锁的加锁:

当一个线程访问同步块并获取锁时,会在锁对象的对象头栈帧中的锁记录里存储锁偏向的线程ID,以后该进程进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单的测试一下锁对象的对象头的MarkWord里是否存储着当前线程的偏向锁(线程ID是当前线程),如果测试成功,表示线程已经获得了锁;如果测试失败,则需要测试一下 MarkWord 中偏向锁的标识是否设置成1(表示当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将锁对象的对象头的偏向锁指向当前线程。

偏向锁的撤销:

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销需要等到全局安全点(在这个时间点上没有正在执行的字节码)。首先会暂停持有偏向锁的线程,然后检查持有偏向锁的线程是否存活,如果线程不处于活动状态,则将锁对象的对象头设置为无锁状态;如果线程仍然活着,则将对象的对象头中的 MarkWord栈中的锁记录 要么重新偏向于其他线程要么恢复到无锁状态,最后唤醒暂停的线程(释放偏向锁的线程)。

总结

偏向锁在 Java 1.6 及更高版本中默认启用的,但是它在程序启动几秒钟之后才会激活。可以使用 -XX:BiasedLockingStartupDelay=0 参数来关闭偏向锁的启动延迟,也可以使用 -XX:-UseBiasedLocking=false 参数来关闭偏向锁,那么程序会直接进入轻量级锁状态。

6.2. 轻量级锁

当出现有两个线程来竞争锁的话,那么偏向锁就失效了,此时锁就会膨胀, 升级为轻量级锁。

轻量级锁加锁

线程在执行同步块之前,JVM 会先在当前线程的栈帧中创建用户存储锁记录的空间,并在对象头中的 MarkWord 复制到锁记录中,然后线程尝试使用 CAS 将对象头中的 MarkWord 替换为指向锁记录的指针。如果成功,当前线程获得锁;如果失败,表示其它线程竞争锁,当前线程便尝试使用自旋来获取锁,之后再来的线程,发现是轻量级锁,就开始进行自旋

轻量级锁解锁

轻量级锁解锁时,会使用原子的 CAS操作将当前线程的锁记录替换为对象头,如果成功,表示没有竞争发生;如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

总结

总结一下加锁解锁过程,有线程A线程B来竞争对象c的锁(如: synchronized©{} ), 这时线程A线程B同时将对象c的MarkWord复制到自己的锁记录中, 两者竞争去获取锁, 假设线程A成功获取锁, 并将对象c的对象头中的线程ID(MarkWord中)修改为指向自己的锁记录的指针, 这时线程B仍旧通过CAS去获取对象c的锁, 因为对象c的MarkWord中的内容已经被线程A改了, 所以获取失败。 此时为了提高获取锁的效率, 线程B会循环去获取锁, 这个循环是有次数限制的,如果在循环结束之前CAS操作成功, 那么线程B就获取到锁,如果循环结束依然获取不到锁, 则获取锁失败, 对象c的MarkWord中的记录会被修改为重量级锁, 然后线程B就会被挂起, 之后有线程C来获取锁时, 看到对象c的MarkWord中的是重量级锁的指针, 说明竞争激烈, 直接挂起。

解锁时, 线程A尝试使用CAS对象c的MarkWord改回自己栈中复制的那个MarkWord, 因为对象c中的MarkWord已经被指向为重量级锁了, 所以CAS失败。 线程A会释放锁并唤起等待的线程, 进行新一轮的竞争。

6.3. 锁的比较

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步代码方法的性能相差无几如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用只有一个线程访问的同步场景
轻量级锁竞争的线程不会阻塞,提高了程序的相应速度如果始终得不到锁竞争追求响应时间, 同步快执行速度非常快
重量级锁线程竞争不适用自旋, 不会消耗CPU线程堵塞, 响应时间缓慢追求吞吐量, 同步快执行时间速度较长

7. 总结

首先要明确一点是引入这些锁是为了提高获取锁的效率, 要明白每种锁的使用场景, 比如偏向锁适合一个线程对一个锁的多次获取的情况; 轻量级锁适合锁执行体比较简单(即减少锁粒度或时间), 自旋一会儿就可以成功获取锁的情况。

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值