什么是共享资源?
共享资源指的是多个线程同时对同一份资源进行访问(读写操作),被多个线程访问的资源就称为共享资源,如何保证多个线程访问到的数据是一致的,则被称为数据同步或者资源同步。

1.数据同步

1.1数据不一致问题的引入

  1. public class TicketWindowRunnable implements Runnable{
  2. private int index = 1;
  3. private final static int MAX=50;
  4. public void run() {
  5. while(index <= 50) {
  6. System.out.println(Thread.currentThread() + " 的号码是:" + (index++));
  7. }
  8. }
  9. public static void main(String[] args) {
  10. final TicketWindowRunnable task = new TicketWindowRunnable();
  11. Thread thread = new Thread(task, "一号窗口");
  12. Thread thread1 = new Thread(task, "二号窗口");
  13. Thread thread2 = new Thread(task, "三号窗口");
  14. Thread thread3 = new Thread(task, "四号窗口");
  15. thread.start();
  16. thread1.start();
  17. thread2.start();
  18. thread3.start();
  19. }
  20. }

第一,某个号码被略过没有出现。
第二,某个号码被多次显示。
第三,号码超过了最大值500。

1.2 数据不一致问题原因分析

1.2.1号码被略过

如图所示, 线程的执行是由CPU时间片轮询调度的, 假设此时线程1和2都执行到了index=65的位置, 其中线程2将index修改为66之后未输出之前, CPU调度器将执行权利交给了线程1,线程1直接将其累加到67,那么66就被忽略了。
image.png

1.2.2号码重复出现

线程1执行index+1, 然后CPU执行权落入线程2手里, 由于线程1并没有给index赋予计算后的结果393, 因此线程2执行index+1的结果仍然是393, 所以会出现重复号码的情况。
image.png

1.2.3号码超过了最大值

下面来分析一下号码超过最大值的情况, 当index=499的时候, 线程1和线程2都看到条件满足, 线程2短暂停顿, 线程1将index增加到了500, 线程2恢复运行后又将500增加到了501,此时就出现了超过最大值的情况。
我们虽然使用了时序图的方式对数据同步问题进行了分析,但是这样的解释还是不够严谨, 本书的第三部分将会讲解Java的内存模型以及CPU缓存等知识, 到时候会更加清晰和深入地讲解数据不一致的问题。

image.png

2.初识synchronized关键字

2.1什么是synchronized

image.png

上述解释的意思是: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() 。示例代码如下:
    1. public synchronized void sync() {}
    2. public synchronized static void Sync(){}

    2.2.2.同步代码块

    同步代码块的语法示例如下: ```java private final Object MUTEX = new Object();

public void sync() { synchronized (MUTEX) {

  1. }

}

  1. <a name="LhPx9"></a>
  2. ## 3.深入理解synchronized关键字
  3. <a name="cIflj"></a>
  4. ### 3.1 线程堆栈分析
  5. synchronized关键字提供了一种互斥机制, 也就是说在同一时刻, 只能有一个线程访问同步资源, 很多资料、书籍将synchronized(mutex) 称为锁, 其实这种说法是不严谨的,准确地讲应该是某线程获取了与mutex关联的monitor锁(当然写程序的时候知道它想要表达的语义即可),下面我们来看一个简单的例子对其进行说明:
  6. ```java
  7. public class TestHtr {
  8. private final static Object MUTEX = new Object();
  9. public void accessResource() {
  10. synchronized (MUTEX) {
  11. try {
  12. TimeUnit.MINUTES.sleep(10);
  13. } catch (InterruptedException e) {
  14. e.printStackTrace();
  15. }
  16. }
  17. }
  18. public static void main(String[] args) {
  19. final TestHtr testHtr = new TestHtr();
  20. for(int i = 0; i < 5; i++ ) {
  21. new Thread(testHtr::accessResource).start();
  22. }
  23. }
  24. }

image.png

使用j stack命令打印进程的线程堆栈信息, 选取其中几处关键的地方对其进行分析。Thread-0持有monitor<0x00000000d7333fd8>的锁并且处于休眠状态中, 那么其他线程将会无法进入access Resource方法, 如图所示。
Thread-1线程进入BLOCKED状态并且等待着获取monitor<0x00000000d7333fd8>的
锁, 其他的几个线程同样也是BLOCKED状态, 如图所示。
image.png

3.2JVM指令分析

使用JDK命令java p对Mutex class进行反汇编, 输出了大量的JVM指令, 在这些指令中, 你将发现monitor enter和monitor exit是成对出现的(有些时候会出现一个monitorenter多个monitor exit, 但是每一个monitor exit之前必有对应的monitor enter, 这是肯定的),运行下面的命令:

  1. D:\soft\jdk\bin>javap -c D:\Idea_project\sqlquery\target\test-classes\com\cmb\tool\sqlquery\TestHtr.class
  2. Compiled from "TestHtr.java"
  3. public class com.cmb.tool.sqlquery.TestHtr {
  4. public com.cmb.tool.sqlquery.TestHtr();
  5. Code:
  6. 0: aload_0
  7. 1: invokespecial #1 // Method java/lang/Object."<init>":()V
  8. 4: return
  9. public void accessResource();
  10. Code:
  11. 0: getstatic #2 // Field MUTEX:Ljava/lang/Object;
  12. 3: dup
  13. 4: astore_1
  14. 5: monitorenter
  15. 6: getstatic #3 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
  16. 9: ldc2_w #4 // long 10l
  17. 12: invokevirtual #6 // Method java/util/concurrent/TimeUnit.sleep:(J)V
  18. 15: goto 23
  19. 18: astore_2
  20. 19: aload_2
  21. 20: invokevirtual #8 // Method java/lang/InterruptedException.printStackTrace:()V
  22. 23: aload_1
  23. 24: monitorexit
  24. 25: goto 33
  25. 28: astore_3
  26. 29: aload_1
  27. 30: monitorexit
  28. 31: aload_3
  29. 32: athrow
  30. 33: return
  31. Exception table:
  32. from to target type
  33. 6 15 18 Class java/lang/InterruptedException
  34. 6 25 28 any
  35. 28 31 28 any
  36. public static void main(java.lang.String[]);
  37. Code:
  38. 0: new #9 // class com/cmb/tool/sqlquery/TestHtr
  39. 3: dup
  40. 4: invokespecial #10 // Method "<init>":()V
  41. 7: astore_1
  42. 8: iconst_0
  43. 9: istore_2
  44. 10: iload_2
  45. 11: iconst_5
  46. 12: if_icmpge 42
  47. 15: new #11 // class java/lang/Thread
  48. 18: dup
  49. 19: aload_1
  50. 20: dup
  51. 21: invokevirtual #12 // Method java/lang/Object.getClass:()Ljava/lang/Class;
  52. 24: pop
  53. 25: invokedynamic #13, 0 // InvokeDynamic #0:run:(Lcom/cmb/tool/sqlquery/TestHtr;)Ljava/lang/Runnable;
  54. 30: invokespecial #14 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V
  55. 33: invokevirtual #15 // Method java/lang/Thread.start:()V
  56. 36: iinc 2, 1
  57. 39: goto 10
  58. 42: return
  59. static {};
  60. Code:
  61. 0: new #16 // class java/lang/Object
  62. 3: dup
  63. 4: invokespecial #1 // Method java/lang/Object."<init>":()V
  64. 7: putstatic #2 // Field MUTEX:Ljava/lang/Object;
  65. 10: return
  66. }

选取其中的片段, 进行重点分析。①获取到MUTEX引用, 然后②执行monitor enterJVM指令, 休眠结束之后goto至③monitor exit的位置(a store存储引用至本地变量表; a load从本地变量表加载引用; get static从class中获得静态属性) :

  1. public void accessResource();
  2. Code:
  3. 0: getstatic #2 // 1)获取Mutex // Field MUTEX:Ljava/lang/Object;
  4. 3: dup
  5. 4: astore_1
  6. 5: monitorenter // 2)执行monitor enter
  7. 6: getstatic #3 // Field java/util/concurrent/TimeUnit.MINUTES:Ljava/util/concurrent/TimeUnit;
  8. 9: ldc2_w #4 // long 10l
  9. 12: invokevirtual #6 // Method java/util/concurrent/TimeUnit.sleep:(J)V
  10. 15: goto 23 // 3)goto 到23行
  11. 18: astore_2
  12. 19: aload_2
  13. 20: invokevirtual #8 // Method java/lang/InterruptedException.printStackTrace:()V
  14. 23: aload_1 // 4)
  15. 24: monitorexit 5 执行monitorexit
  16. 25: goto 33
  17. 28: astore_3
  18. 29: aload_1
  19. 30: monitorexit
  20. 31: aload_3
  21. 32: athrow
  22. 33: return
  23. Exception table:
  24. from to target type
  25. 6 15 18 Class java/lang/InterruptedException
  26. 6 25 28 any
  27. 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关联的对象不能为空

  1. private final Object mutex = null;
  2. public void syncmethod() {
  3. synchronized (mutex) {
  4. }
  5. }

Mutex为null, 很多人还是会犯这么简单的错误, 每一个对象和一个monitor关联, 对象都为null了,monitor肯定无从谈起。

3.3.2.synchronized作用域太大

由于synchronized关键字存在排他性, 也就是说所有的线程必须串行地经过synchronized保护的共享区域, 如果synchronized作用域越大, 则代表着其效率越低, 甚至还会丧失并发的优势,示例代码如下:

  1. public static class Task implements Runnable {
  2. @Override
  3. public synchronized void run() {
  4. }
  5. }

上面的代码对整个线程的执行逻辑单元都进行了synchronized同步, 从而丧失了并发的能力, synchronized关键字应该尽可能地只作用于共享资源(数据) 的读写作用域。

3.3.3.不同的monitor企图锁相同的方法

  1. public static class Task implements Runnable {
  2. private final Object MUTEX = new Object(); // 独立的变量
  3. @Override
  4. public void run() {
  5. // ...
  6. synchronized (MUTEX) {
  7. //...
  8. }
  9. }
  10. }
  11. public static void main(String[] args) {
  12. for(int i = 0; i < 5; i++) {
  13. new Thread(Task::new).start();
  14. }
  15. }

上面的代码构造了五个线程, 同时也构造了五个Runnable实例, Runnable作为线程逻辑执行单元传递给Thread, 然后你将发现, synchronized根本互斥不了与之对应的作用域,线程之间进行monitor lock的争抢只能发生在与monitor关联的同一个引用上, 上面的代码每一个线程争抢的monitor关联引用都是彼此独立的, 因此不可能起到互斥的作用。

3.3.4 多个锁的交叉导致死锁

多个锁的交叉很容易引起线程出现死锁的情况,程序并没有任何错误输出,但就是不工作,如下面的代码所示(本书的4.5节中会分析死锁的原因以及教大家如何对其进行诊断):

  1. private final Object MUTEX_READ = new Object();
  2. private final Object MUTEX_WRITE = new Object();
  3. public void read() {
  4. synchronized (MUTEX_READ) {
  5. synchronized (MUTEX_WRITE) {
  6. }
  7. }
  8. }
  9. public void write() {
  10. synchronized (MUTEX_WRITE) {
  11. synchronized (MUTEX_READ) {
  12. }
  13. }
  14. }

4.ThisMonitor和ClassMonitor的详细介绍

4.1 thismonitor

在下面的代码This Monitor中, 两个方法method 1和method 2都被synchronized关键字修饰, 启动了两个线程分别访问method 1和method 2, 在开始运行之前请读者思考一个问题:synchronized关键字修饰了同一个实例对象的两个不同方法, 那么与之对应的monitor是什么?两个monitor是否一致呢?
image.png

笔者将重点的地方用红色的框标识了出来,T1线程获取了<0x00000000d7334700>monitor的lock并且处于休眠状态, 而T 2线程企图获取<0x00000000d7334700>monitor的lock时陷入了BLOCKED状态, 可见使用synchronized关键字同步类的不同实例方法, 争抢的是同一个monitor的lock, 而与之关联的引用则是This Monitor的实例引用, 为了证实我们的推论,将上面的代码稍作修改,如下所示:
image.png
其中, method1保持方法同步的方式, method2则采用了同步代码块的方式, 并且使用的是this的monitor, 运行修改后的代码将会发现效果完全一样, 在JDK官方文档中也有这样的描述(见https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html):

  1. Locks In Synchronized Methods
  2. 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.
  3. 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

  1. public class ClassMonitor {
  2. public static synchronized void method1() {
  3. System.out.println(Thread.currentThread().getName() + " enter to method1");
  4. try {
  5. TimeUnit.MINUTES.sleep(10);
  6. } catch (InterruptedException e) {
  7. e.printStackTrace();
  8. }
  9. }
  10. public static synchronized void method2() {
  11. System.out.println(Thread.currentThread().getName() + " enter to method2");
  12. try {
  13. TimeUnit.MINUTES.sleep(10);
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. }
  18. public static void main(String[] args) {
  19. new Thread(ClassMonitor::method1,"T1").start();
  20. new Thread(ClassMonitor::method2,"T2").start();
  21. }
  22. }

image.png

5.程序死锁的原因以及如何诊断

5.1程序死锁

5.1.1 交叉锁可导致程序出现死锁

5.1.2 内存不足

5.1.3 一问一答式的数据交换

5.1.4 数据库锁

5.1.5 文件锁

5.1.6 死循环引起的死锁

5.2死锁案例

举一例交叉死锁

  1. public class DeadLock {
  2. private final Object MUTEX_READ = new Object();
  3. private final Object MUTEX_WRITE = new Object();
  4. public void read() {
  5. synchronized (MUTEX_READ) {
  6. System.out.println(Thread.currentThread().getName() + " get Read lock");
  7. synchronized (MUTEX_WRITE) {
  8. System.out.println(Thread.currentThread().getName() + " get Write lock");
  9. }
  10. System.out.println(Thread.currentThread().getName() + " release write lock");
  11. }
  12. System.out.println(Thread.currentThread().getName() + " release read lock");
  13. }
  14. public void write() {
  15. synchronized (MUTEX_WRITE) {
  16. System.out.println(Thread.currentThread().getName() + " get Write lock");
  17. synchronized (MUTEX_READ) {
  18. System.out.println(Thread.currentThread().getName() + " get Read lock");
  19. }
  20. System.out.println(Thread.currentThread().getName() + " release Read lock");
  21. }
  22. System.out.println(Thread.currentThread().getName() + " release Write lock");
  23. }
  24. public static void main(String[] args) {
  25. final DeadLock deadLock = new DeadLock();
  26. new Thread(
  27. () -> {
  28. while(true) {
  29. deadLock.read();
  30. }
  31. }
  32. , "READ-THREAD").start();
  33. new Thread(
  34. () -> {
  35. while(true) {
  36. deadLock.write();
  37. }
  38. }
  39. , "WRITE-THREAD").start();
  40. }
  41. }