2 Java中的隐式锁
在Java中,提供了关键字synchronized
。这个关键字可以应用在不同的地方,如下面的表格所示:
应用位置 | 锁存在于哪里 | 代码示例 |
---|---|---|
实例方法 | 当前类的实例对象 | public synchronized void method(){......} |
静态方法 | 当前类对象 | public static synchronized void method(){......} |
代码块 | 指定的实例对象 | synchronized(object){......} //这里的object可以是任何的对象实例,比如this,Integer.MAX_VALUE这样的对象实例,都可以 |
代码块 | 指定的类对象 | syncrhonized(Demo.class){......} |
使用了synchronized
关键字之后,就可以给相应的方法/代码块加上锁,保证在一个JVM中,同时只有一个线程能够执行这个方法/这个代码。但是我们并没有看到加锁和释放锁的操作,因此又被称为“隐式锁”。
synchronized
的这个功能是在JVM中通过使用monitor来实现的。
2.1 sychronized对应的字节码
我们先来看看下面的代码:
public class DemoOnInstance {
public int value = 0;
public int addAndGet(int increment){
synchronized (this){
value = value + increment;
return value;
}
}
}
在上面的代码中的addAndGet方法里面,我们使用了synchronized
关键字标记了一个同步代码块,并且是将锁加在了当前实例this
上。那么,JVM是怎么帮我们实现锁的呢。我们可以看看生成的字节码。
我们可以在编译后的class文件上使用javap命令来查看字节码,javap -v DemoOnInstance.class
。得到的结果会包含下面的片段,这个片段是addAndGet方法对应的字节码。
public int addAndGet(int);
descriptor: (I)I
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=2
0: aload_0
1: dup
2: astore_2
3: monitorenter
4: aload_0
5: aload_0
6: getfield #2 // Field value:I
9: iload_1
10: iadd
11: putfield #2 // Field value:I
14: aload_0
15: getfield #2 // Field value:I
18: aload_2
19: monitorexit
20: ireturn
21: astore_3
22: aload_2
23: monitorexit
24: aload_3
25: athrow
Exception table:
from to target type
4 20 21 any
21 24 21 any
上面的字节码中,请注意标记行号为3和19的两行,分别为monitorenter
和monitorexit
。这两行指令告诉JVM要获取Monitor和释放Monitor。 我们可以看到第23行也有monitorexit
,这个是异常情况下的释放monitor的处理。因此无论代码是正常执行还是异常执行,都会执行monitorexit
,保证锁会被释放。
当synchronized
关键字应用在方法上的时候,情况略有不同。
public class DemoOnInstanceMethod {
private int value = 0;
public synchronized int addAndGet(int increment){
value = value + increment;
return value;
}
}
上面的代码中的addAndGet方法编译后的字节码如下:
public synchronized int addAndGet(int);
descriptor: (I)I
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=3, locals=2, args_size=2
0: aload_0
1: aload_0
2: getfield #2 // Field value:I
5: iload_1
6: iadd
7: putfield #2 // Field value:I
10: aload_0
11: getfield #2 // Field value:I
14: ireturn
在字节码中,没有看到monitor相关的指令,但是在方法的flags中,有ACC_SYNCHRONIZED这个值。JVM在执行该方法的时候,如果有这个flag,就会获取锁,方法退出时(无论是正常退出还是异常退出)释放锁。
2.2 有了字节码之后呢?
synchronize
关键字在不同的位置会生成不同的字节码,JVM在执行时会添加获取monitor和释放monitor的操作。 前面我们提到了,不同的代码锁定的对象是不一样的。那么这个是怎么实现的呢?
前面也提到了,不同的代码方式,锁存在的位置是不一样的,这是因为在Java中,对应 synchronized
有两种锁:对象锁和类锁。
对象锁:在非静态方法上使用 synchronized
关键字,或者使用 synchronized(objectInstance)
这样的方式来创建同步代码块,使用的是对象锁。每个对象实例都有一个对象锁,不同的对象实例各自有各自的对象锁。对象锁是线程可重入的,因此在同一个线程中,可以在一个同步方法中调用另外一个同步方法。但是,一个线程在执行一个实例上的同步代码,其他线程如果要执行同样实例上的同步代码,无论是不是相同的代码段,都会被阻塞。如果两个线程执行的是同一个类的不同实例对象的同步代码块,则可以同时执行。
类锁:在静态方法上使用 synchronized
关键字,或者使用 synchronized(Demo.class)
这样的方法创建的同步代码块,使用的就是类锁。一个类只有一个类锁(感觉类似静态变量)。同样的,使用同一个类锁的同步代码段,同一时间只有一个能执行。
类锁和对象锁互不干扰。
2.3 锁的升级
新创建的类锁或者对象锁,都是处于偏向锁的状态。在使用过程中,随着竞争的情况,会逐步升级为轻量锁状态或者重量锁状态。这一点在上一篇文章中已经讲过了,就不再赘述。
2.4 小结
synchronized
关键字会使用“隐式锁”,这个是由字节码和JVM共同来实现的,因此这个锁是“本地锁”,只能应用于同一个JVM中的不同进程,不能应用于不同的JVM之间。
不同的 synchronized
的用法使用的锁不尽相同,有对象锁和类锁两种。对象锁和类锁都是可重入锁和独享锁,同时也都是非公平锁。