什么是共享资源?
共享资源指的是多个线程同时对同一份资源进行访问(读写操作),被多个线程访问的资源就称为共享资源,如何保证多个线程访问到的数据是一致的,则被称为数据同步或者资源同步。
1.数据同步
1.1数据不一致问题的引入
public class TicketWindowRunnable implements Runnable{
private int index = 1;
private final static int MAX=50;
public void run() {
while(index <= 50) {
System.out.println(Thread.currentThread() + " 的号码是:" + (index++));
}
}
public static void main(String[] args) {
final TicketWindowRunnable task = new TicketWindowRunnable();
Thread thread = new Thread(task, "一号窗口");
Thread thread1 = new Thread(task, "二号窗口");
Thread thread2 = new Thread(task, "三号窗口");
Thread thread3 = new Thread(task, "四号窗口");
thread.start();
thread1.start();
thread2.start();
thread3.start();
}
}
第一,某个号码被略过没有出现。
第二,某个号码被多次显示。
第三,号码超过了最大值500。
1.2 数据不一致问题原因分析
1.2.1号码被略过
如图所示, 线程的执行是由CPU时间片轮询调度的, 假设此时线程1和2都执行到了index=65的位置, 其中线程2将index修改为66之后未输出之前, CPU调度器将执行权利交给了线程1,线程1直接将其累加到67,那么66就被忽略了。
1.2.2号码重复出现
线程1执行index+1, 然后CPU执行权落入线程2手里, 由于线程1并没有给index赋予计算后的结果393, 因此线程2执行index+1的结果仍然是393, 所以会出现重复号码的情况。
1.2.3号码超过了最大值
下面来分析一下号码超过最大值的情况, 当index=499的时候, 线程1和线程2都看到条件满足, 线程2短暂停顿, 线程1将index增加到了500, 线程2恢复运行后又将500增加到了501,此时就出现了超过最大值的情况。
我们虽然使用了时序图的方式对数据同步问题进行了分析,但是这样的解释还是不够严谨, 本书的第三部分将会讲解Java的内存模型以及CPU缓存等知识, 到时候会更加清晰和深入地讲解数据不一致的问题。
2.初识synchronized关键字
2.1什么是synchronized
上述解释的意思是:synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行,具体表现如下。
- synchronized关键字提供了一种锁的机制, 能够确保共享变量的互斥访问, 从而防止数据不一致问题的出现。
- synchronized关键字包括monitor enter和monitor exit两个JVM指令, 它能够保证在任何时候任何线程执行到monitor enter成功之前都必须从主内存中获取数据, 而不是从缓存中, 在monitor exit运行成功之后, 共享变量被更新后的值必须刷人主内存(在本书的第三部分会重点介绍)。
- synchronized的指令严格遵守java happens-before规则, 一个monitor exit指令之前必定要有一个monitor enter(在本书的第三部分会详细介绍) 。
2.2synchronized关键字的用法
synchronized可以用于对代码块或方法进行修饰, 而不能够用于对class以及变量进行修饰。2.2.1同步方法
同步方法的语法非常简单即:[default|public|private|protected] synchronized[static] type method() 。示例代码如下:public synchronized void sync() {}
public synchronized static void Sync(){}
2.2.2.同步代码块
同步代码块的语法示例如下: ```java private final Object MUTEX = new Object();
public void sync() { synchronized (MUTEX) {
}
}
<a name="LhPx9"></a>
## 3.深入理解synchronized关键字
<a name="cIflj"></a>
### 3.1 线程堆栈分析
synchronized关键字提供了一种互斥机制, 也就是说在同一时刻, 只能有一个线程访问同步资源, 很多资料、书籍将synchronized(mutex) 称为锁, 其实这种说法是不严谨的,准确地讲应该是某线程获取了与mutex关联的monitor锁(当然写程序的时候知道它想要表达的语义即可),下面我们来看一个简单的例子对其进行说明:
```java
public class TestHtr {
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 TestHtr testHtr = new TestHtr();
for(int i = 0; i < 5; i++ ) {
new Thread(testHtr::accessResource).start();
}
}
}
使用j stack命令打印进程的线程堆栈信息, 选取其中几处关键的地方对其进行分析。Thread-0持有monitor<0x00000000d7333fd8>的锁并且处于休眠状态中, 那么其他线程将会无法进入access Resource方法, 如图所示。
Thread-1线程进入BLOCKED状态并且等待着获取monitor<0x00000000d7333fd8>的
锁, 其他的几个线程同样也是BLOCKED状态, 如图所示。
3.2JVM指令分析
使用JDK命令java p对Mutex class进行反汇编, 输出了大量的JVM指令, 在这些指令中, 你将发现monitor enter和monitor exit是成对出现的(有些时候会出现一个monitorenter多个monitor exit, 但是每一个monitor exit之前必有对应的monitor enter, 这是肯定的),运行下面的命令:
D:\soft\jdk\bin>javap -c D:\Idea_project\sqlquery\target\test-classes\com\cmb\tool\sqlquery\TestHtr.class
Compiled from "TestHtr.java"
public class com.cmb.tool.sqlquery.TestHtr {
public com.cmb.tool.sqlquery.TestHtr();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void accessResource();
Code:
0: getstatic #2 // Field MUTEX:Ljava/lang/Object;
3: dup
4: astore_1
5: 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
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
public static void main(java.lang.String[]);
Code:
0: new #9 // class com/cmb/tool/sqlquery/TestHtr
3: dup
4: invokespecial #10 // Method "<init>":()V
7: astore_1
8: iconst_0
9: istore_2
10: iload_2
11: iconst_5
12: if_icmpge 42
15: new #11 // class java/lang/Thread
18: dup
19: aload_1
20: dup
21: invokevirtual #12 // Method java/lang/Object.getClass:()Ljava/lang/Class;
24: pop
25: invokedynamic #13, 0 // InvokeDynamic #0:run:(Lcom/cmb/tool/sqlquery/TestHtr;)Ljava/lang/Runnable;
30: invokespecial #14 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
33: invokevirtual #15 // Method java/lang/Thread.start:()V
36: iinc 2, 1
39: goto 10
42: return
static {};
Code:
0: new #16 // class java/lang/Object
3: dup
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: putstatic #2 // Field MUTEX:Ljava/lang/Object;
10: return
}
选取其中的片段, 进行重点分析。①获取到MUTEX引用, 然后②执行monitor enterJVM指令, 休眠结束之后goto至③monitor exit的位置(a store
public void accessResource();
Code:
0: getstatic #2 // 1)获取Mutex // Field MUTEX:Ljava/lang/Object;
3: dup
4: astore_1
5: monitorenter // 2)执行monitor enter
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 // 3)goto 到23行
18: astore_2
19: aload_2
20: invokevirtual #8 // Method java/lang/InterruptedException.printStackTrace:()V
23: aload_1 // 4)
24: monitorexit 5) 执行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
(1) Monitor enter
每个对象都与一个monitor相关联, 一个monitor的lock的锁只能被一个线程在同一时间获得, 在一个线程尝试获得与对象关联monitor的所有权时会发生如下的几件事情。
- 如果monitor的计数器为0, 则意味着该monitor的lock还没有被获得, 某个线程获得之后将立即对该计数器加一, 从此该线程就是这个monitor的所有者了。
- 如果一个已经拥有该monitor所有权的线程重入, 则会导致monitor计数器再次累加。
- 如果monitor已经被其他线程所拥有, 则其他线程尝试获取该monitor的所有权时, 会被陷入阻塞状态直到monitor计数器变为0, 才能再次尝试获取对monitor的所有权
(2) Monitor exit
释放对monitor的所有权, 想要释放对某个对象关联的monitor的所有权的前提是, 你曾经获得了所有权。释放monitor所有权的过程比较简单, 就是将monitor的计数器减一,如果计数器的结果为0, 那就意味着该线程不再拥有对该monitor的所有权, 通俗地讲就是解锁。与此同时被该monitor block的线程将再次尝试获得对该monitor的所有权。
3.3 使用synchronized需要注意的问题
3.3.1.与monitor关联的对象不能为空
private final Object mutex = null;
public void syncmethod() {
synchronized (mutex) {
}
}
Mutex为null, 很多人还是会犯这么简单的错误, 每一个对象和一个monitor关联, 对象都为null了,monitor肯定无从谈起。
3.3.2.synchronized作用域太大
由于synchronized关键字存在排他性, 也就是说所有的线程必须串行地经过synchronized保护的共享区域, 如果synchronized作用域越大, 则代表着其效率越低, 甚至还会丧失并发的优势,示例代码如下:
public static class Task implements Runnable {
@Override
public synchronized void run() {
}
}
上面的代码对整个线程的执行逻辑单元都进行了synchronized同步, 从而丧失了并发的能力, synchronized关键字应该尽可能地只作用于共享资源(数据) 的读写作用域。
3.3.3.不同的monitor企图锁相同的方法
public static 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();
}
}
上面的代码构造了五个线程, 同时也构造了五个Runnable实例, Runnable作为线程逻辑执行单元传递给Thread, 然后你将发现, synchronized根本互斥不了与之对应的作用域,线程之间进行monitor lock的争抢只能发生在与monitor关联的同一个引用上, 上面的代码每一个线程争抢的monitor关联引用都是彼此独立的, 因此不可能起到互斥的作用。
3.3.4 多个锁的交叉导致死锁
多个锁的交叉很容易引起线程出现死锁的情况,程序并没有任何错误输出,但就是不工作,如下面的代码所示(本书的4.5节中会分析死锁的原因以及教大家如何对其进行诊断):
private final Object MUTEX_READ = new Object();
private final Object MUTEX_WRITE = new Object();
public void read() {
synchronized (MUTEX_READ) {
synchronized (MUTEX_WRITE) {
}
}
}
public void write() {
synchronized (MUTEX_WRITE) {
synchronized (MUTEX_READ) {
}
}
}
4.ThisMonitor和ClassMonitor的详细介绍
4.1 thismonitor
在下面的代码This Monitor中, 两个方法method 1和method 2都被synchronized关键字修饰, 启动了两个线程分别访问method 1和method 2, 在开始运行之前请读者思考一个问题:synchronized关键字修饰了同一个实例对象的两个不同方法, 那么与之对应的monitor是什么?两个monitor是否一致呢?
笔者将重点的地方用红色的框标识了出来,T1线程获取了<0x00000000d7334700>monitor的lock并且处于休眠状态, 而T 2线程企图获取<0x00000000d7334700>monitor的lock时陷入了BLOCKED状态, 可见使用synchronized关键字同步类的不同实例方法, 争抢的是同一个monitor的lock, 而与之关联的引用则是This Monitor的实例引用, 为了证实我们的推论,将上面的代码稍作修改,如下所示:
其中, method1保持方法同步的方式, method2则采用了同步代码块的方式, 并且使用的是this的monitor, 运行修改后的代码将会发现效果完全一样, 在JDK官方文档中也有这样的描述(见https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html):
Locks In Synchronized Methods
When a thread invokes a synchronized method, it automatically acquires the intrinsic lock for that method's object and releases it when the method returns. The lock release occurs even if the return was caused by an uncaught exception.
You might wonder what happens when a static synchronized method is invoked, since a static method is associated with a class, not an object. In this case, the thread acquires the intrinsic lock for the Class object associated with the class. Thus access to class's static fields is controlled by a lock that's distinct from the lock for any instance of the class.
4.2classmonitor
public class ClassMonitor {
public static synchronized void method1() {
System.out.println(Thread.currentThread().getName() + " enter to method1");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static synchronized void method2() {
System.out.println(Thread.currentThread().getName() + " enter to method2");
try {
TimeUnit.MINUTES.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new Thread(ClassMonitor::method1,"T1").start();
new Thread(ClassMonitor::method2,"T2").start();
}
}
5.程序死锁的原因以及如何诊断
5.1程序死锁
5.1.1 交叉锁可导致程序出现死锁
5.1.2 内存不足
5.1.3 一问一答式的数据交换
5.1.4 数据库锁
5.1.5 文件锁
5.1.6 死循环引起的死锁
5.2死锁案例
举一例交叉死锁
public class DeadLock {
private final Object MUTEX_READ = new Object();
private final Object MUTEX_WRITE = new Object();
public void read() {
synchronized (MUTEX_READ) {
System.out.println(Thread.currentThread().getName() + " get Read lock");
synchronized (MUTEX_WRITE) {
System.out.println(Thread.currentThread().getName() + " get Write lock");
}
System.out.println(Thread.currentThread().getName() + " release write lock");
}
System.out.println(Thread.currentThread().getName() + " release read lock");
}
public void write() {
synchronized (MUTEX_WRITE) {
System.out.println(Thread.currentThread().getName() + " get Write lock");
synchronized (MUTEX_READ) {
System.out.println(Thread.currentThread().getName() + " get Read lock");
}
System.out.println(Thread.currentThread().getName() + " release Read lock");
}
System.out.println(Thread.currentThread().getName() + " release Write lock");
}
public static void main(String[] args) {
final DeadLock deadLock = new DeadLock();
new Thread(
() -> {
while(true) {
deadLock.read();
}
}
, "READ-THREAD").start();
new Thread(
() -> {
while(true) {
deadLock.write();
}
}
, "WRITE-THREAD").start();
}
}