深入理解 Java 并发编程:线程同步与 Synchronized 关键字

小七学习网,助您升职加薪,遇问题可联系:客服微信【1601371900】 备注:来自网站

掌握 Java 并发编程是深入理解 Java 的必经之路。市面上许多高性能的开源框架中都用到了 Java 并发编程技术。本文从 Java 并发相关的知识点逐一切入,循序渐进,最终将 Java 并发相关…

掌握 Java 并发编程是深入理解 Java 的必经之路。市面上许多高性能的开源框架中都用到了 Java 并发编程技术。本文从 Java 并发相关的知识点逐一切入,循序渐进,最终将 Java 并发相关的知识完美的呈现在读者面前。

在本文中,会讲到如下内容:

  • Object 类的 wait/notify/notifyAll 方法原理
  • Object 类的同步方法使用案例
  • Synchronized 的概念与使用案例
  • Synchronized 的底层原理与性能分析
  • 对象的同步监视器(Monitor)的概念
  • 对象的同步监视器(Monitor)的底层代码分析

适合人群:对 Java 并发感兴趣,工作、面试中需要用到 Java 并发编程知识的技术人员。



wait/notify/notifyAll

谈到并发控制,就不得不提 Object 类提供的线程间通信方式。Object 类提供了三个用于线程间通信的方法:wait/notify/notifyAll。所有的 Java 对象都自然继承这些方法。下面对这些方法一一讲解。

wait

调用对象的 wait() 方法会使当前线程等待,直到另一个线程调用此对象的 notify() 方法或 notifyAll() 方法。

调用 wait 方法时,当前线程必须先拥有此对象的监视器(monitor),否则报错。调用 wait 方法后,线程释放此监视器的所有权并等待,直到另一个线程通过调用 notify 方法或 notifyAll 方法唤醒在此对象监视器上等待的线程。被唤醒的线程与其它线程公平竞争该对象的锁,直到它可以重新获得对象监视器的所有权后才恢复执行。

划重点:对象的 wait() 方法只能被此对象监视器(monitor)的所有者的线程调用。也就是说当调用 wait 方法时,首先要确保调用 wait 方法的线程已经获得了对象的锁。

中断和虚假唤醒是可能的,因此应该向下面示例这样,始终在循环中调用 wait 方法,而不是使用 if 判断。

synchronized (obj) {     while (<condition does not hold>)           obj.wait();           ... // Perform action appropriate to condition } 

wait 方法有个重载的版本,即 wait(long timeout),timeout 为等待的最长时间(以毫秒为单位)。

调用 wait() 方法等价于调用 wait(0) 方法。换句话说,wait() 方法的行为和调用 wait(0) 一样。

调用 wait(timeout) 方法会导致当前线程等待,直到另一个线程调用此对象的 notify() 方法或 notifyAll() 方法,或者指定的时间量已经过去。和 wait 方法一样,调用 wait(timeout) 方法时当前线程必须拥有此对象的监视器。

wait(timeout) 方法使当前线程(称之为 T)将自己置于此对象的等待集(wait set)中,然后放弃对此对象的所有同步声明。 线程 T 出于线程调度目的而被禁用并处于休眠状态,直到发生以下四种情况之一:

  • 某个其它线程调用了此对象的 notify 方法,而线程 T 恰好被任意选择为要唤醒的线程。
  • 其他一些线程调用了此对象 notifyAll 方法。
  • 其他一些线程中断了线程 T。
  • 等待已经过了指定的时间。如果 timeout 为零,则不考虑等待时间,线程只是等待直到收到通知。

然后,线程 T 从该对象的等待集中移除,并重新启用线程调度。 然后它以正常的方式与其他线程竞争在对象上同步的权利; 一旦它获得了对象的控制权,它对对象的所有同步声明都将恢复到之前的状态。也就是说,恢复到调用 wait 方法时的情况。 然后线程 T 从 wait 方法的调用中返回。 因此,从 wait 方法返回时,对象和线程 T 的同步状态与调用 wait 方法时完全相同。

线程也可以在没有被通知、中断或超时的情况下唤醒,即所谓的虚假唤醒。 虽然这在实践中很少发生,但应用程序必须再次判断导致线程被唤醒的条件来防止虚假唤醒,如果条件不满足则继续等待。 换句话说,等待应该总是在循环中发生,就像下面的例子一样。

这里使用 while 循环判断而不是 if 单次判断,保证满足条件才会继续执行。

synchronized (obj) {      while (<condition does not hold>)           obj.wait(timeout);           ... // Perform action appropriate to condition } 

如果调用对象的 wait 方法时当前线程没有获得对象的 monitor,则会抛出异常,例如下面这个例子。

public class Test1 {     public static void main(String[] args) throws InterruptedException {         Object o = new Object();         o.wait();     } } 

运行时异常:

Exception in thread \"main\" java.lang.IllegalMonitorStateException     at java.lang.Object.wait(Native Method)     at java.lang.Object.wait(Object.java:502)     at com.bigbird.conc.Test1.main(Test1.java:6) 

当前线程必须拥有该对象的监视器(monitor),才能调用该对象的 wait() 方法。

获得某个对象的同步监视器可以使用 synchronized 关键字,一旦进入到 synchronized 块内,该线程必然获取到了对象的监视器。

public class Test1 {     public static void main(String[] args) throws InterruptedException {         Object o = new Object();         synchronized (o){             o.wait();         }     } } 

Tip:sleep() 方法和 wait() 方法的区别。

在调用对象的 wait() 方法时,线程必须持有被调用对象的锁(monitor);调用 wait() 方法后,线程就会释放掉该对象的锁。而在调用线程的 sleep() 方法时,不需要获得对象的 monitor,也不会释放任何该线程持有的对象的锁(monitor)。

notify

调用对象的 notify() 方法会唤醒一个正在等待这个对象的锁的线程。如果有多个线程都在等待这个对象的锁,则会任意选择其中一个将其唤醒。正如前文所述,一个线程可以通过调用该对象的 wait() 方法来等待该对象的锁。被唤醒的线程不会继续执行,而是和其它线程一样共同竞争该对象的锁。获得对象的锁之后才会继续执行。

和 wait() 方法一样,调用对象的 notify() 方法的线程也必须先持有该对象的锁(monitor)。

获取对象的锁(monitor)的方式有下列几种:

  • 调用该对象的一个用 synchronized 修饰的实例方法
  • 通过执行一个由 synchronized 修饰的代码块
  • 通过调用类的一个 synchronized static 修饰的类方法来获得 Class 类型对象的锁

一个对象的锁(monitor)同一时刻只能被一个线程拥有。

notifyAll

调用对象的 notifyAll() 方法会唤醒等待此对象锁(monitor)的所有线程,即该对象等待集合(wait set)中的所有线程被唤醒。

前文已经介绍了,一个线程可以通过调用某个对象的 wait() 方法使其等待该对象的锁,即进入该对象的等待集合。

被唤醒的线程不会继续执行直到其获得该对象的锁。被唤醒的线程和其它线程相比,在对象锁的竞争上没有什么劣势或者优势,它们公平竞争对象的同步锁(monitor),都有可能成为该对象锁的下一个持有者。

和 wait()、notify() 方法一样,一个对象的 notifyAll() 方法只能被持有这个对象的 monitor 的线程所调用。

获取对象的锁(monitor)的 3 种方式上面已经介绍,无疑离不开 synchronized 关键字。

案例

结合一个案例讲解一下上述几个方法的应用。

需求描述:创建一个计数器对象,提供加一和减一方法。创建多个线程调用该对象的加一、减一方法。

并输出这样一个结果序列:10101010…

代码如下:

//计数器对象 public class MyCounter {     private int counter;     public synchronized void increase() throws InterruptedException {         //此处不能用 if 判断,必须放到循环中判断         while (counter != 0) {             wait();         }         counter++;         System.out.println(counter);         notify();     }     public synchronized void decrease() throws InterruptedException {         while (counter != 1) {             wait();         }         counter--;         System.out.println(counter);         notify();     } } 
//加 1 线程 public class IncreaseThread extends Thread {     private MyCounter counter;     public IncreaseThread(MyCounter counter) {         this.counter = counter;     }     @Override     public void run() {         for (int i = 0; i < 20; i++) {             try {                 Thread.sleep((long) (Math.random() * 1000));                 counter.increase();             } catch (Exception e) {                 e.printStackTrace();             }         }     } } 
//减 1 线程 public class DecreaseThread extends Thread {     private MyCounter counter;     public DecreaseThread(MyCounter counter) {         this.counter = counter;     }     @Override     public void run() {         for (int i = 0; i < 20; i++) {             try {                 Thread.sleep((long) (Math.random() * 1000));                 counter.decrease();             } catch (Exception e) {                 e.printStackTrace();             }         }     } } 

测试

public class MainApp {     public static void main(String[] args) {         MyCounter myCounter = new MyCounter();         IncreaseThread increaseThread1 = new IncreaseThread(myCounter);         IncreaseThread increaseThread2 = new IncreaseThread(myCounter);         DecreaseThread decreaseThread1 = new DecreaseThread(myCounter);         DecreaseThread decreaseThread2 = new DecreaseThread(myCounter);         increaseThread1.start();         increaseThread2.start();         decreaseThread1.start();         decreaseThread2.start();     } } 

控制台输出 1010101010....,符合预期。也可以将 while 循环判断改成 if 判断,看看结果是否满足预期。

synchronized

synchronized 关键字是 Java 语言层面提供的并发控制工具,且在 JDK 1.5 之前只能通过 synchronized 关键字实现同步。

从 JDK 1.5 开始,增加一些 API 层面的可用于同步控制的 Lock 工具,后面会详细介绍。

synchronized 的使用

synchronized 可以修饰一个静态方法、普通方法、代码块,例如下列案例。

class MyClass {     public synchronized static void method1() {         //do something     }     public synchronized static void method11() {         //do something     }     public synchronized void method2() {         //do something     }     public synchronized void method22() {         //do something     }     public void method3() {         //do something         synchronized (this) {             //do something         }     }     public void method4() {         //do something         synchronized (MyClass.class) {             //do something         }     }     private Object object;     public void method5() {          //do something         synchronized (object) {             //do something         }     } } 

修饰静态方法

线程锁定的是当前类的 Class 对象。比如调用 method1 或者 method11 方法时,锁定的是 MyClass 类的 Class 对象。因此调用该类的任意一个由 synchronized 修饰的静态方法都会竞争同一把锁,即 MyClass 类的 Class 对象的 monitor。即使是在不同的 MyClass 类的实例中调用这两个静态方法,也是竞争的同一把锁。

修饰实例方法

线程锁定的是当前实例对象。比如调用 method2 或者 method22 方法时,锁定的是 MyClass 类的实例对象。调用该类的同一个实例对象的同步方法会竞争同一把锁,即 MyClass 类的实例对象的 monitor。在不同的 MyClass 类的实例中调用这两个实例方法,则竞争的不是同一把锁。

修饰代码块

修饰代码块锁定的是括号里面的对象,有三种情况。

  • 比如 method3,同步方法锁定的是实例对象。
  • 比如 method4,同步方法锁定的是类的 Class 对象。
  • 比如 method5,同步方法锁定的是任意类的实例对象。这里举例使用的是 Object 类,实际上可以使用任何类。

案例

运行下列程序,看看是先输出 method1 还是先输出 method2。

虽然 method1 在执行时休眠了 5 秒钟,但是结果总是先输出 method1,因为一个对象的所有被 synchronized 修饰的方法都必须先获得对象的锁才能执行,而对象的锁只有唯一的一把,同一时刻只能被一个线程拥有。

由于线程 thread1 先启动,因此 myClass1 对象的 method1 方法先执行,thread1 先获得对象锁。线程 thread2 调用 myClass1 对象的 method2 方法时需要等待 myClass1 的对象锁(monitor),因此后执行。如果创建了两个不同的 MyClass 对象,则结果就不一样了。

public class Test1 {     public static void main(String[] args) throws InterruptedException {         MyClass myClass1 = new MyClass();         Thread1 thread1 = new Thread1(myClass1);         Thread2 thread2 = new Thread2(myClass1);         thread1.start();         Thread.sleep(1000);         thread2.start();     } } class MyClass {     public synchronized void method1() {         try {             //休眠 5 秒钟             Thread.sleep(5000);         } catch (Exception e) {             e.printStackTrace();         }         System.out.println(\"method1\");     }     public synchronized void method2() {         System.out.println(\"method2\");     } } class Thread1 extends Thread {     private MyClass myClass;     public Thread1(MyClass myClass) {         this.myClass = myClass;     }     @Override     public void run() {         myClass.method1();     } } class Thread2 extends Thread {     private MyClass myClass;     public Thread2(MyClass myClass) {         this.myClass = myClass;     }     @Override     public void run() {         myClass.method2();     } } 

创建两个 MyCass 对象,结果先输出 method2,再输出 method1。因为两个线程使用的不同的对象锁,不存在竞争。

public class Test1 {     public static void main(String[] args) throws InterruptedException {         MyClass myClass1 = new MyClass();         MyClass myClass2 = new MyClass();         Thread1 thread1 = new Thread1(myClass1);         Thread2 thread2 = new Thread2(myClass2);         thread1.start();         Thread.sleep(1000);         thread2.start();     } } 

来看下面这个例子。

一个线程正在调用 Test1 类的实例对象的 method1 方法,并且还没执行完,另一个线程能否调用此对象的 method2 方法?

public class Test1 {     public static synchronized void method1(){     }     public synchronized void method2(){     } } 

答案是肯定的,因为两个方法锁定的对象不一样。

method1 方法锁定的是 Test1 所属的类的 Class 对象,method2 方法锁定的是 Test1 类的一个实例对象。

synchronized 的底层原理

下面我们来讲解 synchronized 的底层实现机制。分别通过 synchronized 修饰代码块、方法、静态方法的例子来说明。

synchronized 修饰代码块
public class Test1 {     private Object object = new Object();     public void method1() {         synchronized (object) {             System.out.println(\"hello\");         }     } } 

先编译上述 Test1 类的 Java 文件,再通过 javap -c 的方式反编译其对应的 class 文件。

透过反编译的字节码文件,我们可以看到 synchronized 修饰的代码部分被 monitorenter 和 monitorexit 指令包裹。

由此可见 synchronized 修饰代码块在字节码层面是通过 monitorenter 和 monitorexit 指令来实现锁的获取与释放。

后面的代码还有一个 monitorexit 指令。第二个 monitorexit 指令保证了程序在正常执行完成、发生异常退出时都能释放锁。

F:\\myproject\\nettydemo\\target\\classes>javap -c com.bigbird.conc.Test1 Compiled from \"Test1.java\" public class com.bigbird.conc.Test1 {   public com.bigbird.conc.Test1();     Code:        0: aload_0        1: invokespecial #1                  // Method java/lang/Object.\"<init>\":()V        4: aload_0        5: new           #2                  // class java/lang/Object        8: dup        9: invokespecial #1                  // Method java/lang/Object.\"<init>\":()V       12: putfield      #3                  // Field object:Ljava/lang/Object;       15: return   public void method1();     Code:        0: aload_0        1: getfield      #3                  // Field object:Ljava/lang/Object;        4: dup        5: astore_1        6: monitorenter        7: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;       10: ldc           #5                  // String hello       12: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V       15: aload_1       16: monitorexit       17: goto          25       20: astore_2       21: aload_1       22: monitorexit       23: aload_2       24: athrow       25: return     Exception table:        from    to  target type            7    17    20   any           20    23    20   any } 
synchronized 修饰方法
public class Test1 {     public synchronized void method1() {         System.out.println(\"hello\");     } } 

使用 javap -v 反编译上面 Test1 类的 class 文件结果如下。

F:\\myproject\\nettydemo\\target\\classes>javap -v com.bigbird.conc.Test1 Classfile /F:/myproject/nettydemo/target/classes/com/bigbird/conc/Test1.class   Last modified 2021-9-5; size 495 bytes   MD5 checksum ef0c7ab2d1a721a30fbd55aad5e9a5b5   Compiled from \"Test1.java\" public class com.bigbird.conc.Test1   minor version: 0   major version: 52   flags: ACC_PUBLIC, ACC_SUPER Constant pool:    #1 = Methodref          #6.#17         // java/lang/Object.\"<init>\":()V    #2 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;    #3 = String             #20            // hello    #4 = Methodref          #21.#22        // java/io/PrintStream.println:(Ljava/lang/String;)V    #5 = Class              #23            // com/bigbird/conc/Test1    #6 = Class              #24            // java/lang/Object    #7 = Utf8               <init>    #8 = Utf8               ()V    #9 = Utf8               Code   #10 = Utf8               LineNumberTable   #11 = Utf8               LocalVariableTable   #12 = Utf8               this   #13 = Utf8               Lcom/bigbird/conc/Test1;   #14 = Utf8               method1   #15 = Utf8               SourceFile   #16 = Utf8               Test1.java   #17 = NameAndType        #7:#8          // \"<init>\":()V   #18 = Class              #25            // java/lang/System   #19 = NameAndType        #26:#27        // out:Ljava/io/PrintStream;   #20 = Utf8               hello   #21 = Class              #28            // java/io/PrintStream   #22 = NameAndType        #29:#30        // println:(Ljava/lang/String;)V   #23 = Utf8               com/bigbird/conc/Test1   #24 = Utf8               java/lang/Object   #25 = Utf8               java/lang/System   #26 = Utf8               out   #27 = Utf8               Ljava/io/PrintStream;   #28 = Utf8               java/io/PrintStream   #29 = Utf8               println   #30 = Utf8               (Ljava/lang/String;)V {   public com.bigbird.conc.Test1();     descriptor: ()V     flags: ACC_PUBLIC     Code:       stack=1, locals=1, args_size=1          0: aload_0          1: invokespecial #1                  // Method java/lang/Object.\"<init>\":()V          4: return       LineNumberTable:         line 3: 0       LocalVariableTable:         Start  Length  Slot  Name   Signature             0       5     0  this   Lcom/bigbird/conc/Test1;   public synchronized void method1();     descriptor: ()V     flags: ACC_PUBLIC, ACC_SYNCHRONIZED     Code:       stack=2, locals=1, args_size=1          0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;          3: ldc           #3                  // String hello          5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V          8: return       LineNumberTable:         line 6: 0         line 7: 8       LocalVariableTable:         Start  Length  Slot  Name   Signature             0       9     0  this   Lcom/bigbird/conc/Test1; } SourceFile: \"Test1.java\" 

可以看到 synchronized 修饰的方法会增加一个 ACC_SYNCHRONIZED 标志位。线程执行时通过该标志位知晓需要获取对象的锁。

当方法被调用时,调用指令会检查该方法是否拥有 ACC_SYNCHRONIZED 标志。如果有,则执行线程就会先持有方法所在对象的 Monitor 对象,然后才去执行方法体。在该方法执行期间,其它线程不能再获取到这个 Monitor 对象。当线程执行完该方法后,会释放其持有的 Monitor 对象。

synchronized 修饰静态方法
public class Test1 {     public static synchronized void method1() {         System.out.println(\"hello\");     } } 

使用 javap -v 反编译上面 Test1 类的 class 文件。

可以看到和修饰普通方法一样,也是通过 ACC_SYNCHRONIZED 标志位实现方法同步。只不过执行现场获取的是方法所属类的 Class 对象的锁。因为标志位多了一个 ACC_STATIC 标识。静态方法本质上是属于类的。

F:\\myproject\\nettydemo\\target\\classes>javap -v com.bigbird.conc.Test1 Classfile /F:/myproject/nettydemo/target/classes/com/bigbird/conc/Test1.class   Last modified 2021-9-5; size 477 bytes   MD5 checksum 88a5406f90d2fe61c625cd4397bdf678   Compiled from \"Test1.java\" public class com.bigbird.conc.Test1   minor version: 0   major version: 52   flags: ACC_PUBLIC, ACC_SUPER Constant pool:    #1 = Methodref          #6.#17         // java/lang/Object.\"<init>\":()V    #2 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;    #3 = String             #20            // hello    #4 = Methodref          #21.#22        // java/io/PrintStream.println:(Ljava/lang/String;)V    #5 = Class              #23            // com/bigbird/conc/Test1    #6 = Class              #24            // java/lang/Object    #7 = Utf8               <init>    #8 = Utf8               ()V    #9 = Utf8               Code   #10 = Utf8               LineNumberTable   #11 = Utf8               LocalVariableTable   #12 = Utf8               this   #13 = Utf8               Lcom/bigbird/conc/Test1;   #14 = Utf8               method1   #15 = Utf8               SourceFile   #16 = Utf8               Test1.java   #17 = NameAndType        #7:#8          // \"<init>\":()V   #18 = Class              #25            // java/lang/System   #19 = NameAndType        #26:#27        // out:Ljava/io/PrintStream;   #20 = Utf8               hello   #21 = Class              #28            // java/io/PrintStream   #22 = NameAndType        #29:#30        // println:(Ljava/lang/String;)V   #23 = Utf8               com/bigbird/conc/Test1   #24 = Utf8               java/lang/Object   #25 = Utf8               java/lang/System   #26 = Utf8               out   #27 = Utf8               Ljava/io/PrintStream;   #28 = Utf8               java/io/PrintStream   #29 = Utf8               println   #30 = Utf8               (Ljava/lang/String;)V {   public com.bigbird.conc.Test1();     descriptor: ()V     flags: ACC_PUBLIC     Code:       stack=1, locals=1, args_size=1          0: aload_0          1: invokespecial #1                  // Method java/lang/Object.\"<init>\":()V          4: return       LineNumberTable:         line 3: 0       LocalVariableTable:         Start  Length  Slot  Name   Signature             0       5     0  this   Lcom/bigbird/conc/Test1;   public static synchronized void method1();     descriptor: ()V     flags: ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED     Code:       stack=2, locals=0, args_size=0          0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;          3: ldc           #3                  // String hello          5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V          8: return       LineNumberTable:         line 6: 0         line 7: 8 } SourceFile: \"Test1.java\" 
小结

当使用 synchronized 修饰代码块时,字节码层面是通过 monitorenter 和 monitorexit 指令来实现锁的获取与释放。当线程执行到 monitorenter 指令后,线程将会持有对象的 monitor。执行完 monitorexit 指令后,线程将释放对象的 monitor。

当使用 synchronized 修饰方法时,字节码层面没有 monitorenter 和 monitorexit,而是通过 ACC_SYNCHRONIZED 标志位来标识一个方法是否需要同步。如果执行方法对应的指令时,发现该方法有 ACC_SYNCHRONIZED 标识,那么执行线程会先获得该方法所在对象的 monitor,然后再执行方法体。方法执行期间其它线程无法获得该对象的 monitor。当线程执行完成后会释放该对象的 monitor。

对象的同步监视器(monitor)

对象的 monitor 也叫对象的同步监视器,或者对象的锁。有的资料将 monitor 翻译为管程,其实都是指的同一个东西。

前文中我们提到,JVM 中的同步机制(synchronized)是基于进入和退出监视器对象(monitor)来实现的。那么 Monitor 到底是个什么东西呢?

Monitor 底层是由 C++ 实现的,在 C++ 代码中,Monitor 其实就是一个 C++ 的对象:ObjectMonitor。每个 Java 对象实例都有一个对应的 Monitor 对象,Monitor 对象和 Java 对象一起被创建和销毁。源码可以参见 OpenJDK。

我们知道,Java 本身是开源的,JDK 中大多数源码都可以获取到。但是有部分代码不是公开的,比如 sun 开头的包下的代码。在使用 IDEA 工具时,会自动反编译,因此通常能看到一些反编译之后的代码。那么有没有什么方式能看到完整的 JDK 源代码呢?

答案就是 OpenJDK。

monitor 底层代码

在 OpenJDK 的官网中,我们可以找到 monitor 对应的 C++ 代码:

http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/e2ac513ec7b3/src/share/vm/runtime/objectMonitor.hpp

在这里插入图片描述
在这里插入图片描述

从底层的 C++ 代码(此处省略完整代码),我们可以看到当多个 Java 线程同时访问一段同步代码时,这些线程会被放置到一个 EntryList 集合中,处于阻塞状态的线程都会被放置到该集合中。Monitor 的最底层是基于操作系统的互斥锁(mutex lock)来实现互斥的。当线程获得对象的 monitor 时,线程获得 mutex lock 成功,并持有该互斥锁,此时其它线程无法获取到同一把锁,直到前一个线程释放掉。

如果线程执行了对象的 wait 方法,那么该线程就会释放其持有的 mutex lock。该线程就会进入到该对象的等待集合 WaitSet 中,等待被其它线程调用该对象的 notify/notifyAll 方法唤醒。线程被唤醒后会继续竞争 Monitor 对象,如果竞争不到,则会进入到 EntryList 集合中。如果当前持有 Monitor 的线程顺利执行完同步方法,也会释放其持有的 mutex lock。

  • WaitSet:线程调用了对象的 wait 方法之后所进入的集合
  • EntryList:等待对象锁的线程所进入的集合

上述两个集合本质上都是一个双向链表结构。

wait/notify 底层代码

wait/notify/notifyAll 方法对应的 JDK 底层 C++ 代码参见:

http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/e2ac513ec7b3/src/share/vm/runtime/objectMonitor.cpp

在这里插入图片描述

synchronized 性能分析

问题

由于 Monitor 对象底层是依赖操作系统的互斥锁实现的,所以就会存在用户态和内核态之间的切换,导致增加性能开销。

互斥锁的标记可以保证在任意时刻,只能有一个线程访问该对象。

处于 WaitSet 和 EntryList 中的线程都是阻塞状态,阻塞操作由 OS 来完成。Linux 下是通过 pthread_mutex_lock 函数实现的。一旦线程进入到阻塞状态,就会导致用户态到内核态的转换。线程被唤醒后又会从内核态切换回用户态。这种上下文切换的成本是较高的,严重影响锁的性能,因此要尽量避免。

解决

有一种解决思路:自旋(spin-lock),即让 CPU 空转。用轻量级的自旋锁代替重量级的阻塞锁

自旋是基于这样一个事实:当发生对 Monitor 对象的争用时,通常 Monitor 的持有者能够在很短的时间内释放掉锁。

那么那些竞争 Monitor 的线程就可以不立即阻塞,而是稍微等待一下(即所谓的自旋),在 Monitor 的持有者释放锁时,竞争的线程可能立即获得锁,从而避免进入阻塞状态。当 Monitor 的持有者线程运行时间超过了临界值后,争用线程依然没有获得锁,这时争用线程就会停止自旋而进入到阻塞状态。

简而言之,就是先自旋,自旋一定时间后还没获得锁就阻塞,从总体上降低系统阻塞的可能性。这对于短时间持有锁(执行时间很短)的线程来说,有极大的性能提升。当然,只有在多核 CPU 上自旋才有意义,因为在单核 CPU 上已锁定了唯一的一个核。

互斥锁

Linux 操作系统一般支持下列几种互斥锁。

  • PTHREAD_MUTEX_TIMED_NP:默认值,即普通锁。当一个线程对对象加锁后,其余请求锁的线程将会放到一个等待队列中,并在锁释放后按照优先级获取到锁。
  • PTHREAD_MUTEX_RECURSIVE_NP:嵌套锁,类似 Java 中的可重入锁。允许一个线程对同一个锁获取多次,并通过 unlock 操作释放锁。如果是不同的线程请求,则在加锁解锁时需要重新竞争。
  • PTHREAD_MUTEX_ERRORCHECK_NP:检错锁,一个线程请求同一个锁获则返回 EDEADLK。否则同第一种锁。
  • PTHREAD_MUTEX_ADAPTIVE_NP:适应锁,最简单的锁,仅仅是等待解锁后重新竞争。
小结

在 JDK 1.5 之前,只能通过 synchronized 关键字来实现线程同步,synchronized 关键字能够保证数据的原子性操作。

synchronized 关键字可以看作是 JVM 提供的一种内置锁,底层依赖操作系统,锁的获取与释放是由 JVM 隐式实现的。

在 Java 代码中,我们只要用 synchronized 这个关键字,就能实现线程的同步,而不用关心底层的 JVM 实现。

synchronized 最底层是基于操作系统的 Mutex Lock 实现的,Mutex Lock 是操作系统内核提供的一种互斥锁。每次加锁、释放锁都会进行系统调用,导致内核态和用户态的切换,增加了系统开销。特别是在并发量非常大,锁的竞争比较激烈时,synchronized 锁的性能可能会非常差。

相比之下,从 JDK 1.5 开始,Java 的并发包引入了 Lock 锁,Lock 锁是基于 Java 代码实现的,锁的申请和释放都是通过 Java API 层面的代码来控制的,是由代码开发者手动实现加锁、解锁,非常灵活。

小七学习网,助您升职加薪,遇问题可联系:客服微信【1601371900】 备注:来自网站

免责声明: 1、本站信息来自网络,版权争议与本站无关 2、本站所有主题由该帖子作者发表,该帖子作者与本站享有帖子相关版权 3、其他单位或个人使用、转载或引用本文时必须同时征得该帖子作者和本站的同意 4、本帖部分内容转载自其它媒体,但并不代表本站赞同其观点和对其真实性负责 5、用户所发布的一切软件的解密分析文章仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。 6、您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。 7、请支持正版软件、得到更好的正版服务。 8、如有侵权请立即告知本站(邮箱:1099252741@qq.com,备用微信:1099252741),本站将及时予与删除 9、本站所发布的一切破解补丁、注册机和注册信息及软件的解密分析文章和视频仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。本站信息来自网络,版权争议与本站无关。您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。如有侵权请邮件与我们联系处理。