synchronized 有哪几种使用方式?
- 普通同步方法,锁是当前实例对象
- 静态同步方法,锁是当前类的class对象
- 同步代码块,锁事synchronized 括号中的对象
jvm 怎么标识线程已经获取锁呢?这得要说说java对象头了
| 存储对象的hashcode或锁信息 | 存储到对象类型数据的指针 | 数组长度(如果当前对象是数组) |
|---|---|---|
| Mark Word | Class Metadata Address | Array length |
注意:锁可以升级但是不能降级
hotspot作者研究发现,锁在大多数情况下不存在竞争,并且总是被同一个线程获取,为了让线程获得锁的代价更低,于是引入了偏向锁:当一个线程获取锁并且进入了同步代码块之后,会在当前的锁对象头中记录当前线程的线程Id,后续再次进入代码块时,只需要判断锁对象头中的线程ID是否是自己,如果是的话则不再需要进行Cas加锁和解锁,否则需要重新获取。什么是偏向锁?
-
偏向锁的获取
- 线程A进入临界点前,判断对象头中Mark Word中是否存着指向本线程的偏向锁,如果有,则直接获取锁成功,进入临界区,如果不存在,则执行下一步
- Mark word的偏向锁表示位是否设置成1(当前已经是偏向锁),如果没有,则使用CAS竞争锁,否则
- 使用CAS将对象头的偏向锁指向当前线程
-
偏向锁的撤销
- 锁撤销只会发生在锁竞争的时候并且当前没有正在执行的字节码
- 暂停拥有偏向锁的线程
- 检查拥有偏向锁的进程是否存活,如果不处于存活状态,则将对象头设置为无锁状态,否则
- 如果线程一直存活,则拥有偏向锁的栈会被执行,栈中的锁记录和对象头的Mark word要么重新偏向于其他线程,要么恢复到无无锁状态,最后唤醒暂停的线程
-
关闭偏向锁
偏向锁在jdk中默认是启用的,如果你确定你的线程中会发生锁竞争,那么你可以使用如下命令关闭偏向锁
-XX:BiasedLockingStartupDelay = 0;
轻量级锁
-
轻量级锁加锁
- 线程在执行同步代码块之前,JVM会在当前的栈桢中创建存储锁记录的空间,并将锁对象头的Mark Word内容复制到刚才创建的锁记录空间中,这一步叫做“Displaced Mark Word”,
- 然后当前线程尝试使用CAS将对象头中的Mark Word替换为指向当前锁记录空间的内存地址,
- 如果替换成功则成功获取锁,否则表示有其他线程在竞争这把锁,则
- 使用自旋的方式来获取锁。
-
轻量级锁解锁
- 使用CAS操作将获取锁之前复制的Mark Word替换回锁对象头中,如果成果成功,则表示没有竞争发生,
- 如果失败,锁就会膨胀成重量级锁,并且在使用自旋方式尝试获取轻量级锁的线程会被阻塞
- 拥有轻量级锁的线程释放锁并唤醒等待的线程,被唤醒的线程重新争夺锁,访问临界区。
因为自旋锁会消耗CPU,为了避免无用的自旋操作,特别是当持有锁的线程被阻塞了,一旦锁升级为重量级锁,就不会再恢复到轻量级锁,当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后,会唤醒这个阻塞的线程,被唤醒的线程重新争夺锁。为什么锁不能被降级
锁的优缺点对比
| 锁 | 优点 | 缺点 | 使用场景 |
|---|---|---|---|
| 偏向锁 | 加锁和解锁不需要额外的消耗 | 如果线程间存在竞争,则需要额外消耗 | 只有一个线程的场景 |
| 轻量级锁 | 竞争的线程不会阻塞,提高程序响应速度 | 自旋操作,时间过长会消耗过多的cpu | 追求响应时间,同步块中的任务执行的很快,不会长时间阻塞 |
| 重量级锁 | 线程竞争不使用自旋,不会消耗过多cpu | 线程阻塞,响应时间慢 | 追求吞吐量,同步块执行时间长 |
总结:本篇博文通过几个疑问,让大家对synchronized理解的更加深刻,后续会更新更多的有关并发编程相关的教程,感谢关注。大家可以关注我的公众号"乐哉开讲",领取更多资料。