Java:Synchronized实现原理
一、Synchronized实现同步代码块:
先来看个两个简单的程序
// 代码一
public class OutOfSyncMonitor {
private int data = 0;
public void method1() {
try {
data = data + 1;
// 线程休眠1s,使其在还未执行完毕时,cpu进行时间片轮转
TimeUnit.SECONDS.sleep(1);
System.out.println(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
final OutOfSyncMonitor monitor = new OutOfSyncMonitor();
new Thread(monitor::method1).start();
new Thread(monitor::method1).start();
}
}
// 代码二
public class OutOfSyncMonitor {
private int data = 0;
public void method1() {
synchronized (this) {
try {
data = data + 1;
TimeUnit.SECONDS.sleep(1);
System.out.println(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final OutOfSyncMonitor monitor = new OutOfSyncMonitor();
new Thread(monitor::method1).start();
new Thread(monitor::method1).start();
}
}
可以很容易的看出代码一是线程不安全的,代码二在method1方法中采用synchronized实现同步代码块保证了线程安全,我们再来看个代码
// 代码三
public class OutOfSyncMonitor {
// 所有的OutOfSyncMonitor的对象操作的多是同一个data
private static int data = 0;
public void method1() {
synchronized (this) {
try {
data = data + 1;
TimeUnit.SECONDS.sleep(1);
System.out.println(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
OutOfSyncMonitor monitor = new OutOfSyncMonitor();
OutOfSyncMonitor monitor1 = new OutOfSyncMonitor();
new Thread(monitor::method1).start();
new Thread(monitor1::method1).start();
}
}
在method1方法中还是采用synchronized实现同步代码块,期待保证数据安全,但是令人意外的时输出的data并不是我们所想要的,这样的实现并不能保证线程安全。为什么呢?我们来分析下
synchronized是对同步代码块进行加锁,线程再去争抢锁来达到同步的目的。但是如果锁不唯一呢?换句话来说就是不是同一把锁呢?这显然达不到设计的初衷。而代码三就是这样的,在method1中锁的对象不唯一
public static void main(String[] args) {
OutOfSyncMonitor monitor = new OutOfSyncMonitor();
OutOfSyncMonitor monitor1 = new OutOfSyncMonitor();
new Thread(monitor::method1).start();
new Thread(monitor1::method1).start();
}
在这里创建了两个OutOfSyncMonitor对象,两个线程分别执行它们的method1方法,而在method1中synchronized的对象时this,导致了我们非期望的效果。为什么会出现这种情况呢?我们看下线程的堆栈信息(jstack
"Thread-1" #13 prio=5 os_prio=0 tid=0x0000022a97b14000 nid=0x2d60 waiting on condition [0x000000e8343ff000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at cn.itcod.thread.OutOfSyncMonitor.method1(OutOfSyncMonitor.java:14)
- locked <0x000000076b933780> (a cn.itcod.thread.OutOfSyncMonitor)
at cn.itcod.thread.OutOfSyncMonitor$$Lambda$2/1096979270.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)"Thread-0" #12 prio=5 os_prio=0 tid=0x0000022a97b10000 nid=0x3cf8 waiting on condition [0x000000e8342ff000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at java.lang.Thread.sleep(Thread.java:340)
at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
at cn.itcod.thread.OutOfSyncMonitor.method1(OutOfSyncMonitor.java:14)
- locked <0x000000076b933770> (a cn.itcod.thread.OutOfSyncMonitor)
at cn.itcod.thread.OutOfSyncMonitor$$Lambda$1/1324119927.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
从上可以看出每个线程争抢的monitor关联引用是彼此独立的,这也就导致了锁失败的原因。可以采取以下方法来实现我们的预期效果
public class OutOfSyncMonitor {
private static int data = 0;
public void method1() {
synchronized (OutOfSyncMonitor.class) {
try {
data = data + 1;
TimeUnit.SECONDS.sleep(1);
System.out.println(data);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {// 省略}
}
我们通过锁类来实现同步代码块。这就有两个概念:对象锁 和 类锁
1、对象级别的锁是锁定在对象上的一把锁,只有在线程在访问的是同一个对象时,才会通过竞争来获取得锁。
2、类级别的锁是锁定在类上的一把锁,当线程执行这类的方法时,不管调用的对象是否是同一个,多会产生锁竞争来获得锁。
二、Synchronized的实现原理
我们使用javap对代码一和代码二进行反汇编看下JVM指令:
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=4, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter
4: aload_0
5: aload_0
6: getfield #2 // Field data:I
9: iconst_1
10: iadd
11: putfield #2 // Field data:I
14: getstatic #3 // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;
17: lconst_1
18: invokevirtual #4 // Method java/util/concurrent/TimeUnit.sleep:(J)V
21: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
24: aload_0
25: getfield #2 // Field data:I
28: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
31: goto 39
34: astore_2
35: aload_2
36: invokevirtual #8 // Method java/lang/InterruptedException.printStackTrace:()V
39: aload_1
40: monitorexit
41: goto 49
44: astore_3
45: aload_1
46: monitorexit
47: aload_3
48: athrow
49: return
我们可以看到代码二的反汇编第9行和第40行有两个特别的指令monitorenter和monitorexit,并且是成对出现的(有些时候会出现一个monitorenter多个monitorexit,但是每一个monitorexit之前必有对应的monitor enter,这是肯定的。【Java高并发编程详解:汪文君】);在不加锁的代码一中却没有出现这两个指令。
public void method1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=3, locals=2, args_size=1
0: aload_0
1: aload_0
2: getfield #2 // Field data:I
5: iconst_1
6: iadd
7: putfield #2 // Field data:I
10: getstatic #3 // Field java/util/concurrent/TimeUnit.SECONDS:Ljava/util/concurrent/TimeUnit;
13: lconst_1
14: invokevirtual #4 // Method java/util/concurrent/TimeUnit.sleep:(J)V
17: getstatic #5 // Field java/lang/System.out:Ljava/io/PrintStream;
20: aload_0
21: getfield #2 // Field data:I
24: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
27: goto 35
30: astore_1
31: aload_1
32: invokevirtual #8 // Method java/lang/InterruptedException.printStackTrace:()V
35: return
这是synchronized关键字包裹monitor enter和monitor exit两个JVM指令,它能够保证在如何时候线程执行到monitor enter成功之前必须从主内存中获取数据,而不是从缓存中,在monitor exit运行成功后,共享变量被更新后的值必须刷入主内存。下面来讲下这两个指令
1、monitor enter
每个对象多有与monitor与之关联,一个monitor的lock锁只能被一个线程在同一时间获取,在一个线程尝试获取与锁对象相关联的monitor时会发生以下几件事情。
如果monitor的计数器为0,意味着该monitor的lock锁还没有被获取,当一个线程获的后会立刻对该计数器+1,这样就代表这该monitor被占有
如果一个已经拥有该monitor所有权的线程重入,则会导致monitor的计数器再次被累加
如果monitor已经被其他线程占有,其他线程尝试获取该monitor的所有权时,被陷入到阻塞状态,知道monitor计数器变为0,才再次尝试获取monitor所有权
2、monitor exit
释放对monitor的所有权,前提是曾经获得过所有权。释放的过程较为简单,就是将monitor的计数器-1,如果计数器的结果为0。则代表这线程失去了对该monitor的所有权,与此同时被该monitor block的线程将再次尝试获取该monitor的所有权。
PS:如有不足,还望大佬斧正