Synchronized是怎么实现的?

回答重点

synchronized 实现原理依赖于JVM 的 Monitor(监视器锁)和对象头(Object Header)

  • synchronized 修饰代码块:会在代码块的前后插入 monitorentermonitorexit 指令。可以把 monitorenter理解为加锁,monitorexit 理解为解锁。(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止
  • synchroized修饰方法:synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

其它问题

synchronized的用法有哪些?

  1. 修饰普通方法:作用于当前对象实例,进入同步代码前要获得当前对象实例的锁
  2. 修饰静态方法:作用于当前类,进入同步代码前要获得当前类对象的锁,synchronized关键字加到static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁
  3. 修饰代码块:指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

synchronized的作用有哪些?

  • 原子性:确保线程互斥的访问同步代码;
  • 可见性:保证共享变量的修改能够及时可见;
  • 有序性:有效解决重排序问题。

Synchronized 修饰静态方法和修饰普通方法有什么区别?

  • Synchronized 修饰静态方法:锁的是这个类的 class 对象。也就是说,无论创建了多少个该类的实例,所有的实例共享同一个锁,因为这个锁属于类本身而不是某个对象实例。
  • Synchronized 修饰实例方法:锁的是当前实例(调用该方法的对象),也就是这个对象的内在锁。这也就是说每个对象实例都有自己独立的锁。

构造方法可以用 synchronized 修饰么?

构造方法不能使用 synchronized 关键字修饰。不过,可以在构造方法内部使用 synchronized 代码块。

另外,构造方法本身是线程安全的,但如果在构造方法中涉及到共享资源的操作,就需要采取适当的同步措施来保证整个构造过程的线程安全。

volatile和synchronized的区别是什么?

  1. volatile只能使用在变量上;而synchronized可以在类,变量,方法和代码块上。
  2. volatile至保证可见性;synchronized保证原子性与可见性。
  3. volatile禁用指令重排序;synchronized不会。
  4. volatile不会造成阻塞;synchronized会。

Synchronized 能不能禁止指令重排序?

synchronized 无法完全禁止指令重排序,但能通过内存屏障保证多线程环境下的有序性。对于需要严格禁止重排序的场景,应优先选择 volatile。

这是因为同步块内部的代码仍可能被重排序,只要这种重排序不违反单线程语义

ReentrantLock和synchronized区别

  1. 使用synchronized关键字实现同步,线程执行完同步代码块会自动释放锁,而ReentrantLock需要手动释放锁。
  2. synchronized是非公平锁,ReentrantLock可以设置为公平锁。
  3. ReentrantLock上等待获取锁的线程是可中断的,线程可以放弃等待锁。而synchonized会无限期等待下去。
  4. ReentrantLock 可以设置超时获取锁。在指定的截止时间之前获取锁,如果截止时间到了还没有获取到锁,则返回。
  5. ReentrantLock 的 tryLock() 方法可以尝试非阻塞的获取锁,调用该方法后立刻返回,如果能够获取则返回true,否则返回false。
  6. synchronized 和 ReentrantLock 都是可重入锁

什么是可重入锁

可重入锁是一种特殊的互斥锁,它允许同一个线程在持有锁的情况下再次获取该锁。也就是说,同一个线程可以多次获取同一个可重入锁,而不会发生死锁。

在 Java 中,synchronized关键字就是一种可重入锁。当一个线程使用synchronized修饰的方法或代码块时,它会获得该对象的锁。如果该线程在持有锁的情况下再次调用同一个对象的synchronized方法或代码块,那么它会再次获得该对象的锁,而不会等待其他线程释放锁。

可重入锁的好处是可以避免死锁的发生。因为同一个线程可以多次获取同一个锁,所以当一个线程在持有锁的情况下需要再次获取锁时,它不需要等待其他线程释放锁,从而避免了死锁的发生。

需要注意的是,可重入锁并不是绝对安全的。如果一个线程在持有锁的情况下进行了一些不当的操作,仍然可能导致死锁的发生。因此,在使用可重入锁时,需要注意避免出现这种情况。

锁升级原理了解吗?

在 Java 6 之后, synchronized 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 synchronized 锁的效率提升了很多(JDK18 中,偏向锁已经被彻底废弃)。

锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

synchronized 升级到重量级锁后,所有线程都释放锁了,此时它还是重量级锁吗?

当重量级锁释放了之后,锁对象是无锁的。

有新的线程来竞争的话又会从无锁再到轻量级锁开始后续的升级流程。

扩展——底层机制详细剖析

加锁释放锁原理

synchronized是 Java内建的同步机制,所以也被称为 Intrinsic Locking,提供了互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取锁的线程时只能等待或者阻塞在那里。

synchronized是基于一对 monitorenter/monitorexit 指令实现的,Monitor对象是同步的基本实现单元,无论是显示同步,还是隐式同步都是如此。区别是同步代码块是通过明确的 monitorenter 和 monitorexit 指令实现,而同步方法通过ACC_SYNCHRONIZED 标志来隐式实现。

同步代码块
java

   public class Test1 {

       public void fun1(){

           synchronized (this){

               System.out.println("fun111111111111");

           }

       }

   }

将.java文件使用javac命令编译为.class文件,然后将class文件反编译出来。反编译的字节码文件截取:

通过反编译后的内容查看可以发现,synchronized编译后,同步块的前后有monitorenter/monitorexit两个 字节码指令。在Java虚拟机规范中有描述两条指令的作用:翻译一下如下:

每个对象有一个监视器锁(monitor)。当monitor被占用时就会处于锁定状态,线程执行monitorenter指令时尝试获取monitor的所有权,过程如下:

  1. 如果monitor的进入数为0,则该线程进入monitor,然后将进入数设置为1,该线程即为monitor的所有者。
  2. 如果线程已经占有该monitor,只是重新进入,则进入monitor的进入数加1.
  3. 如果其他线程已经占用了monitor,则该线程进入阻塞状态,直到monitor的进入数为0,再重新尝试获取monitor的所有权

monitorexit:

  1. 执行monitorexit的线程必须是objectref所对应的monitor的所有者。
  2. 指令执行时,monitor的进入数减1,如果减1后进入数为0,那线程退出monitor,不再是这个monitor的所有者。其他被这个monitor阻塞的线程可以尝试去获取这个 monitor 的所有权。

Q:synchronized 代码块内出现异常会释放锁吗?

A:会自动释放锁,查看字节码指令可以知道,monitorexit插入在方法结束处(13行)和异常处(19行)。从Exception table异常表中也可以看出。

同步方法代码
java

   public class Test1 {

       //锁当前对象(this)

       public synchronized void fun2(){

           System.out.println("fun2222222222222222222222");

       }

        //静态synchronized修饰:使用的锁对象是当前类的class对象

       public synchronized static void fun3(){

           System.out.println("fun33333333333333");

       }

   }

编译之后反编译截图:

从反编译的结果来看,同步方法表面上不是通过monitorenter/monitorexit指令来完成,但是与普通方法相比,常量池中多出来了ACC_SYNCHRONIZED标识符。java虚拟机就是根据ACC_SYNCHRONIZED标识符来实现方法的同步,当调用方法时,调用指令先检查方法是否有 ACC_SYNCHRONIZED访问标志,如果存在,执行线程将先获取monitor,获取成功之后才执行方法体,执行完后再释放monitor。在方法执行期间,其他线程都无法再获取到同一个monitor对象。 虽然编译后的结果看起来不一样,但实际上没有本质的区别,只是方法的同步是通过隐式的方式来实现,无需通过字节码来完成。

ACC_SYNCHRONIZED的访问标志,其实就是代表:当线程执行到方法后,如果检测到有该访问标志就会隐式的去调用monitorenter/monitorexit两个命令来将方法锁住。

小结

synchronized 同步代码块的实现是通过 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor的持有权(monitor对象存在于每个Java对象的对象头中, synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因)。

其内部包含一个计数器,当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0 ,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

可重入锁原理

ReentrantLock和synchronized都是可重入锁

定义

指的是 同一个线程的 可以多次获得 同一把锁(一个线程可以多次执行synchronized,重复获取同一把锁)。

java

   /*  可重入特性    指的是 同一个线程获得锁之后,可以再次获取该锁。*/

   public class Demo01 {

       public static void main(String[] args) {

           Runnable sellTicket = new Runnable() {

               @Override

               public void run() {

                   synchronized (Demo01.class) {

                       System.out.println("我是run");

                       test01();

                   }

               }

   

               public void test01() {

                   synchronized (Demo01.class) {

                       System.out.println("我是test01");

                   }

               }

           };

           new Thread(sellTicket).start();

           new Thread(sellTicket).start();

       }

   }
为什么要有可重入性?

可重入性主要有以下核心原因:

  • 避免死锁:在嵌套调用场景下(如递归方法、多层服务调用),同一个线程需要多次获取同一把锁。若不可重入,外层获取锁后内层再次尝试获取会被阻塞,导致线程永久等待。
  • 简化编程模型:业务代码中可能隐式调用已加锁的方法,可重入锁允许我们不必手动维护"当前线程是否已持有锁"的状态,降低心智负担
  • 提升性能:可重入机制通过维护重入计数器,避免了同一线程重复获取锁时的网络通信开销(如Redis的多次SETNX操作)。
  • 业务场景驱动:常见于需要嵌套事务、递归处理、链式调用等场景。例如:
java

   // 伪代码示例:嵌套调用

   public void methodA() {

   	 lock.lock();

   	 try {

   		 methodB(); // 需要能再次获取同一个锁

   	 } finally {

   		 lock.unlock();

   	 }

   }

   

   public void methodB() {

   	 lock.lock();

   	 try {

   		 // do something

   	 } finally {

   		 lock.unlock();

   	 }

   }
原理

synchronized 的锁对象中有一个计数器(recursions变量)会记录线程获得几次锁,每重入一次,计数器就 + 1,在执行完一个同步代码块时,计数器数量就会减1,直到计数器的数量为0才释放这个锁。

  • 执行monitorenter获取锁 :
    • (monitor计数器=0,可获取锁)
    • 执行method1()方法,monitor计数器+1 -> 1 (获取到锁)
    • 执行method2()方法,monitor计数器+1 -> 2
    • 执行method3()方法,monitor计数器+1 -> 3
  • 执行monitorexit命令 :
    • method3()方法执行完,monitor计数器-1 -> 2
    • method2()方法执行完,monitor计数器-1 -> 1
    • method2()方法执行完,monitor计数器-1 -> 0 (释放了锁)
    • (monitor计数器=0,锁被释放了)

优点

可以一定程度上避免死锁(如果不能重入,那就不能再次进入这个同步代码块,导致死锁);更好地封装代码(可以把同步代码块写入到一个方法中,然后在另一个同步代码块中直接调用该方法实现可重入);

保证可见性原理

这个主要在于内存模型和happens-before规则

Synchronized的happens-before规则,即监视器锁规则:对同一个监视器的解锁,happens-before于对该监视器的加锁。

java

   public class MonitorDemo {

       private int a = 0;

   

       public synchronized void writer() {     // 1

           a++;                                // 2

       }                                       // 3

   

       public synchronized void reader() {    // 4

           int i = a;                         // 5

       }                                      // 6

   }

该代码的happens-before关系如图所示:

在图中每一个箭头连接的两个节点就代表之间的happens-before关系,黑色的是通过程序顺序规则推导出来,红色的为监视器锁规则推导而出:线程A释放锁happens-before线程B加锁,蓝色的则是通过程序顺序规则和监视器锁规则推测出来happens-befor关系,通过传递性规则进一步推导的happens-before关系。

这里是2 happens-before 5,通过这个关系可以得出:根据happens-before的定义中的一条:如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B。线程A先对共享变量A进行加一,由2 happens-before 5关系可知线程A的执行结果对线程B可见即线程B所读取到的a的值为1


__EOF__

  • 本文作者: Seven
  • 本文链接: https://www.cnblogs.com/seven97-top/p/18903349
  • 关于博主: Seven的菜鸟成长之路
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。
原创作者: seven97-top 转载于: https://www.cnblogs.com/seven97-top/p/18903349
<think>我们正在讨论Java中的synchronized关键字,特别是其实现原理。根据用户的问题,他之前已经询问了“什么是synchronized”,现在他想深入了解其实现机制。因此,我们需要重点解释synchronized的底层工作原理。 根据引用[1][3][4],synchronized的底层实现主要涉及对象头、Monitor以及锁升级机制。同时,引用[5]提到了synchronized可以修饰代码块、方法等,并对其使用方式做了说明。 下面,我将按照以下结构组织回答: 1. 对象头与Monitor:解释synchronized如何利用对象头和Monitor实现锁。 2. 字节码指令:说明synchronized在字节码层面的表现。 3. 锁升级过程:详细描述从无锁到偏向锁、轻量级锁、重量级锁的升级过程。 4. 内存语义:解释synchronized如何保证可见性和有序性。 同时,为了满足用户要求,我们将使用LaTeX格式表达数学公式(如果有的话),并在适当位置添加引用标识。最后,生成相关问题。 注意:由于用户要求使用中文,并且要求结构清晰,我们将分点展开,并使用代码块和图表(mermaid)进行说明。</think>### Java中synchronized关键字的实现原理详解 #### 一、核心实现机制 synchronized的底层实现基于**对象监视器锁(Monitor)** 机制,通过JVM内置锁实现线程同步[^1][^3]。其核心组件包括: 1. **对象头结构** 每个Java对象在堆内存中包含对象头,其中**Mark Word**存储锁状态信息: ```mermaid classDiagram class MarkWord { +锁标志位(2bit) +偏向锁标志(1bit) +线程ID(54bit) +epoch(2bit) +轻量级锁指针(62bit) +重量级锁指针(62bit) } ``` 2. **Monitor对象** 当发生锁竞争时,JVM创建ObjectMonitor对象: ```cpp class ObjectMonitor { void* _owner; // 持有锁的线程 ObjectWaiter* _EntryList; // 阻塞线程队列 ObjectWaiter* _WaitSet; // wait()等待队列 int _recursions; // 重入计数器 } ``` #### 二、字节码指令实现 1. **同步代码块** 编译器生成`monitorenter`和`monitorexit`指令: ```java public void demo() { synchronized(obj) { // ↓ monitorenter指令 // ... } // ↑ monitorexit指令 } ``` - `monitorenter`:尝试获取对象锁 - `monitorexit`:释放对象锁(包括异常路径) 2. **同步方法** 方法访问标志添加`ACC_SYNCHRONIZED`: ```java public synchronized void method() { // JVM隐式加锁 } ``` JVM在方法调用时自动获取锁,返回时释放锁[^3] #### 三、锁升级优化(JDK 6+) ```mermaid graph LR A[无锁] -->|首次获取锁| B[偏向锁] B -->|多线程竞争| C[轻量级锁] C -->|自旋失败| D[重量级锁] D -->|释放锁| A ``` 1. **偏向锁** - **原理**:Mark Word记录线程ID(无需CAS) - **场景**:单线程重复访问同步块 - **优势**:消除无竞争同步开销[^4] 2. **轻量级锁** - **原理**:通过CAS自旋尝试获取锁 - **流程**: ```mermaid sequenceDiagram ThreadA->>JVM: 复制对象头到栈帧 ThreadA->>MarkWord: CAS替换为锁记录指针 Note over ThreadA: 成功则获取锁 ThreadB->>MarkWord: CAS失败则自旋 ThreadB-->>JVM: 自旋超过阈值升级重量级锁 ``` 3. **重量级锁** - **原理**:依赖操作系统互斥量(mutex) - **流程**:竞争线程进入`_EntryList`队列阻塞等待 - **开销**:线程上下文切换成本高[^4] #### 四、内存语义实现 1. **可见性保证** 通过内存屏障实现: - 加锁时:清空工作内存 → 从主内存加载最新值 - 解锁时:刷新工作内存 → 写回主内存 $$ \text{LoadLoad屏障} \rightarrow \text{LoadStore屏障} \rightarrow \text{临界区} \rightarrow \text{StoreStore屏障} \rightarrow \text{StoreLoad屏障} $$ 2. **有序性保证** - 遵循**锁规则**:解锁操作先于后续加锁操作 - 禁止临界区内指令重排序[^3] #### 五、典型场景实现分析 1. **同步代码块** ```java public void transfer(Account target) { synchronized(Account.class) { // 类锁 this.balance -= amount; target.balance += amount; } } ``` - 字节码:`monitorenter` → 转账操作 → `monitorexit` 2. **双检锁单例** ```java public class Singleton { private volatile static Singleton instance; public static Singleton getInstance() { if (instance == null) { // 第一次检查 synchronized (Singleton.class) { // 加锁 if (instance == null) { // 第二次检查 instance = new Singleton(); // volatile写 } } } return instance; } } ``` - `volatile`防止指令重排序 - 同步块保证构造过程原子性[^5] #### 六、性能优化建议 1. **减小锁粒度** 锁定最小必要范围(如私有锁对象) ```java public class Cache { private final Object lock = new Object(); // 私有锁对象 private Map<String, String> data = new HashMap<>(); public void put(String key, String value) { synchronized (lock) { // 仅锁定私有对象 data.put(key, value); } } } ``` 2. **避免锁升级** - 短时间操作用轻量级锁 - 长时间操作考虑`ReentrantLock` 3. **锁分离技术** 读写分离(如`ReentrantReadWriteLock`) --- ### 总结 1. **对象头控制**:通过Mark Word实现锁状态存储[^1][^4] 2. **Monitor机制**:依赖`_owner`和`_EntryList`管理线程阻塞[^3] 3. **锁升级策略**:无锁→偏向锁→轻量级锁→重量级锁的渐进优化[^4] 4. **内存屏障**:保证可见性和有序性(`happens-before`原则) 5. **字节码指令**:`monitorenter`/`monitorexit`实现显式同步[^3] --- **相关问题** 1. 偏向锁如何解决同步问题?为什么JDK 15默认禁用偏向锁? 2. `monitorenter`和`monitorexit`指令在异常处理中如何保证锁释放? 3. 轻量级锁的自旋次数如何动态调整?与CPU核心数的关系是什么? 4. 对象头中的Mark Word在不同锁状态下如何存储信息? 5. 为什么双检锁单例模式需要配合volatile使用?[^5]
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值