synchronized与monitor(监视器)的关系
synchronized 译同步的,但我们平时也称之为锁。它呈现给编程人员的视角是:
① synchronized 作用于普通方法,锁的对象是当前实例
② synchronized 作用于静态方法,锁的对象是类的Class对象
③ synchronized 作用于方法块,锁的对象是括号里匹配的对象
如下代码:
public class Test {
private static int a = 0;
public static void main(String args []) {
Test test = new Test();
test.testOne(2);
}
public void testOne(int a) {
synchronized (this) {
a++;
}
}
public synchronized void testTwo() {
a++;
}
public static synchronized void testThree() {
a++;
}
}
反编译后:
public void testOne(int);
descriptor: (I)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_0
1: dup
2: astore_2
3: monitorenter
4: iinc 1, 1
7: aload_2
8: monitorexit
9: goto 17
12: astore_3
13: aload_2
14: monitorexit
15: aload_3
16: athrow
17: return
Exception table:
from to target type
4 9 12 any
12 15 12 any
LineNumberTable:
line 11: 0
line 12: 4
line 13: 7
line 14: 17
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 12
locals = [ class Test, int, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void testTwo();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #5 // Field a:I
3: iconst_1
4: iadd
5: putstatic #5 // Field a:I
8: return
LineNumberTable:
line 17: 0
line 18: 8
public static synchronized void testThree();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=0, args_size=0
0: getstatic #5 // Field a:I
3: iconst_1
4: iadd
5: putstatic #5 // Field a:I
8: return
LineNumberTable:
line 21: 0
line 22: 8
可以很清楚的看到,不管synchronized 作用于普通方法还是静态方法,都是在其方法对应的 flags 设置为:ACC_SYNCHRONIZED. 也就是将运行时常量池对应的 method_info 结构中的访问标志置为:ACC_SYNCHRONIZED.
而对于synchronized 作用于方法块,则是使用monitorenter 和 monitorexit 指令来实现。
不管是将方法的access_flags: 置为ACC_SYNCHRONIZED ,还是采用monitorenter 和 monitorexit 指令。两者的实现都是在进入同步代码的开始处尝试获取对象所关联的监视器(即获取这个对象的锁)
监视器是JVM用来实现synchronized语义的。
Java中的监视器支持两种线程通信:互斥和协作。互斥就是获取实例对象或类对象的锁;协作就是Object 类中的 wait, notify, notifyall
如图:JVM中的监视器模型可以分为三个区域,入口区、持有者、等待区。当一个线程到达监视区的开始处时,它会通过最左边的一号门进入监视器;如果发现没有其它线程持有监视器,也没有其它线程在入口处等待,这个线程就会通过下一道门——2号门,并持有监视器,作为监视器的持有者,它将继续执行监视区域中的代码。也可能出现这样的情况,已经有另一个线程正持有监视器,这个线程会被阻塞。当监视器的持有者执行Object.wait 方法时,会释放对象的锁,进入等待区,直到某个时候持有该对象锁的另一个线程执行了notify方法或notifyAll方法后才从等待区中苏醒,并和入口区和等待区中的其他线程竞争获取对象锁。区别notity 和 notifyAll 方法,notify方法会随机从等待区中唤醒一个线程,notifyAll方法会唤醒全部等待区中的线程。
对象头
Java对象头:以JDK 1.8 为例
// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
锁的标志位:
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
// not valid at any other time
偏向锁
特征:对象偏向于某个线程,等到存在锁竞争的时候,才会撤销锁。
缺点:如果线程间存在锁的竞争,会带来额外的锁的撤销操作。
使用场景:使用于只有一个线程访问同步块场景
偏向锁的获取
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下MarkWord中偏向锁的标志是否置为1,如果没有设置则使用CAS竞争锁;如果设置了,尝试使用CAS将对象头的偏向锁指向当前线程。
偏向锁的撤销
偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放。
轻量级锁
特征:当锁存在竞争的时候,使用自旋的方式尝试获取锁。
优点:竞争的线程 不会阻塞,提高了程序的响应速度。
缺点:自旋会消耗CPU
适用场景:追求响应时间,同步块执行速度非常快
重量级锁 也叫监视器锁
特征:线程会阻塞
优点:线程竞争不会使用自旋,不会消耗CPU
缺点:线程阻塞,响应时间缓慢
适用场景:追求吞吐量,同步块执行速度较长。
本文是对《Java并发编程的艺术》2.2 synchronized 的实现原理与应用的总结,部分材料引用了《深入Java虚拟机》第二版,第二十章的内容。