我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Synchronized的底层实现原理是什么

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Synchronized的底层实现原理是什么

Synchronized的底层实现原理是什么,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。

(1)给静态方法加锁

public class Main {         public static synchronized void staticSynPrint(String str) {         System.out.println(str);     }  }

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

(2)给静态变量加锁

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

(3)synchronized(xxx.class)

public class Main {      public void classSynPrint(String str) {         synchronized (Main.class) {             System.out.println(str);         }     }  }

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

(4)synchronized(this)

public class Main {      public void thisSynPrint(String str) {         synchronized (this) {             System.out.println(str);         }     }  }

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

(5)给实例方法加锁

public class Main {      public synchronized void synPrint(String str) {         System.out.println(str);     }  }

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

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

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

public class SingletonDCL {     private volatile static SingletonDCL instance;      private SingletonDCL() {     }      public static SingletonDCL getInstance() {         if (instance == null) {             synchronized (Singleton.class) {                 if (instance == null) {                     instance = new SingletonDCL();                 }             }         }         return instance;     }  }

有几个疑问:

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

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

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

public static SingletonDCL getInstance() {        if (instance == null) {            synchronized (Singleton.class) {                    //一些耗时的操作                    instance = new SingletonDCL();            }        }        return instance;    }

但这样,存在一个问题。线程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)同步语句块

public class Main {      public static final Object object = new Object();      public void print() {         synchronized (object) {             System.out.println("123");         }     }  }

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

public class com.yang.testSyn.Main {   public static final java.lang.Object object;    public com.yang.testSyn.Main();     Code:        0: aload_0        1: invokespecial #1                  // Method java/lang/Object."<init>":()V        4: return    public void print();     Code:        0: getstatic     #2                  // Field object:Ljava/lang/Object;        3: dup        4: astore_1        5: monitorenter        6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;        9: ldc           #4                  // String 123       11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V       14: aload_1       15: monitorexit       16: goto          24       19: astore_2       20: aload_1       21: monitorexit       22: aload_2       23: athrow       24: return     Exception table:        from    to  target type            6    16    19   any           19    22    19   any    static {};     Code:        0: new           #6                  // class java/lang/Object        3: dup        4: invokespecial #1                  // Method java/lang/Object."<init>":()V        7: putstatic     #2                  // Field object:Ljava/lang/Object;       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)同步方法

public class Main {      public synchronized void print(String str) {         System.out.println(str);     }  }

使用javap -v Main.class查看

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

public class com.yang.testSyn.Main   minor version: 0   major version: 52   flags: ACC_PUBLIC, ACC_SUPER Constant pool:    #1 = Methodref          #5.#14         // java/lang/Object."<init>":()V    #2 = Fieldref           #15.#16        // java/lang/System.out:Ljava/io/PrintStream;    #3 = Methodref          #17.#18        // java/io/PrintStream.println:(Ljava/lang/String;)V    #4 = Class              #19            // com/yang/testSyn/Main    #5 = Class              #20            // java/lang/Object    #6 = Utf8               <init>    #7 = Utf8               ()V    #8 = Utf8               Code    #9 = Utf8               LineNumberTable   #10 = Utf8               print   #11 = Utf8               (Ljava/lang/String;)V   #12 = Utf8               SourceFile   #13 = Utf8               Main.java   #14 = NameAndType        #6:#7          // "<init>":()V   #15 = Class              #21            // java/lang/System   #16 = NameAndType        #22:#23        // out:Ljava/io/PrintStream;   #17 = Class              #24            // java/io/PrintStream   #18 = NameAndType        #25:#11        // println:(Ljava/lang/String;)V   #19 = Utf8               com/yang/testSyn/Main   #20 = Utf8               java/lang/Object   #21 = Utf8               java/lang/System   #22 = Utf8               out   #23 = Utf8               Ljava/io/PrintStream;   #24 = Utf8               java/io/PrintStream   #25 = Utf8               println {   public com.yang.testSyn.Main();     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    public synchronized void print(java.lang.String);     descriptor: (Ljava/lang/String;)V     flags: ACC_PUBLIC, ACC_SYNCHRONIZED     Code:       stack=2, locals=2, args_size=2          0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;          3: aload_1          4: invokevirtual #3                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V          7: return       LineNumberTable:         line 32: 0         line 33: 7 }

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

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

看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注编程网行业资讯频道,感谢您对编程网的支持。

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

Synchronized的底层实现原理是什么

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

chatgpt底层实现的原理是什么

chatgpt底层实现的原理是通过人工的标注方式来训练出一种强化学习的冷启动模型和reward反馈模型,然后再通过强化学习的模式来学习出对话友好的chatGPT。chatgpt的原理ChatGPT是在 GPT(Generative Pre-
2023-02-09

HashMap的底层实现原理是什么

这篇文章给大家介绍HashMap的底层实现原理是什么,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。1.HashMap的常用方法// Hashmap存值:--------------------------------
2023-06-06

redis的底层实现原理是什么

Redis的底层实现原理主要包括以下几个方面:数据结构:Redis支持多种数据结构,如字符串、哈希表、列表、集合、有序集合等。这些数据结构在内存中以特定的数据结构形式存储,以满足对应的操作需求。内存分配:Redis使用自己的内存分配器来管理
redis的底层实现原理是什么
2024-04-19

Synchronized的底层实现原理(原理解析,面试必备)

synchronized 一. synchronized解读 1.1 简单描述 synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized 翻译为中文的意思是同步,也称之为同步锁。 synchronize
2023-08-19

golang map底层实现原理是什么

Golang中的map是基于散列表(hash table)实现的。散列表是一种用于存储键值对的数据结构,它通过将键映射到数组的索引来实现高效的插入、查找和删除操作。具体来说,Golang中的map底层实现原理如下:Golang的map使用
2023-10-21

spring注解的底层实现原理是什么

Spring注解的底层实现原理主要依赖于Java的反射机制。在Spring中,通过使用注解来标识类、方法或字段,从而告诉Spring容器如何处理它们。当Spring容器启动时,它会扫描应用程序中的注解,并根据注解的信息生成相应的对象和配置。
2023-10-09

js数组底层实现原理是什么

JavaScript数组底层实现原理可以分为两种情况:稠密数组和稀疏数组。1. 稠密数组(Dense Array):稠密数组是指数组中的元素连续存储在内存中的连续地址上。在JavaScript中,稠密数组一般是通过类似于C语言中的连续内存分
2023-09-12

Java必会的Synchronized底层原理剖析

synchronized作为Java程序员最常用同步工具,很多人却对它的用法和实现原理一知半解,以至于还有不少人认为synchronized是重量级锁,性能较差,尽量少用。但不可否认的是synchronized依然是并发首选工具,本文就来详细讲讲
2022-11-13

Java并发编程之synchronized底层实现原理分析

Java并发编程中的synchronized关键字通过JVM内部锁机制实现线程同步。锁对象与其监视器锁关联,单个线程同一时刻只能访问锁对象保护的代码段。获取锁时,线程需先获取锁对象监视器锁,释放锁时需释放该锁。每个Java对象的对象头中存储锁状态,偏向锁、轻量级锁、重量级锁会根据不同场景用于优化性能。
Java并发编程之synchronized底层实现原理分析
2024-04-02

HashMap的底层原理是什么

这篇文章将为大家详细讲解有关HashMap的底层原理是什么,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。一:HashMap的节点:HashMap是一个集合,键值对的集合,源码中每个节点用No
2023-06-04

Vue的底层原理是什么

这篇文章主要介绍Vue的底层原理是什么,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!Observer (数据劫持)核心是通过Obeject.defineProperty()来监听数据的变动,这个函数内部可以定义set
2023-06-29

编程热搜

目录