内置锁(synchronized)

Java 提供了一种内置的锁机制来支持原子性:同步代码块(Synchronized Block)。
同步代码块包括两部分:

  1. 作为锁的对象引用
  2. 作为由这个锁保护的代码块

以关键字 synchronized 来修饰的方法就是一种横跨整个方法体的同步代码块,其中该同步代码块的锁就是方法调用所在的对象。
上面提到的锁具体表现为以下 3 种形式:

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

synchronized 方法以对象作为锁:

  1. synchronized (lock){
  2. //访问或修改由锁保护的共享状态
  3. }

每个 Java 对象都可以用做一个实现同步的锁,这些锁被称为内置锁(Intrinsic Lock)或监视器锁(Monitor Lock)。
线程在进入同步代码块之前会自动获得锁,并且在同步代码块时自动释放锁——无论是通过正常的控制路径退出,还是通过从代码块中抛出的异常退出
获得内置锁的唯一途径就是进入由这个锁保护的同步代码块或方法。
Java 的内置锁相当于一种互斥锁,最多只有一个线程能持有这种锁。
当线程 A 尝试获取一个由 B 线程持有的锁时,线程 A 必须等待或者阻塞,直到线程 B 释放这个锁。
如果 B 永远不释放锁,那么 A 也将永远地等下去。

实现原理

JVM 基于进入和退出 Monitor 对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,而方法同步是使用另一种方式实现的,细节在 JVM 规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。
monitorenter 指令在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处,JVM 要保证每个 monitorenter 必须有对应的 monitorexit 与之配对。
任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。

Java 对象头

synchronized 用的锁是存在于 Java 对象头里的。如果对象是数组类型,则虚拟机用 3 个子宽(Word)存储对象头,如果对象是非数组类型,则用 2 子宽存储对象头。在 32 位虚拟机中,1 子宽等于 4 字节,即 32bit。
Java 对象头里的 Mark Word 默认存储对象的 HashCode、分代年龄和锁标记位:

锁状态 25bit 4bit 1bit 是否是偏向锁 2bit 锁标志位
无锁状态 对象的hashCode 对象分代年龄 0 01

内置锁的重入

当某个线程请求一个由其他线程持有的锁时,发出的请求的线程就会阻塞。
然而,内置锁是可以重入的,因此如果某个线程试图获得一个已经由它自己持有的锁,那么这个请求就会成功。

重入的实现方式

为每个锁关联一个计数值和一个所有者线程。当计数值为 0,这个锁就被认为是没有任何线程持有。当线程请求一个未被持有的锁时,JVM 将记下锁的持有者,并且将获取计数值置为 1。如果同一个线程再次获取这个锁,计数值将递增,而当线程退出同步代码块时,计数器会相应地递减。当计数值为 0,这个锁会被释放。

重入行为

重入进一步提升了加锁行为的封装性,因此简化了面向对象并发代码的开发。
下面的代码展示了重入锁的使用,如果没有重入锁,下面的代码会产生死锁。

  1. public class Widget {
  2. public synchronized void doSomething() {
  3. //...
  4. }
  5. }
  6. public class LoggingWidget extends Widget {
  7. public synchronized void doSomething() {
  8. System.out.println(toString() + ": calling doSomething");
  9. super.doSomething();
  10. }
  11. public static void main(String[] args) {
  12. Widget widget = new LoggingWidget();
  13. widget.doSomething();
  14. }
  15. }

由于 Widget 和 LoggingWidget 中 doSomething 方法都是 synchronized 方法,因此每个父类 doSomething 方法在执行前都会获取 Widget 上的锁。
如果内置锁是不可重入的,那么调用 super.soSomething 时永远获取不到 Widget 上的锁。
重入锁避免了这种死锁的发生。