当前位置: 首页 > news >正文

synchronized 关键字深度解析

1. 概述

在 Java 多线程编程中,synchronized 关键字是一个至关重要的工具,它用于解决并发访问共享资源时可能出现的数据一致性问题,即防止“脏数据”的产生。当多个线程试图同时访问和修改同一个共享资源时,如果没有适当的同步机制,就可能导致数据混乱、逻辑错误甚至程序崩溃。synchronized 关键字提供了一种内置的同步机制,确保在同一时刻,只有一个线程能够执行被 synchronized 修饰的代码块或方法,从而保证了共享数据的原子性、可见性和有序性。

synchronized 是 Java 语言层面的关键字,由 JVM 实现,因此使用起来相对简单。尽管在 JDK 5 之后引入了 java.util.concurrent.locks.Lock 接口提供了更灵活的锁机制,但 synchronized 关键字因其易用性和可靠性,在实际开发中仍然被广泛使用。

核心作用:

  • 原子性: 确保被 synchronized 保护的代码块或方法在执行过程中不会被中断,要么全部执行成功,要么全部不执行,中间不会出现其他线程的干扰。
  • 可见性: 确保当一个线程修改了共享变量的值时,新值对于其他线程是立即可见的。当一个线程释放锁时,它会把对共享变量的修改刷新到主内存中;当另一个线程获取锁时,它会从主内存中读取共享变量的最新值。
  • 有序性: 确保指令重排序不会对 synchronized 保护的代码块造成影响。在 synchronized 块内部,指令会按照程序的顺序执行,避免了重排序可能带来的并发问题。

简而言之,synchronized 关键字通过提供一种互斥访问机制,使得在多线程环境下对共享资源的访问变得安全可靠。任何线程要执行 synchronized 里的代码,都必须先获取到对应的锁,没有获取到锁的线程只能等待。

2. 使用

synchronized 关键字可以修饰方法或代码块,其作用范围和锁定的对象有所不同。理解其不同的使用方式对于正确地实现线程安全至关重要。

2.1. 修饰实例方法

synchronized 关键字修饰一个非静态方法时,它锁定的是当前实例对象(this)。这意味着,如果一个类的多个实例同时运行,每个实例都有自己的锁,它们之间互不影响。但对于同一个实例,在任何时刻,只有一个线程能够执行该实例的 synchronized 方法。

示例:

public class Counter {private int count = 0;public synchronized void increment() {count++;System.out.println(Thread.currentThread().getName() + ": " + count);}public int getCount() {return count;}
}// 使用示例
public class SynchronizedMethodDemo {public static void main(String[] args) {Counter counter = new Counter();Runnable task = () -> {for (int i = 0; i < 5; i++) {counter.increment();}};Thread t1 = new Thread(task, "Thread-1");Thread t2 = new Thread(task, "Thread-2");t1.start();t2.start();}
}

在上述 Counter 类中,increment() 方法被 synchronized 修饰。当 Thread-1 调用 counter.increment() 时,它会获取 counter 对象的锁。在 Thread-1 释放锁之前,即使 Thread-2 也尝试调用 counter.increment(),它也必须等待,直到 Thread-1 完成并释放锁。这保证了 count 变量的原子性操作。

2.2. 修饰静态方法

synchronized 关键字修饰一个静态方法时,它锁定的是当前类的 Class 对象。由于类的 Class 对象是唯一的,因此无论创建多少个该类的实例,静态 synchronized 方法的锁都是同一个。这意味着,在任何时刻,只有一个线程能够执行该类的任何一个静态 synchronized 方法。

示例:

public class StaticCounter {private static int count = 0;public static synchronized void increment() {count++;System.out.println(Thread.currentThread().getName() + ": " + count);}public static int getCount() {return count;}
}// 使用示例
public class SynchronizedStaticMethodDemo {public static void main(String[] args) {Runnable task = () -> {for (int i = 0; i < 5; i++) {StaticCounter.increment();}};Thread t1 = new Thread(task, "Thread-A");Thread t2 = new Thread(task, "Thread-B");t1.start();t2.start();}
}

在这个例子中,StaticCounter.increment() 方法被 synchronized 修饰。Thread-AThread-B 都会尝试调用这个静态方法。由于锁是 StaticCounter.class 对象,因此在任何时候,只有一个线程能够成功进入 increment() 方法,从而保证了静态变量 count 的线程安全。

2.3. 修饰代码块

synchronized 关键字也可以修饰一个代码块,这提供了更细粒度的控制。被修饰的代码块称为同步语句块,其作用范围是大括号 {} 括起来的代码。括号中需要指定一个对象作为锁,这个对象被称为“监视器锁”或“互斥锁”。

语法:

synchronized (object) {// 需要同步的代码
}

object 可以是任何非 null 的 Java 对象。当线程进入 synchronized 代码块时,它会尝试获取 object 对象的锁。只有成功获取锁的线程才能执行代码块中的内容。这种方式的优点是,可以将锁的范围限制在真正需要同步的代码上,而不是整个方法,从而提高程序的并发性。

示例:

public class BlockCounter {private int count = 0;private final Object lock = new Object(); // 定义一个专门的锁对象public void increment() {synchronized (lock) { // 使用 lock 对象作为锁count++;System.out.println(Thread.currentThread().getName() + ": " + count);}}public int getCount() {return count;}
}// 使用示例
public class SynchronizedBlockDemo {public static void main(String[] args) {BlockCounter counter = new BlockCounter();Runnable task = () -> {for (int i = 0; i < 5; i++) {counter.increment();}};Thread t1 = new Thread(task, "Thread-X");Thread t2 = new Thread(task, "Thread-Y");t1.start();t2.start();}
}

在这个例子中,increment() 方法内部的 synchronized 代码块使用了 lock 对象作为锁。Thread-XThread-Y 都会尝试获取 lock 对象的锁。这确保了 count++ 操作的原子性。与修饰方法不同,BlockCounter 类的其他非同步方法可以被其他线程同时访问,从而提高了并发度。

选择锁对象的注意事项:

  • 实例方法内部: 通常使用 this 作为锁对象。
  • 静态方法内部: 通常使用 ClassName.class 作为锁对象。
  • 代码块: 可以使用任何非 null 的对象作为锁。通常建议创建一个私有的 final 对象作为锁,以避免外部代码意外地获取到这个锁,从而导致死锁或性能问题。避免使用字符串字面量作为锁对象,因为字符串常量池可能导致不同地方的字符串字面量是同一个对象,从而造成不必要的竞争。

总结不同使用方式的锁对象:

使用方式锁对象适用场景
修饰实例方法当前实例对象 (this)保护实例的共享数据,不同实例间互不影响
修饰静态方法当前类的 Class 对象 (ClassName.class)保护类的静态共享数据,所有实例共享同一把锁
修饰代码块指定的任意对象细粒度控制,保护特定代码块,提高并发性

正确选择 synchronized 的使用方式和锁对象是编写高效、线程安全并发程序的关键。

3. 实现原理

synchronized 关键字在 Java 虚拟机(JVM)层面是通过监视器(Monitor)来实现的。每个 Java 对象都可以关联一个监视器。当一个线程试图获取对象的锁时,它实际上是试图获取该对象关联的监视器。JVM 内部通过 monitorentermonitorexit 字节码指令来实现 synchronized 的同步功能。

3.1. 同步代码块的实现原理

对于 synchronized 修饰的代码块,JVM 会在同步代码块的入口处插入 monitorenter 指令,在同步代码块的出口处(包括正常退出和异常退出)插入 monitorexit 指令。这两个指令都需要一个引用类型的参数,即要加锁的对象。

  • monitorenter 指令: 当执行 monitorenter 指令时,线程会尝试获取对象的监视器。如果对象的监视器计数器为 0,则该线程可以成功获取监视器,并将计数器设置为 1。如果当前线程已经持有该对象的监视器,则可以重入,监视器计数器会递增。如果其他线程已经持有该对象的监视器,则当前线程会被阻塞,直到持有监视器的线程释放。
  • monitorexit 指令: 当执行 monitorexit 指令时,线程会释放对象的监视器。监视器计数器会递减。当计数器减为 0 时,表示该线程完全释放了监视器,其他被阻塞的线程可以尝试获取该监视器。

字节码示例(伪代码):

// Java 代码
public void method() {synchronized (this) {// 同步代码}
}// 对应的字节码(简化)
public void method();Code:0: aload_01: dup2: astore_13: monitorenter4: // 同步代码开始5: // ...6: // 同步代码结束7: aload_18: monitorexit9: goto 1712: astore_213: aload_114: monitorexit15: aload_216: athrow17: return

从字节码可以看出,monitorexit 指令有两个,一个在正常执行路径的末尾,另一个在异常处理路径的末尾。这确保了即使在同步代码块中发生异常,锁也能够被正确释放,避免死锁。

3.2. 同步方法的实现原理

对于 synchronized 修饰的方法(包括实例方法和静态方法),JVM 并没有使用 monitorentermonitorexit 指令。而是通过方法修饰符 ACC_SYNCHRONIZED 来实现的。当一个方法被 ACC_SYNCHRONIZED 修饰时,JVM 会隐式地为该方法添加同步功能。

当线程调用一个 ACC_SYNCHRONIZED 修饰的方法时,它会尝试获取该方法对应对象的监视器(对于实例方法是实例对象,对于静态方法是 Class 对象)。如果获取成功,则执行方法体;执行完毕后,无论正常退出还是异常退出,都会释放监视器。如果获取失败,则线程会被阻塞。

3.3. synchronized 的优化

在 JDK 1.6 之后,JVM 对 synchronized 进行了大量的优化,引入了偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)和自适应自旋锁(Adaptive Spinning)等技术,以减少锁竞争带来的性能开销。

  • 偏向锁: 适用于只有一个线程访问同步块的场景。当一个线程第一次访问同步块时,会把锁偏向于该线程,后续该线程再次进入同步块时,无需进行同步操作,从而提高性能。如果出现其他线程竞争,偏向锁会升级为轻量级锁。
  • 轻量级锁: 适用于多个线程交替执行同步块的场景,且线程之间没有激烈的竞争。当偏向锁升级或多个线程竞争时,JVM 会尝试使用轻量级锁。它通过 CAS(Compare And Swap)操作在栈帧中创建锁记录(Lock Record)来实现。如果竞争激烈,轻量级锁会升级为重量级锁。
  • 自旋锁: 当一个线程尝试获取锁但失败时,它不会立即阻塞,而是会进行忙循环(自旋)一段时间,看看持有锁的线程是否会很快释放锁。如果很快释放,则避免了线程上下文切换的开销。自适应自旋锁会根据前一次自旋的结果和锁的拥有者的状态来动态调整自旋的次数。
  • 重量级锁: 当锁竞争非常激烈时,轻量级锁会升级为重量级锁。重量级锁是基于操作系统互斥量(Mutex)实现的,线程的阻塞和唤醒都需要操作系统的介入,会涉及到用户态和内核态的切换,开销较大。

这些优化使得 synchronized 在大多数情况下都能提供不错的性能,因此在 Java 并发编程中仍然是一个非常重要的同步工具。

3.4. synchronizedvolatile 的区别

虽然 synchronized 能够保证原子性、可见性和有序性,但它是一个重量级操作。而 volatile 关键字则是一个轻量级的同步机制,它只能保证共享变量的可见性和有序性,不能保证原子性。

特性synchronizedvolatile
原子性保证不保证
可见性保证保证
有序性保证(通过禁止指令重排)保证(通过内存屏障)
适用范围方法和代码块变量
性能相对较重(JDK 1.6 后有优化)相对较轻

通常情况下,如果需要保证复合操作的原子性,或者需要更复杂的同步逻辑,应该使用 synchronized。如果只需要保证变量的可见性和有序性,并且操作本身是原子性的(例如对 intboolean 变量的读写),那么 volatile 是一个更轻量级的选择。

4. 总结

synchronized 关键字作为 Java 并发编程的基石,为多线程环境下的共享资源访问提供了强大的同步保障。它通过对象监视器机制,确保了代码的原子性、可见性和有序性,有效避免了并发问题。

从使用层面来看,synchronized 可以灵活地修饰实例方法、静态方法和代码块,以适应不同粒度的同步需求。修饰实例方法时锁定当前实例对象,修饰静态方法时锁定当前类的 Class 对象,而修饰代码块则可以指定任意对象作为锁,提供了更细致的控制。在选择锁对象时,应遵循最佳实践,避免潜在的死锁或性能问题。

从实现原理来看,synchronized 底层依赖于 JVM 的 monitorentermonitorexit 字节码指令(对于代码块)或 ACC_SYNCHRONIZED 标志(对于方法),结合对象监视器来实现线程的互斥访问。JDK 1.6 之后引入的偏向锁、轻量级锁和自适应自旋锁等优化,显著提升了 synchronized 的性能,使其在大多数场景下都能高效运行。

尽管 java.util.concurrent.locks.Lock 接口提供了更高级和灵活的锁机制,但 synchronized 关键字因其简洁、易用和 JVM 层面优化的特性,在 Java 并发编程中依然占据着不可替代的地位。作为 Java 后端开发者,深入理解 synchronized 的工作原理和正确使用方式,是编写健壮、高效并发应用程序的关键。

http://www.lqws.cn/news/476263.html

相关文章:

  • SAP顾问职位汇总(第25周)
  • SAP金属行业解决方案:无锡哲讯科技助力企业数字化转型与高效运营
  • Vui:轻量级语音对话模型整合包,让交互更自然
  • Python 包管理新选择:全面了解 uv(附 Conda 对比)
  • 931、下降路径最小和
  • 硬件面经-具身机器人通用技术要求
  • Flink SQL Connector Kafka 核心参数全解析与实战指南
  • vue3 el-table 行字体颜色 根据字段改变
  • Flink SourceFunction深度解析:数据输入的起点与奥秘
  • Flink作业三种部署模式:架构、配置与实战应用
  • C++主要知识点详解(引用,内联函数)
  • webpack+vite前端构建工具 - 8 代码分割
  • 生成器函数概念与用法详解
  • 【Clickhouse系列】增删改查:对比mysql
  • Clickhouse官方文档学习笔记
  • FastAPI 入门教程 #06:FastAPI 请求体和数据模型
  • 从零理解鱼眼相机的标定与矫正(含 OpenCV 代码与原理讲解)
  • PostgreSQL全栈部署指南:从零构建企业级高可用数据库集群
  • React Next快速搭建前后端全栈项目并部署至Vercel
  • 《DeepSeek原生应用与智能体开发实践》案例重现
  • 关于数学函数和数据类型扩展的详细讲解(从属GESP二级)
  • 30天pytorch从入门到熟练(day1)
  • Mybatis-Plus支持多种数据库
  • 【机器学习四大核心任务类型详解】分类、回归、聚类、降维智能决策指南
  • 多项目预算如何集中管控与动态调整
  • 将Linux装进口袋: Ubuntu to Go 制作
  • 【Linux】进程间多种通信方式对比
  • Typescript基础
  • 【后端】负载均衡
  • MiniMax-M1 开源,Kimi 深度研究内测,GPT-5 今夏发布,Gemini 2.5 稳定上线!| AI Weekly 6.16-22