synchronized 原理分析!

简介: 你好,我是猿java。本文详细解析了Java中`synchronized`关键字的工作原理,从对象监视器锁的角度阐述其实现机制,并介绍了锁的获取、释放及字节码级别的实现细节。同时,还探讨了锁优化技术,如偏向锁、轻量级锁、适应性自旋等,以及锁消除和锁粗化等编译器优化。通过具体的字节码分析,展示了不同类型的`synchronized`应用实例。希望对你有所帮助,欢迎关注猿java,获取更多硬核文章。

你好,我是猿java。

synchronized关键字是Java中用于实现线程同步的机制之一,它可以确保在同一时刻只有一个线程可以访问某个代码块或方法,从而避免线程之间的竞争条件和数据不一致的问题。这篇文章,我们将从字节码角度来剖析synchronized工作原理。

工作原理

synchronized 实现原理主要依赖于 Java对象的内置锁(也称为监视器锁,Monitor Lock)。

  1. 对象监视器(Monitor):

    • 每个Java对象都有一个与之关联的监视器(Monitor),这个监视器是实现同步的核心。
    • 当一个线程试图进入一个synchronized方法或代码块时,它必须首先获得对象的监视器锁。如果监视器锁已经被其他线程持有,那么当前线程将被阻塞,直到监视器锁被释放。
  2. 锁的获取和释放:

    • 对于实例方法,锁是对象实例的监视器。
    • 对于静态方法,锁是类对象的监视器。
    • 对于代码块,锁是指定对象的监视器。
    • 当线程进入synchronized方法或代码块时,它会尝试获取相应的监视器锁。如果锁不可用,它会进入阻塞状态,直到锁被释放。
    • 当线程退出synchronized方法或代码块时,它会释放监视器锁,从而允许其他阻塞的线程继续执行。
  3. 字节码级别的实现:

    • 在字节码级别,当编译器遇到synchronized关键字时,它会生成相应的监视器进入和退出指令。
    • 对于synchronized方法,编译器会在方法的开始和结束处插入monitorentermonitorexit指令。
    • 对于synchronized代码块,编译器会在代码块的开始和结束处插入monitorentermonitorexit指令。
  4. 偏向锁和轻量级锁:

    • 为了提高性能,Java引入了偏向锁和轻量级锁的机制。
    • 偏向锁:如果一个对象的锁被一个线程多次获取,那么该锁会偏向这个线程,从而减少获取锁的开销。
    • 轻量级锁:如果偏向锁被其他线程竞争,那么锁会升级为轻量级锁,通过自旋的方式尝试获取锁,而不是直接阻塞线程。
    • 如果竞争依然激烈,轻量级锁会升级为重量级锁,此时会导致线程阻塞。
  5. 锁的升级和降级:

    • 锁可以从偏向锁升级为轻量级锁,再升级为重量级锁。
    • 一旦锁升级为重量级锁,它不会降级为轻量级锁或偏向锁。

锁优化

在 Java中,为了提高多线程环境下的性能,Java虚拟机(JVM)对锁的实现进行了多种优化,包括偏向锁、轻量级锁、适应性自旋、锁消除和锁粗化。这些优化技术主要是为了减少锁的开销,提高并发性能。

偏向锁

偏向锁(Biased Locking)是为了减少同一线程多次获取锁的开销而设计的。

  • 原理:当一个线程首次获取锁时,锁会偏向这个线程,即在对象头中记录该线程ID。之后,如果该线程再次获取锁,不需要进行任何同步操作,只需检查对象头中的线程ID是否与当前线程匹配。
  • 撤销:如果有其他线程尝试获取偏向锁,则偏向锁会被撤销并升级为轻量级锁。

轻量级锁

轻量级锁(Lightweight Locking)是为了减少竞争不激烈的情况下的锁开销而设计的。

  • 原理:当线程尝试获取轻量级锁时,如果锁是空闲的,则通过CAS操作将锁对象的Mark Word复制到当前线程的栈帧中,并将Mark Word指向栈帧中的锁记录。如果获取成功,锁状态变为轻量级锁。
  • 自旋:如果锁已经被其他线程持有,当前线程会进行自旋,而不是直接进入阻塞状态。自旋的次数是有限的,如果超过一定次数,自旋失败,锁会升级为重量级锁。

适应性自旋

适应性自旋(Adaptive Spinning)是在轻量级锁的基础上进一步优化自旋等待的机制。

  • 原理:自旋的次数不再是固定的,而是根据前一次自旋的结果动态调整。如果前一次自旋成功,那么下一次自旋的次数会增加;如果前一次自旋失败,下一次自旋的次数会减少。
  • 优势:通过动态调整自旋次数,可以更好地适应不同的锁竞争情况,减少不必要的线程阻塞和上下文切换。

锁消除

锁消除(Lock Elimination)是编译器优化的一种技术,用于在编译期间消除不必要的锁操作。

  • 原理:在JIT编译过程中,编译器通过逃逸分析(Escape Analysis)确定某个对象是否只在单线程中使用。如果对象没有逃逸到其他线程,那么对该对象的锁操作是多余的,可以被消除。
  • 示例:在方法内部创建的局部变量对象,如果没有逃逸到方法外部或其他线程,则对该对象的同步操作可以被消除。

锁粗化

锁粗化(Lock Coarsening)是为了减少频繁获取和释放锁的开销而设计的。

  • 原理:如果编译器检测到在一段代码中频繁地对同一个对象进行加锁和解锁操作,它会将这些操作合并成一个更大的范围。在更大范围内进行一次加锁和解锁,从而减少锁的开销。
  • 示例:在循环中频繁进行加锁和解锁操作,可以将锁的范围扩大到整个循环外部。

通过上述锁优化的过程可以看出,锁优化技术的共同目标是提高多线程环境下的性能,减少锁的开销。具体来说:

  • 偏向锁:减少同一线程多次获取锁的开销。
  • 轻量级锁:减少竞争不激烈情况下的锁开销。
  • 适应性自旋:动态调整自旋次数,更好地适应不同的锁竞争情况。
  • 锁消除:在编译期间消除不必要的锁操作。
  • 锁粗化:减少频繁获取和释放锁的开销。

字节码分析

在Java中,synchronized关键字通过在字节码中插入特定的指令来实现线程同步。这些指令主要是 monitorentermonitorexit

让我们通过具体的例子和字节码分析来理解 synchronized 在不同类型上的实现。

同步实例方法

public class MyClass {
   
    public synchronized void instanceMethod() {
   
        System.out.println("Instance method");
    }
}

编译后的字节码(可以使用 javap -c MyClass 命令查看):

public synchronized void instanceMethod();
  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 Instance method
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

分析:

  • ACC_SYNCHRONIZED 标志在方法的访问标志中,表示这是一个同步方法。
  • JVM 会在调用这个方法时自动获取和释放对象实例的监视器锁(this对象)。

同步静态方法

public class MyClass {
   
    public static synchronized void staticMethod() {
   
        System.out.println("Static method");
    }
}

编译后的字节码:

public static synchronized void staticMethod();
  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 Static method
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

分析:

  • ACC_SYNCHRONIZED 标志在方法的访问标志中,表示这是一个同步方法。
  • ACC_STATIC 标志表示这是一个静态方法。
  • JVM 会在调用这个方法时自动获取和释放类对象的监视器锁(MyClass.class)。

同步代码块

public class MyClass {
   
    private final Object lock = new Object();

    public void method() {
   
        synchronized (lock) {
   
            System.out.println("Synchronized block");
        }
    }
}

编译后的字节码:

public void method();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=3, locals=2, args_size=1
       0: aload_0
       1: getfield      #2                  // Field lock:Ljava/lang/Object;
       4: dup
       5: astore_1
       6: monitorenter
       7: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
      10: ldc           #4                  // String Synchronized block
      12: invokevirtual #5                  // 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

分析:

  • monitorenter 指令在进入同步块时执行,尝试获取lock对象的监视器锁。
  • monitorexit 指令在离开同步块时执行,释放lock对象的监视器锁。
  • 字节码中包括异常处理,以确保即使在异常情况下也能正确释放锁。

从上述字节码的分析可以看出:

  • 对于同步实例方法和静态方法,字节码中使用了ACC_SYNCHRONIZED标志,JVM会自动处理锁的获取和释放。
  • 对于同步代码块,字节码中显式地插入了monitorentermonitorexit指令,以手动处理锁的获取和释放。

总结

总结来说,synchronized关键字通过对象监视器锁的机制,确保在同一时刻只有一个线程能够执行synchronized方法或代码块,从而实现线程同步。Java通过偏向锁和轻量级锁等优化手段,提高了锁的性能,减少了线程阻塞的开销。

学习交流

如果你觉得文章有帮助,请帮忙转发给更多的好友,或关注:猿java,持续输出硬核文章。

目录
相关文章
|
存储 Java 索引
【面试题精讲】ArrayList 和 Array(数组)的区别?
【面试题精讲】ArrayList 和 Array(数组)的区别?
|
6月前
|
人工智能 自然语言处理 搜索推荐
2025中国AI数字人企业厂商权威推荐与技术、场景、口碑综合对比
数字人企业正以AI与图形技术融合之势崛起,像衍科技、阿里、百度等领军者在零售、金融、政务多场景落地。依托大模型与3D渲染,数字人实现智能交互,广泛应用于教育、服务、内容创作等领域,推动产业降本增效。2025年市场规模将超600亿,技术革新与伦理规范需协同并进,构建有温度的数字未来。
|
存储 缓存 监控
JVM详解 --- 垃圾回收机制
JVM详解 --- 垃圾回收机制
JVM详解 --- 垃圾回收机制
|
NoSQL Java Redis
redisson分布式锁
Redisson 分布式锁提供了一种简单高效的方式来实现分布式系统中的锁机制。通过本文介绍的基本用法和高级用法,开发者可以根据具体的业务需求选择合适的锁类型来确保系统的稳定性和高并发性。希望本文能帮助读者更好地理解和使用 Redisson 分布式锁,提高系统的并发处理能力和可靠性。
739 10
|
存储 缓存 Java
理解Java引用数据类型:它们都是对象引用
本文深入探讨了Java中引用数据类型的本质及其相关特性。引用变量存储的是对象的内存地址而非对象本身,类似房子的地址而非房子本身。文章通过实例解析了引用赋值、比较(`==`与`equals()`的区别)以及包装类缓存机制等核心概念。此外,还介绍了Java引用类型的家族,包括类、接口、数组和枚举。理解这些内容有助于开发者避免常见错误,提升对Java内存模型的掌握,为高效编程奠定基础。
626 0
|
机器学习/深度学习 人工智能 自然语言处理
MetaGPT开源自动生成智能体工作流,4.55%成本超GPT-4o
AFlow是由Jiayi Zhang等学者提出的一项新研究,发表于arXiv。它通过将工作流优化问题转化为代码表示空间中的搜索,并引入蒙特卡洛树搜索(MCTS)算法,实现了高效的工作流自动化生成与优化。在六个基准数据集上,AFlow性能比现有基线平均提高5.7%,并使小模型以较低成本超越GPT-4。尽管存在一些局限性,如通用性和计算复杂度,AFlow为降低大型语言模型应用成本提供了新思路,推动了人工智能技术的进步。论文地址:https://arxiv.org/abs/2410.10762。
530 27
|
存储 Java 对象存储
String 属于基础的数据类型吗
String 在多数编程语言中被视为一种基础数据类型,但实际上它是由字符组成的序列。在一些语言中,如 Java 和 Python,String 被设计为不可变的对象,以简化编程和提高安全性。
562 6
|
存储 数据管理 C语言
C 语言中的文件操作:数据持久化的关键桥梁
C语言中的文件操作是实现数据持久化的重要手段,通过 fopen、fclose、fread、fwrite 等函数,可以实现对文件的创建、读写和关闭,构建程序与外部数据存储之间的桥梁。
|
监控 Java
线程池大小如何设置
在并发编程中,线程池是一个非常重要的组件,它不仅能够提高程序的响应速度,还能有效地利用系统资源。合理设置线程池的大小对于优化系统性能至关重要。本文将探讨如何根据应用场景和系统资源来设置线程池的大小。
|
网络协议 物联网 网络性能优化
物联网江湖风云变幻!MQTT CoAP RESTful/HTTP XMPP四大门派谁主沉浮?
【8月更文挑战第14天】本文概览了MQTT、CoAP、RESTful/HTTP及XMPP四种物联网通信协议。MQTT采用发布/订阅模式,轻量高效;CoAP针对资源受限设备,基于UDP,低延迟;RESTful/HTTP易于集成现有Web基础设施;XMPP支持双向通信,扩展性强。每种协议均附有示例代码,助您根据不同场景和设备特性作出最佳选择。
309 5