我们都知道synchronized可以修饰方法和代码块,那么这两者的内部实现原理是相同的吗?我们来仔细看一下。
修饰方法和代码块的不同
首先我们先看一下通过反编译出的字节码两者有何不同。
源代码如下:
package jvm;
/**
* Created by ljm on 29/1/2018.
*/
public class ClassCompile {
synchronized void test(){}
void test1(){
synchronized (ClassCompile.class){
}
}
}
我们使用命令javap来反编译:
javap -verbose ClassCompile
得到的字节码如下(这里只截取与这里所说有关的部分):
synchronized void test();
descriptor: ()V
flags: ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 7: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Ljvm/ClassCompile;
void test1();
descriptor: ()V
flags:
Code:
stack=2, locals=3, args_size=1
0: ldc #2 // class jvm/ClassCompile
2: dup
3: astore_1
4: monitorenter
5: aload_1
6: monitorexit
7: goto 15
10: astore_2
11: aload_1
12: monitorexit
13: aload_2
14: athrow
15: return
Exception table:
from to target type
5 7 10 any
10 13 10 any
我们发现两者在处理上是有不同的。
- 同步方法,JVM使用ACC_SYNCHRONIZED标识来实现。即JVM通过在方法访问标识符(flags)中加入ACC_SYNCHRONIZED来实现同步功能。
- 同步代码块,JVM使用monitorenter和monitorexit两个指令实现同步。即JVM为代码块的前后真正生成了两个字节码指令来实现同步功能的。
然后我们分别对这两个做详细解释。
同步方法
The Java® Virtual Machine Specification针对同步方法的说明:
Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.
这段话适合好好读读,大致含义如下:
同步方法是隐式的。一个同步方法会在运行时常量池中的method_info结构体中存放ACC_SYNCHRONIZED标识符。当一个线程访问方法时,会去检查是否存在ACC_SYNCHRONIZED标识,如果存在,则先要获得对应的monitor锁,然后执行方法。当方法执行结束(不管是正常return还是抛出异常)都会释放对应的monitor锁。如果此时有其他线程也想要访问这个方法时,会因得不到monitor锁而阻塞。当同步方法中抛出异常且方法内没有捕获,则在向外抛出时会先释放已获得的monitor锁
第一句话解释一下:
同步方法是隐式的。一个同步方法会在运行时常量池中的method_info结构体中存放ACC_SYNCHRONIZED标识符。
这句话的解读是:我们从上面的反编译结果也可以看到,同步方法会在class文件中的access_flags中存放ACC_SYNCHRONIZED,那这句话为什么说在运行时常量池中呢?
答:我们看看ACC_SYNCHRONIZED在class文件中的位置如下:
可以看到ACC_SYNCHRONIZED标识存放在常量池中,而method_info结构体中的access_flags字段是u2的,所以它只是一个指向常量池的标记。而常量池在加载时就会加载到运行时常量池中。所以这里解释了为什么上面说ACC_SYNCHRONIZED在运行时常量池中,而看class文件是存放在access_flags中的道理。
同步代码块
同步代码块使用monitorenter和monitorexit两个指令实现同步, The Java® Virtual Machine Specification中有关于这两个指令的介绍:
Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
- If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.- If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
大致含义如下:
每个对象都会与一个monitor相关联,当某个monitor被拥有之后就会被锁住,当线程执行到monitorenter指令时,就会去尝试获得对应的monitor。步骤如下:
- 每个monitor维护着一个记录着拥有次数的计数器。未被拥有的monitor的该计数器为0,当一个线程获得monitor(执行monitorenter)后,该计数器自增变为 1 。
- 当同一个线程再次获得该monitor的时候,计数器再次自增;
- 当不同线程想要获得该monitor的时候,就会被阻塞。
- 当同一个线程释放 monitor(执行monitorexit指令)的时候,计数器再自减。当计数器为0的时候。monitor将被释放,其他线程便可以获得monitor。
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
这段好很好理解,大致含义如下:
当线程执行monitorexit指令时,会去讲monitor的计数器减一,如果结果是0,则该线程将不再拥有该monitor。其他线程就可以获得该monitor了。
总结一下
- 同步方法和同步代码块底层都是通过monitor来实现同步的。
- 两者的区别:同步方式是通过方法中的access_flags中设置ACC_SYNCHRONIZED标志来实现;同步代码块是通过monitorenter和monitorexit来实现
- 我们知道了每个对象都与一个monitor相关联。而monitor可以被线程拥有或释放。
至此我们还有两个问题没有搞清楚:
- 到底啥是monitor?
- 每个对象是如何与monitor关联的?
我们下一篇文章细说。