浅说Synchronized的底层实现原理

开发 前端
synchronized关键字用来保证在同一时刻只有一个线程可以执行被它修饰的变量或者代码块。

[[374555]]

 一、前言

synchronized关键字用来保证在同一时刻只有一个线程可以执行被它修饰的变量或者代码块。

这一篇中,只涉及synchronized的底层实现原理,不涉及对synchronized效率以及如何优化的讨论。

二、使用方式

(1)给静态方法加锁

  1. public class Main { 
  2.     
  3.     public static synchronized void staticSynPrint(String str) { 
  4.         System.out.println(str); 
  5.     } 
  6.  

 静态方法不属于任何一个实例,而是属于该类。不管该类被实例化多少次,静态成员只有一份。在同一时刻,不管是使用实例.staticSynPrint方式还是直接类名.staticSynPrint的方式,都会进行同步处理。

(2)给静态变量加锁

同(1),他们都是该类的静态成员。

(3)synchronized(xxx.class)

  1. public class Main { 
  2.  
  3.     public void classSynPrint(String str) { 
  4.         synchronized (Main.class) { 
  5.             System.out.println(str); 
  6.         } 
  7.     } 
  8.  

 给当前类加锁(注意是当前类,不是实例对象),会作用于该类的所有实例对象,多个线程访问Main类中的所有同步方法,都需要先进行同步处理。

(4)synchronized(this)

  1. public class Main { 
  2.  
  3.     public void thisSynPrint(String str) { 
  4.         synchronized (this) { 
  5.             System.out.println(str); 
  6.         } 
  7.     } 
  8.  

 this代表实例对象,因此现在锁住的是当前实例对象,因此多个线程访问不同实例的同步方法不需要进行同步。

(5)给实例方法加锁

  1. public class Main { 
  2.  
  3.     public synchronized void synPrint(String str) { 
  4.         System.out.println(str); 
  5.     } 
  6.  

 不同线程访问同一个实例底下的该方法,才会需要进行同步。

三、实际使用方式之一:单例模式中的双重检验锁

更多单例模式的种类可以参考我的另外一篇博文【设计模式】单例模式

  1. public class SingletonDCL { 
  2.     private volatile static SingletonDCL instance; 
  3.  
  4.     private SingletonDCL() { 
  5.     } 
  6.  
  7.     public static SingletonDCL getInstance() { 
  8.         if (instance == null) { 
  9.             synchronized (Singleton.class) { 
  10.                 if (instance == null) { 
  11.                     instance = new SingletonDCL(); 
  12.                 } 
  13.             } 
  14.         } 
  15.         return instance; 
  16.     } 
  17.  

 有几个疑问:

(1)这里为什么要检验两次null?

最初的想法,是直接利用synchronized将整个getInstance方法锁起来,但这样效率太低,考虑到实际代码更为复杂,我们应当缩小锁的范围。

在单例模式下,要的就是一个单例,new SingletonDCL()只能被执行一次。因此,现在初步考虑成以下的这种方式:

  1. public static SingletonDCL getInstance() { 
  2.        if (instance == null) { 
  3.            synchronized (Singleton.class) { 
  4.                    //一些耗时的操作 
  5.                    instance = new SingletonDCL(); 
  6.            } 
  7.        } 
  8.        return instance; 
  9.    } 

 但这样,存在一个问题。线程1判断instance为null,然后拿到锁,执行到了耗时的操作,阻塞了一会儿,还没有对instance进行实例化,instance还是为null。线程2判断instance为null,尝试去获取锁。线程1实例化instance之后,释放了锁。而线程2获取锁之后,同样进行了实例化操作。线程1和线程2拿到了两个不同的对象,违背了单例的原则。

因此,在获取锁之后,又进行了一次null检验。

(2)为什么使用volatile 修饰单例变量?

关于volatie和synchronized的区别,可以先参考我的另外一篇文章【JAVA】volatile和synchronized的区别

这段代码,instance = new SingletonDCL(),在虚拟机层面,其实分为了3个指令:

为instance分配内存空间,相当于堆中开辟出来一段空间

实例化instance,相当于在上一步开辟出来的空间上,放置实例化好的SingletonDCL对象

将instance变量引用指向第一步开辟出来的空间的首地址

但由于虚拟机做出的某些优化,可能会导致指令重排序,由1->2->3变成1->3->2。这种重新排序在单线程下不会有任何问题,但出于多线程的情况下,可能会出现以下的问题:

线程1获取锁之后,执行到了instance = new SingletonDCL()阶段,此时,刚好由于虚拟机进行了指令重排序,先进行了第1步开辟内存空间,然后执行了第3步,instance指向空间首地址,第2步还没来得及执行,此时恰好有线程2执行getInstance方法,最外层判断instance不为null(instance已经指向了某一段地址,因此不为null),直接返回了单例对象,接着线程2在获取单例对象属性的时候,出现了空指针错误!

因此使用volatile 修饰单例变量,可以避免由于虚拟机的指令重排序机制可能导致的空指针异常。

四、实现原理

这里可以分两种情况讨论:

(1)同步语句块

  1. public class Main { 
  2.  
  3.     public static final Object object = new Object(); 
  4.  
  5.     public void print() { 
  6.         synchronized (object) { 
  7.             System.out.println("123"); 
  8.         } 
  9.     } 
  10.  

 使用java Main.java,之后使用javap -c Main.class(-c代表反汇编)得到:

  1. public class com.yang.testSyn.Main { 
  2.   public static final java.lang.Object object; 
  3.  
  4.   public com.yang.testSyn.Main(); 
  5.     Code: 
  6.        0: aload_0 
  7.        1: invokespecial #1                  // Method java/lang/Object."<init>":()V 
  8.        4: return 
  9.  
  10.   public void print(); 
  11.     Code: 
  12.        0: getstatic     #2                  // Field object:Ljava/lang/Object; 
  13.        3: dup 
  14.        4: astore_1 
  15.        5: monitorenter 
  16.        6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream; 
  17.        9: ldc           #4                  // String 123 
  18.       11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V 
  19.       14: aload_1 
  20.       15: monitorexit 
  21.       16: goto          24 
  22.       19: astore_2 
  23.       20: aload_1 
  24.       21: monitorexit 
  25.       22: aload_2 
  26.       23: athrow 
  27.       24: return 
  28.     Exception table
  29.        from    to  target type 
  30.            6    16    19   any 
  31.           19    22    19   any 
  32.  
  33.   static {}; 
  34.     Code: 
  35.        0: new           #6                  // class java/lang/Object 
  36.        3: dup 
  37.        4: invokespecial #1                  // Method java/lang/Object."<init>":()V 
  38.        7: putstatic     #2                  // Field object:Ljava/lang/Object; 
  39.       10: return 

 其中print方法中的第5行、15行出现了monitorenter和monitorexit,而这两行其中的字节码代表的正是同步语句块里的内容。

当线程执行到monitorenter时,代表即将进入到同步语句块中,线程首先需要去获得Object的对象锁,而对象锁处于每个java对象的对象头中,对象头中会有一个锁的计数器,当线程查询对象头中计数器,发现内容为0时,则代表该对象没有被任何线程所占有,此时该线程可以占有此对象,计数器于是加1。

线程占有该对象后,也就是拿到该对象的锁,可以执行同步语句块里面的方法。此时,如果有其他线程进来,查询对象头发现计数器不为0,于是进入该对象的锁等待队列中,一直阻塞到计数器为0时,方可继续执行。

第一个线程执行到enterexit后,释放了Object的对象锁,此时第二个线程可以继续执行。

这边依然有几个问题:

[1]为什么有一个monitorenter指令,却有两个monitorexit指令?

因为编译器必须保证,无论同步代码块中的代码以何种方式结束(正常 return 或者异常退出),代码中每次调用 monitorenter 必须执行对应的 monitorexit 指令。为了保证这一点,编译器会自动生成一个异常处理器,这个异常处理器的目的就是为了同步代码块抛出异常时能执行 monitorexit。这也是字节码中,只有一个 monitorenter 却有两个 monitorexit 的原因。

当然这一点,也可以从Exception table(异常表)中看出来,字节码中第6(from)到16(to)的偏移量中如果出现任何类型(type)的异常,都会跳转到第19(target)行。

(2)同步方法

  1. public class Main { 
  2.  
  3.     public synchronized void print(String str) { 
  4.         System.out.println(str); 
  5.     } 
  6.  

 使用javap -v Main.class查看

-v 选项可以显示更加详细的内容,比如版本号、类访问权限、常量池相关的信息,是一个非常有用的参数。

  1. public class com.yang.testSyn.Main 
  2.   minor version: 0 
  3.   major version: 52 
  4.   flags: ACC_PUBLIC, ACC_SUPER 
  5. Constant pool: 
  6.    #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V 
  7.    #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream; 
  8.    #3 = Methodref          #17.#18        // java/io/PrintStream.println:(Ljava/lang/String;)V 
  9.    #4 = Class              #19            // com/yang/testSyn/Main 
  10.    #5 = Class              #20            // java/lang/Object 
  11.    #6 = Utf8               <init> 
  12.    #7 = Utf8               ()V 
  13.    #8 = Utf8               Code 
  14.    #9 = Utf8               LineNumberTable 
  15.   #10 = Utf8               print 
  16.   #11 = Utf8               (Ljava/lang/String;)V 
  17.   #12 = Utf8               SourceFile 
  18.   #13 = Utf8               Main.java 
  19.   #14 = NameAndType        #6:#7          // "<init>":()V 
  20.   #15 = Class              #21            // java/lang/System 
  21.   #16 = NameAndType        #22:#23        // out:Ljava/io/PrintStream; 
  22.   #17 = Class              #24            // java/io/PrintStream 
  23.   #18 = NameAndType        #25:#11        // println:(Ljava/lang/String;)V 
  24.   #19 = Utf8               com/yang/testSyn/Main 
  25.   #20 = Utf8               java/lang/Object 
  26.   #21 = Utf8               java/lang/System 
  27.   #22 = Utf8               out 
  28.   #23 = Utf8               Ljava/io/PrintStream; 
  29.   #24 = Utf8               java/io/PrintStream 
  30.   #25 = Utf8               println 
  31.   public com.yang.testSyn.Main(); 
  32.     descriptor: ()V 
  33.     flags: ACC_PUBLIC 
  34.     Code: 
  35.       stack=1, locals=1, args_size=1 
  36.          0: aload_0 
  37.          1: invokespecial #1                  // Method java/lang/Object."<init>":()V 
  38.          4: return 
  39.       LineNumberTable: 
  40.         line 3: 0 
  41.  
  42.   public synchronized void print(java.lang.String); 
  43.     descriptor: (Ljava/lang/String;)V 
  44.     flags: ACC_PUBLIC, ACC_SYNCHRONIZED 
  45.     Code: 
  46.       stack=2, locals=2, args_size=2 
  47.          0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream; 
  48.          3: aload_1 
  49.          4: invokevirtual #3                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V 
  50.          7: return 
  51.       LineNumberTable: 
  52.         line 32: 0 
  53.         line 33: 7 

 只看最后两个方法,第一个方法是编译后自动生成的默认构造方法,第二个方法则是我们的同步方法,可以看到同步方法比默认的构造方法多了一个ACC_SYNCHRONIZED的标志位。

与同步语句块不同,虚拟机不会在字节码层面实现锁同步,而是会先观察该方法是否含有ACC_SYNCHRONIZED标志。如果含有,则线程会首先尝试获取锁。如果是实例方法,则会尝试获取实例锁;如果是静态方法(类方法),则会尝试获取类锁。最后不管方法执行是否出现异常,都会释放锁。

 

责任编辑:姜华 来源: 今日头条
相关推荐

2022-12-26 09:27:48

Java底层monitor

2024-03-15 15:12:27

关键字底层代码

2017-12-06 16:28:48

Synchronize实现原理

2022-04-13 14:43:05

JVM同步锁Monitor 监视

2024-03-07 07:47:04

代码块Monitor

2022-10-28 10:23:27

Java多线程底层

2019-05-27 08:11:13

高并发Synchronize底层

2023-01-04 07:54:03

HashMap底层JDK

2023-07-11 08:00:00

2022-12-19 08:00:00

SpringBootWeb开发

2017-10-23 10:13:18

IO底层虚拟

2020-08-23 10:03:51

SynchronizeJava

2021-07-04 08:01:30

Synchronize线程安全并发编程

2024-01-29 08:00:00

架构微服务开发

2023-07-17 08:02:44

ZuulIO反应式

2024-03-14 14:56:22

反射Java数据库连接

2017-02-27 10:43:07

Javasynchronize

2019-10-31 10:08:15

Synchronize面试线程

2024-02-29 16:49:20

volatileJava并发编程

2021-08-29 07:41:48

数据HashMap底层
点赞
收藏

51CTO技术栈公众号