synchronized关键字,再java 程序开发中经常会使用。
当声明 synchronized代码块时,编译而成的字节码会包含Monitorenter和moinitorexit指令。这两种指定均会消耗消耗操作数栈上的一个引用类型的元素(也就是 synchronized 关键字括号里的引用),作为所要加锁解锁的锁对象。
示例:
public class App {
public static void main(String[] args) throws Exception {}
public void foo(Object lock) {
synchronized (lock) {
lock.hashCode();
}
}
public synchronized void foo2(Object lock) { lock.hashCode(); }
public static synchronized void foo3(Object lock) { lock.hashCode(); }
}
使用命令查看字节码如下:
public void foo(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC
Code:
stack=2, locals=4, args_size=2
0: aload_1
1: dup
2: astore_2
(1) 3: monitorenter
4: aload_1
5: invokevirtual #2 // Method java/lang/Object.hashCode:()I
8: pop
9: aload_2
(2) 10: monitorexit
11: goto 19
14: astore_3
15: aload_2
16: monitorexit
17: aload_3
18: athrow
19: return
Exception table:
from to target type
4 11 14 any
14 17 14 any
LineNumberTable:
line 5: 0
line 6: 4
line 7: 9
line 8: 19
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 14
locals = [ class App, class java/lang/Object, class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4
public synchronized void foo2(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, (3)ACC_SYNCHRONIZED
Code:
stack=1, locals=2, args_size=2
0: aload_1
1: invokevirtual #2 // Method java/lang/Object.hashCode:()I
4: pop
5: return
LineNumberTable:
line 10: 0
public static synchronized void foo3(java.lang.Object);
descriptor: (Ljava/lang/Object;)V
flags: ACC_PUBLIC, ACC_STATIC, (4)ACC_SYNCHRONIZED
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokevirtual #2 // Method java/lang/Object.hashCode:()I
4: pop
5: return
LineNumberTable:
line 12: 0
}
(1)(2) :普通代码块生成的字节码,发现程序调用monitorEnter,monitorExit表示加锁与解锁。发现代码有一个monitorEnter有多个monitorExit对应,这是为了处理程序出现异常时也能释放锁
(3)(4) :这里是隐式添加的monitorEnter和monitorExit。成员函数有synchronized关键字时加锁对像为this,静态函数有synchronized关键字时加锁对象为当前class 对象。
当前线程执行到monitorEnter时,如果当前目标锁对象没有被其他线程锁住。则会获取锁,并将锁对象的计数器加一。
当线程执行到monitorExit时,则会将目标锁计数器减一
HotSpot 虚拟机实现
重量锁:实现是依赖于系统实现的,是jvm最基础的锁机制。例如linux是用pthread实现。但他是重量级锁需要调用系统内核态代码。为了避免这些昂贵的操作(线程阻塞,唤醒等)。
轻量锁:jvm会尝试自旋处理一段时间如果此时目标对象锁恰好释放了则线程不会进入阻塞状态。
jvm会自适应自选,根据以往自选登台时间,动态调整循环数目,其他线程是有该锁,此时jvm会把锁膨胀为重量级锁,并且阻塞当前线程。
偏向锁:从始至终只有一个线程请求锁的时候,使用偏向锁,当请求加锁的线程和锁对象标记字段保持的线程地址不匹配时(而且 epoch 值相等,如若不等,那么当前线程可以将该锁重偏向至自己),Java 虚拟机需要撤销该偏向锁,如果某一类锁对象的总撤销数超过了一个阈值(对应 Java 虚拟机参数 -XX:BiasedLockingBulkRebiasThreshold,默认为 20),那么 Java 虚拟机会宣布这个类的偏向锁失效。并将epoch值加1表示当前一代的偏向锁已经失效,而新的变相所则需要复制新的epoch值。
为了保证当前持有偏向锁并且已加锁的线程不至于因此丢锁,Java 虚拟机需要遍历所有线程的 Java 栈,找出该类已加锁的实例,并且将它们标记字段中的 epoch 值加 1。该操作需要所有线程处于安全点状态。
如果总撤销数超过另一个阈值(对应 Java 虚拟机参数 -XX:BiasedLockingBulkRevokeThreshold,默认值为 40),那么 Java 虚拟机会认为这个类已经不再适合偏向锁,再之后加锁过程中直接为该实例设置轻量级锁。
借鉴:https://wiki.openjdk.java.net/display/HotSpot/Synchronization