本文,我们主要将探讨如何在安全的前提下高效的共享数据
- 数据同步
- synchronized关键字
- This monitor 和 Class monitor的详细介绍
- 程序死锁的原因和诊断
数据同步
首先我们来看一个数据不一致的例子
public class TicketWindowRunnable implements Runnable {
private int index = 1;
private final static int MAX = 500;
@Override
public void run() {
while (index <= MAX)
System.out.println(Thread.currentThread() + " 的号码是: " + (index++));
}
public static void main(String[] args) {
final TicketWindowRunnable task = new TicketWindowRunnable();
Thread wind1 = new Thread(task, " 1 号 窗口");
Thread wind2 = new Thread(task, " 2 号 窗口");
Thread wind3 = new Thread(task, " 3 号 窗口");
Thread wind4 = new Thread(task, " 4 号 窗口");
wind1.start();
wind2.start();
wind3.start();
wind4.start();
}
}
运行多次我们发现
- 某个号码被忽略过没有出现
- 某个号码多次显示
- 号码超过了最大值500
数据不一致的分析
问题的发生主要是下列这个片段代码的引入
public void run() {
while (index <= MAX)
System.out.println(Thread.currentThread() + " 的号码是: " + (index++));
}
某个号码被忽略过没有出现
线程的执行有CPU轮询调度,假设线程1 和 2 都执行到 index = 65 位置,其中线程 2 将 index 修改为 66 之后为输出,此时CPU调度将执行权交给了线程 1,线程 1 直接拿到其累加的67,此时 66 就被忽略了
某个号码多次显示
线程 1 执行 index + 1,然后CPU执行到线程 2 手上,由于线程 1 没有给index 赋值,导致计算结果重复出现
号码超过了最大值
当 index = 499 的时候,线程 1 和 线程 2 都看到条件满足,线程 2 暂停,线程 1 将index加 到 500 ,此时线程 2 恢复后又将 500 添加到501,此时就出现超过最大值的情况
synchronized关键字
通过上述分析,不难发现数据的不一致主要原因是多个线程对index变量(共享变量)同时操作,要解决这个问题只需要引入synchronized关键字即可,synchronized 提供了一种排他锁的机制,可以保证在同一时间只能有一个线程执行某些操作
什么是synchronized
synchronized 关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都可以通过同步的方式进行,具体表现如下
- synchronized 关键字提供一种锁的机制,能确保共享变量的互斥访问,从而防止数据不一致的问题出现
- synchronized 关键字包括monitor enter 和 monitor exit 两个JVM 指令,它能保证在任何线程执行到monitor enter 成功之前都必须从主内存中获取,而不是缓存中,字monitor exit 运行成功之后,共享变量被更新后的值必须刷入主内存
- synchronized 的指令严格遵守Java happens-before 规则,一个monitor exit 指令之前必定要有一个monitor enter
synchronized 关键字的使用
synchronized可以用来对代码块或者方法修饰,而不能对class以及变量进行修饰
synchronized关键字提供了一种互斥机制,也就是在同一时刻,只能有一个线程访问同步资源,具体来说某线程获取了与synchronized关联的monitor锁synchronized关键字
通过上述分析,不难发现数据的不一致主要原因是多个线程对index变量(共享变量)同时操作,要解决这个问题只需要引入synchronized关键字即可,synchronized 提供了一种排他锁的机制,可以保证在同一时间只能有一个线程执行某些操作
什么是synchronized
synchronized 关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都可以通过同步的方式进行,具体表现如下
- synchronized 关键字提供一种锁的机制,能确保共享变量的互斥访问,从而防止数据不一致的问题出现
- synchronized 关键字包括monitor enter 和 monitor exit 两个JVM 指令,它能保证在任何线程执行到monitor enter 成功之前都必须从主内存中获取,而不是缓存中,字monitor exit 运行成功之后,共享变量被更新后的值必须刷入主内存
- synchronized 的指令严格遵守Java happens-before 规则,一个monitor exit 指令之前必定要有一个monitor enter
synchronized 关键字的使用
synchronized可以用来对代码块或者方法修饰,而不能对class以及变量进行修饰
synchronized关键字提供了一种互斥机制,也就是在同一时刻,只能有一个线程访问同步资源,具体来说某线程获取了与synchronized关联的monitor锁
JVM指令分析
通过javap对Mutex class 进行反编译,可以发现monitor enter 和 monitor exit 是成对出现的
public class Mutex {
private final static Object MUTEX = new Object();
public void accessResource(){
synchronized (MUTEX){
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
final Mutex mutex = new Mutex();
for(int i=0; i<5; i++){
new Thread(mutex::accessResource, "main-" + i).start();
}
}
}
运行 javac -c
public class chapter4.Mutex {
public chapter4.Mutex();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void accessResource();
Code:
0: getstatic #2 // 获取MUTEX
3: dup
4: astore_1
5: monitorenter // 执行 monitorenter 指令
6: getstatic #3 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
9: ldc2_w #4 // long 10l
12: invokevirtual #6 // Method java/util/concurrent/TimeUnit.sleep:(J)V
15: goto 23
18: astore_2
19: aload_2
20: invokevirtual #8 // Method java/lang/InterruptedException.printStackTrace:()V
23: aload_1
24: monitorexit // 执行 monitorexit 指令
25: goto 33
28: astore_3
29: aload_1
30: monitorexit
31: aload_3
32: athrow
33: return
Exception table:
from to target type
6 15 18 Class java/lang/InterruptedException
6 25 28 any
28 31 28 any
}
Monitorenter
每一个对象都与一个 monitor 相关联,一个 monitor 的 lock 的锁只能被一个线程再同一时间获得,在一个线程尝试获得与对象关联的 monitor 的所有权会发生如下几个事情
- 如果 monitor 的计数器为0,则意味该 monitor 的 lock 还没有被获得,某个线程获得之后将立即对该计数器+1,从这该线程就是这个 monitor 的所有者
- 如果一个已经拥有该 monitor 的所有权线程重入,则会导致 monitor 计数器再次累加
- 如果 monitor 已经被其他线程所拥有,则其他线程尝试获取该monitor 的所有权时,会被陷入阻塞状态直到 monitor计数器变为0,才能再次尝试获取对 monitor 的所有权
Monitorexit
释放对 monitor 所有权,想释放对某个对象关联的 monitor 的所有权的前提是,你曾经获得过所有权,释放 monitor 所有权的过程比较简单,就是将 monitor 的计数器 -1,如果计数器的结果为0,就意味着该线程不再拥 monitor 的所有权,此时被该 monitor block 的线程将再次尝试获取对 monitor 的所有权
使用 synchronized需要注意的问题
-
与 monitor 关联的对象不能为 null (每一个对象 都有 一个 monitor 相关联)
-
synchronized 作用域太大,由于synchronized 具有排他性,如果作用域太大,代表其效率越低
-
不同的 monitor 企图锁相同的方法
public class Task implements Runnable {
private final Object MUTEX = new Object();
@Override
public void run() {
synchronized (MUTEX){
// …
}
}public static void main(String[] args) { for(int i=0; i<5; i++) new Thread(Task::new).start(); } }
上面的代码构造了5个线程,同时也构造了5个Runnable实例,Runnable作为线程逻辑执行单元传递给Thread,但是互斥不了对应的作用域,线程之间进行 monitor lock 的争抢智能与monitor 关联的同一个引用上,上面的代码争抢的 monitor 关联引用都是彼此独立的,因此不可能起到互斥的作用
-
多个锁的交叉导致死锁(稍后讲解)
this monitor
使用 synchronized 关键字同步类的不同实例方法,争抢的是同一个 monitor 的lock,与之关联的引用是对象的引用class monitor
使用 synchronized 关键字同步类的不同静态方法,争抢的是同一个 monitor 的lock,与之关联的引用是对象class 的实例