深入理解 synchronized 原理
1. Synchronized使用
1.1. 修饰实例方法
public class SynchronizedTest {
int i = 0;
public synchronized void monitor(){
i++;
}
public synchronized void monitor1(){
i++;
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
new Thread(()->{
test.monitor();
}).start();
new Thread(()->{
test.monitor1();
}).start();
}
}上面代码我们在 SynchronizedTest 类中有 monitor() 以及 monitor1() 两个方法.
分别都加了一个 synchronized 两个方法都操作了一个 int 为 i 的共享变量,看main方法比如有两个线程, 线程A访问 monitor() 线程B访问 monitor1() 都是用的 test对象进行调用的,此时我们的 锁的范围 当前 test对象。
修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁。同一Class类不同的实例对象锁的范围,已对象为单位
1.2. 静态对象
public class SynchronizedTest {
static int i = 0;
public synchronized void monitor(){
i++;
}
public static synchronized void monitor1(){
i++;
}
public static void main(String[] args) {
SynchronizedTest test = new SynchronizedTest();
new Thread(()->{
test.monitor();
}).start();
new Thread(()->{
SynchronizedTest.monitor1();
}).start();
}
}上面代码我们在 SynchronizedTest 类中有 monitor() 以及 monitor1() 两个方法.
分别都加了一个 synchronized 两个方法都操作了一个 static int 为 i 的共享变量,看main方法比如有两个线程, 线程A访问 test.monitor(),线程B访问 SynchronizedTest.monitor1(),main运行肯定是线程不安全的,两个方法都加了 synchronized 但是锁的对象不同,一个是实例对象,一个是静态对象
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁。静态变量为全局唯一的,所有对象在锁静态对象时间单位为Class。不同的对象使用静态变量为锁时,所有对象使用的是同一吧锁
1.3. 代码块
public class SynchronizedTest {
Object lock = new Object();
static Object lockStatic = new Object();
static int i = 0;
public void monitor(){
synchronized (lock) {
i++;
}
}
public void monitor1(){
synchronized (lockStatic) {
i++;
}
}
public synchronized void monitor2(){
i++;
}
}修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。代码块加锁需要用实例对象或者静态对象,看自己定义锁的范围标准。
1.4. 如上代码查看同步方法的class文件
public void monitor();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: getfield #3 // Field lock:Ljava/lang/Object;
4: dup
5: astore_1
6: monitorenter
7: getstatic #4 // Field i:I
10: iconst_1
11: iadd
12: putstatic #4 // Field i:I
15: aload_1
16: monitorexit
17: goto 25
20: astore_2
21: aload_1
22: monitorexit
23: aload_2
24: athrow
25: return
...public synchronized void monitor2();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=0, locals=1, args_size=1
0: return
LineNumberTable:
line 18: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lcom/springstudy/simplespring/SynchronizedTest;- monitor() 方法,同步代码块的起始位置插入了 monitorenter 指令,在同步代码块结束的位置插入了 monitorexit指令。(JVM需要保证每一个monitorenter都有一个monitorexit与之相对应,但每个monitorexit不一定都有一个monitorenter)
- monitor2() 方法,翻译成普通的方法调用和返回指令,只是在其常量池中多了 ACC_SYNCHRONIZED 标示符
JVM就是根据该标示符来实现方法的同步的:当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先获取monitor,获取成功之后才能执行方法体,方法执行完后再释放 monitor。在方法执行期间,其他任何线程都无法再获得同一个 monitor 对象。其实本质上没有区别,只是方法的同步是一种隐式的方式来实现,无需通过字节码来完成。 monitorenter 和 monitorexit 指令是通过 monitor 对象实现的。
1.5. monitorenter
每个对象都有一个监视器锁(monitor)与之对应。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:
- 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者.
- 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
- 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权.
1.6. monitorexit
执行monitorexit的线程必须是objectref所对应的monitor的所有者。
指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。
其他被这个monitor阻塞的线程可以尝试去获取这个monitor的所有权。
通过这两个指令我们应该能很清楚的看出synchronized的实现原理,synchronized的语义底层是通过一个monitor的对象来完成,其实wait/notify等方法也依赖于monitor对象,这就是为什么只有在同步的块或者方法中才能调用wait/notify等方法,否则会抛出java.lang.IllegalMonitorStateException的异常的原因。