synchronized是Java 内置的管程方案,synchronized 关键字修饰的代码块和方法在编译期会自动生成相关加锁和解锁的代码。
举个例子, synchronized 分别加在代码块和方法上。
public void methodA() {
synchronized (this) {
System.out.println("methodA");
}
}
synchronized static void methodB() {
System.out.println("methodB");
}
javap -v 反编译来看。
public void methodA();
descriptor: ()V
flags: (0x0001) ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
7: ldc #3 // String methodA
9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: aload_1
13: monitorexit // 正常退出
14: goto 22
17: astore_2
18: aload_1
19: monitorexit // 异常退出
20: aload_2
21: athrow
22: return
...
static synchronized void methodB();
descriptor: ()V
flags: (0x0028) ACC_STATIC, ACC_SYNCHRONIZED //同步方法标记
Code:
stack=2, locals=0, args_size=0
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: ldc #5 // String methodB
5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
8: return
...
-
同步代码块是使用 monitorenter 和 monitorexit 指令实现。
methodA经反编译后的第3、13、19行指令分别对应的是 monitorenter 和 两个monitorexit指令。其中monitorexit 被插入到方法正常结束处和异常处两个地方,这样可以保证抛异常的情况下也能释放锁。
同步代码块开始执行时,执行 monitorenter 的线程会尝试获得 monitor 的所有权。如果该 monitor 的计数为 0,则线程获得该 monitor 并将其计数置为 1,该线程就是这个 monitor 的所有者。 如果线程已经拥有了这个 monitor ,则可以重入这个锁,且累加计数。 如果其他线程已经拥有了这个 monitor,那个这个线程就会被阻塞,直到这个 monitor 被释放它的计数为 0,这个线程就会再次尝试获取这个 monitor。
monitorexit 的作用是将 monitor 的计数器减 1,当减为 0 时代表这个 monitor 被释放,其他正在等待这个 monitor 的线程就可以再次尝试获取这个 monitor 的所有权。
-
同步方法是通过ACC_SYNCHRONIZED 标记实现。
被 synchronized 修饰的方法会有一个 ACC_SYNCHRONIZED 标志。当线程要访问该方法的时候,首先检查方法是否有 ACC_SYNCHRONIZED 标志,如果有则需要先获得 monitor 锁,然后才能开始执行方法,方法执行之后再释放 monitor 锁。如果这时其他线程来请求该方法,会因为无法获得 monitor 锁而被阻塞。