java程序中我们可以使用synchronized关键字对程序加锁,它可以保证方法或者代码块运行时同一时刻只有一个方法可以进入到临界区域,同时它还可以保证共享变量的内存可见性,synchronzied关键字可以声明一个同步代码块,也可以用来修饰静态方法或者实例方法。当synchronized修饰在不同地方时,它也是在对不同对象进行加锁操作:
1.同步代码块:锁是括号里面的对象
2.静态方法:琐是当前类的class对象
3.实例方法:锁是当前实例对象
当声明synchronized代码块时,编译而成的字节码将会包含monitorenter和monitorexit两个指令,这两种指令都会消耗操作数栈上的一个引用类型的元素(代码块中小括号里面的对象)作为加锁和解锁的对象。
public void foo(Object sync) {
synchronized (sync) {
sync.hashCode();
}
}
// 上面的 Java 代码将编译为下面的字节码
public void foo(java.lang.Object);
Code:
0: aload_1
1: dup
2: astore_2
3: monitorenter
4: aload_1
5: invokevirtual java/lang/Object.hashCode:()I
8: pop
9: aload_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
从上面的字节码中可以看到有一个monitorenter指令和多个monitorexit指令,这是因为要确保在任何情况下退出都会释放掉锁。关于monitorenter和monitorexit的作用,我们可以抽象的理解为每个锁对象都拥有一个锁的计数器和一个持有该锁的线程指针。
当执行到monitorenter时如果锁计数器个数为零则代表没有其他线程锁定,这时java虚拟机会将锁对象的持有线程设置为当前线程,并把计数器设置为1。当锁计数器不为零时判断持有锁对象的线程是不是当前线程,如果是当前线程则把计数器加1。(因为synchronized是可重入锁)当执行到monitorexit时,jvm会将计数器个数减1。当计数器等于零的时候代表锁已经释放。
当synchronized标记方法时,在字节码中flags包括ACC_SYNCHRONIZED。此标记标识进入该方法时java虚拟机要进行monitorenter操作,在退出时进行(正常退出或者异常退出)monitorexit操作。
二 、JAVA对象头
在java虚拟机中,每个java对象都有一个对象头(object header),由标记字段(Mark Word)和类型指针(Klass Pointer)构成。标记字段用来存储对象运行时Java虚拟机有关该对象的运行数据,如哈希码、GC信息、锁状态标志、线程持有的锁等等。类型指针是对象指向它的类元数据的指针,Java虚拟机通过该指针确定这个对象是什么类的实例。
三、JVM锁优化
重量级锁
重量级锁是java虚拟机中最基本的锁的实现方式,在这种锁状态下,获取锁失败的线程会进入阻塞状态,当目标锁被释放时唤醒阻塞线程。
java线程中的阻塞和唤醒都是依靠操作系统来实现的,但是这种方式会有系统调用,需要从操作系统的用户状态切换到内核状态,开销很大。