【JAVA并发编程系列】并发 -- synchronized 关键字与锁总结
【1】JAVA 对象头
【2】利用 synchronized 实现同步的基础
Java中的每一个对象都可以作为锁;
具体表现 :
- 1. 对于普通同步方法,锁是当前实例对象;
- 2. 对于静态同步方法,锁是当前类的 Class 对象;
- 3. 对于同步方法块,锁是 Synchonized 括号里配置的对象;
【3】Synchonized 在 JVM 里的实现原理
- JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,即使用 monitorenter 和 monitorexit 指令实现的同步;
- monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处;
- JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之配对;
- 任何对象都有一个 monitor 与之关联,当一个 monitor 被持有后,它将处于锁定状态;
- 线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁;
【4】锁的升级与对比
【4.1】偏向锁
偏向锁引入
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁;
偏向锁的获取
- 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁,只需简单地测试一下对象头的 Mark Word 里是否存储着指向当前线程的偏向锁;
- 如果测试成功,表示线程已经获得了锁;
- 如果测试失败,则需要再测试一下 Mark Word 中偏向锁的标识是否设置成 1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用 CAS 将对象头的偏向锁指向当前线程;
偏向锁的撤销
- 偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁;
- 偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码),它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的 Mark Word 要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程;
【4.2】轻量级锁
轻量级锁加锁
- 线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,官方称为 Displaced Mark Word,然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针,如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁;
轻量级锁解锁
- 轻量级解锁时,会使用原子的 CAS 操作将 Displaced Mark Word 替换回到对象头,如果成功,则表示没有竞争发生,如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁;
注意
- 因为自旋会消耗 CPU,为了避免无用的自旋,一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态;
- 当锁处于重量级状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争;
【4.3】偏向锁、轻量级锁、重量级锁的优缺点对比
【5】锁的内存语义
锁的释放-获取建立的 happens-before 关系
- 锁是 Java 并发编程中最重要的同步机制,锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息;
锁的释放和获取的内存语义
- 当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中;
- 当线程获取锁时,JMM 会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享变量;
- 1. 线程 A 释放一个锁,实质上是线程 A 向接下来将要获取这个锁的某个线程发出了(线程 A 对共享变量所做修改的)消息;
- 2. 线程 B 获取一个锁,实质上是线程 B 接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息;
- 3. 线程 A 释放锁,随后线程 B 获取这个锁,这个过程实质上是线程 A 通过主内存向线程 B 发送消息;
参考致谢
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。
【1】Java并发编程的艺术