前言

Java并发编程(四)——synchronized关键字实现加锁 中我们学习了在并发场景下最简单的同步方式就是利用 synchronized 关键字来修饰代码块或者修饰一个方法,那么这部分被保护的代码,在同一时刻就最多只有一个线程可以运行,而 synchronized 的背后正是利用 monitor 锁实现的。

所以首先我们来看下获取和释放 monitor 锁的时机,每个 Java 对象都可以用作一个实现同步的锁,这个锁也被称为内置锁或 monitor 锁,获得 monitor 锁的唯一途径就是进入由这个锁保护的同步代码块或同步方法,线程在进入被 synchronized 保护的代码块之前,会自动获取锁,并且无论是正常路径退出,还是通过抛出异常退出,在退出的时候都会自动释放锁。

代码示例:

1
2
3
public synchronized void method() {
    --method body
}

为了方便理解其背后的原理,我们把上面这段代码改写为下面这种等价形式的伪代码:

1
2
3
4
5
6
7
8
9
10
public void method() {
    this.intrinsicLock.lock();
    try{
        --method body
    }

    finally {
        this.intrinsicLock.unlock();
    }
}

在这种写法中,进入 method 方法后,立刻添加内置锁,并且用 try 代码块把方法保护起来,最后用 finally 释放这把锁,这里的 intrinsicLock 就是 monitor 锁。

JVM 实现 synchronized 方法和 synchronized 代码块的细节是不一样的,下面我们分别来看一下两者的实现。

synchronized同步代码块

编写一个Java文件,命名为SynTest:

1
2
3
4
5
6
7
public class SynTest {
public void synBlock(){
synchronized (this){
System.out.println("test");
}
}
}

然后在Windows下cd命令进入该文件所在目录,使用javac进行编译成class文件

1
javac SynTest.java

然后使用javap命令可以看到对应的反汇编内容:

1
javap -verbose SynTest.class

我们需要关注下monitorenter指令和monitorexit指令,可以把执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁。可以看到一个monitorenter对应两个monitorexit指令,

这是因为monitorenter 指令被插入到同步代码块的开始位置,而 monitorexit 需要插入到方法正常结束处异常处两个地方,这样就可以保证抛异常的情况下也能释放锁

执行 monitorenter 理解为加锁,执行 monitorexit 理解为释放锁,每个对象维护着一个记录着被锁次数的计数器。未被锁定的对象的该计数器为 0。

monitorenter

执行 monitorenter 的线程尝试获得 monitor 的所有权,会发生以下这三种情况之一:

  1. 如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数设置为 1。然后,该线程就是这个 monitor 的所有者。

  2. 如果线程已经拥有了这个 monitor ,则它将重新进入,并且累加计数。

  3. 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 的计数变成为 0,代表这个 monitor 已经被释放了,于是当前这个线程就会再次尝试获取这个 monitor。

monitorexit

monitorexit 的作用是将 monitor 的计数器减 1,直到减为 0 为止。代表这个 monitor 已经被释放了,已经没有任何线程拥有它了,也就代表着解锁,所以,其他正在等待这个 monitor 的线程,此时便可以再次尝试获取这个 monitor 的所有权。

synchronized同步方法

同样的编写一个SynTest类,里面编写一个synBlock方法,该方法加上synchronized关键字:

1
2
3
4
5
public class SynTest {
public synchronized void synBlock(){
System.out.println("test");
}
}

经过编译及反汇编得到指令如下:

可以看出,被 synchronized 修饰的方法会有一个 ACC_SYNCHRONIZED 标志。当某个线程要访问某个方法的时候,会首先检查方法是否有 ACC_SYNCHRONIZED 标志,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁。其他方面, synchronized 方法和刚才的 synchronized 代码块是很类似的,例如这时如果其他线程来请求执行方法,也会因为无法获得 monitor 锁而被阻塞。

总结

我们深入了解了synchronized关键字的底层原理,synchronized同步代码块和方法的底层实现有些许的区别,同步代码块是通过monitorenter和monitorexit指令,同步方法是通过叫ACC_SYNCHRONIZED的flag修饰符来实现。其他方面比如维护被锁次数的计数器原理都是类似的。