Java synchronized关键字详解:从入门到原理(两课时)

Java synchronized关键字详解:从入门到原理(两课时)
在这里插入图片描述

文章目录

在这里插入图片描述

适用对象

本课程适合具备Java基础语法知识,初步了解多线程概念但尚未系统学习并发编程的开发者。无论你是准备面试的求职者,还是希望提升并发编程能力的工程师,这门课程都将为你打下坚实基础。

学习目标

通过两个课时的系统学习,你将能够:

  • 掌握 synchronized的三种使用方式及其区别
  • 理解 synchronized解决线程安全问题的核心原理
  • 剖析 synchronized底层的Monitor机制和锁升级过程
  • 区分 synchronized与volatile、ReentrantLock的适用场景
  • 应对 面试中关于synchronized的高频问题

课程安排

  • 第一课时(约40分钟):基础概念、使用方式、特性详解
  • 第二课时(约50分钟):底层原理、锁升级机制、对比与实战

第一课时:synchronized基础与使用

1.1 从一个线程安全问题开始

让我们先看一个经典问题:两个线程同时对共享变量执行自增操作。

publicclassCounter{privateint count =0;publicvoidincrement(){ count++;// 不是原子操作!}publicintgetCount(){return count;}}// 测试代码publicclassTest{publicstaticvoidmain(String[] args)throwsInterruptedException{Counter counter =newCounter();Thread t1 =newThread(()->{for(int i =0; i <10000; i++) counter.increment();});Thread t2 =newThread(()->{for(int i =0; i <10000; i++) counter.increment();}); t1.start(); t2.start(); t1.join(); t2.join();System.out.println("最终结果: "+ counter.getCount());// 期望20000,实际小于20000}}

运行结果:多次执行,结果总是小于20000,有时甚至相差甚远。

问题根源count++看似一行代码,但在JVM中分解为三条指令:

  1. 从主内存读取count到工作内存(读)
  2. 对count进行加1操作(改)
  3. 将结果写回主内存(写)

当两个线程同时执行这三步时,可能发生交替执行,导致最后写入的值覆盖了之前的计算结果——这就是典型的竞态条件

1.2 synchronized是什么?

synchronized是Java提供的一个关键字,译为“同步”。它可以保证在同一时刻,最多只有一个线程执行被它修饰的代码段,从而解决并发环境下的线程安全问题。

从专业角度说,synchronized实现了互斥访问,它保证了三个关键特性:

特性说明
原子性被保护的代码块要么全部执行,要么完全不执行,不会被打断
可见性一个线程修改共享变量后,其他线程能立即看到最新值
有序性禁止编译器和处理器对同步块内的代码进行指令重排序

1.3 初识synchronized的三种用法

synchronized的使用非常灵活,可以根据需要选择不同的加锁对象。下面我们通过三个典型场景来理解它们的区别。

1.3.1 修饰实例方法
publicclassSyncMethodDemo{privateint count =0;// synchronized修饰实例方法,锁是当前对象thispublicsynchronizedvoidincrement(){ count++;}}

特点

  • 锁对象是调用该方法的实例对象(即this)
  • 同一个对象的多个synchronized实例方法,共用同一把锁
  • 不同对象的方法互不干扰

示意图

线程A ──> obj1.increment() ──> 获取obj1锁 ──> 执行 线程B ──> obj1.increment() ──> 等待obj1锁 线程C ──> obj2.increment() ──> 获取obj2锁 ──> 执行(与A不冲突) 
1.3.2 修饰静态方法
publicclassSyncStaticDemo{privatestaticint count =0;// synchronized修饰静态方法,锁是当前类的Class对象publicstaticsynchronizedvoidincrement(){ count++;}}

特点

  • 锁对象是当前类的Class对象(如SyncStaticDemo.class)
  • 所有实例对象共享同一把类锁
  • 静态方法锁与实例方法锁互不干扰

思考题:如果一个类既有synchronized静态方法,又有synchronized实例方法,两个线程分别调用它们,会互斥吗?

  • 答案:不会。因为一个是类锁,一个是对象锁,两把不同的锁。
1.3.3 修饰代码块
publicclassSyncBlockDemo{privateObject lock =newObject();privateint count =0;publicvoidincrement(){// 同步代码块,锁是指定的lock对象synchronized(lock){ count++;}}publicvoiddecrement(){// 也可以锁thissynchronized(this){ count--;}}}

特点

  • 锁对象可以任意指定,灵活性最高
  • 可以精确控制同步范围,提高并发性能
  • 常用锁对象:this、自定义锁对象、类.class

1.4 深入理解锁的范围

1.4.1 三种锁的对比表格
使用形式锁对象作用范围典型应用场景
修饰实例方法当前实例对象单个实例内保护实例变量
修饰静态方法当前类的Class对象所有实例间保护静态变量
修饰代码块指定的任意对象代码块内细粒度控制
1.4.2 常见面试题解析

问题1:synchronized修饰代码块可以给类加锁吗?
当然可以!只要在括号内传入类名.class即可。例如:

synchronized(SyncBlockDemo.class){// 这是类锁}

问题2:构造方法可以用synchronized修饰吗?
不可以!Java语法规定构造方法不能是同步的。实际上,构造方法本身就是线程安全的,因为JVM会保证在对象初始化完成前,其他线程无法访问该对象。

问题3:静态同步方法和非静态同步方法同时调用会互斥吗?
不会互斥。假设线程A调用实例对象的非静态同步方法,线程B同时调用该对象所属类的静态同步方法,它们持有的是两把不同的锁(对象锁 vs 类锁),因此可以并行执行。

1.5 synchronized的核心特性

1.5.1 可重入性

概念:同一个线程在持有锁的情况下,可以再次获取同一把锁,而不会被阻塞。

publicclassReentrantDemo{publicsynchronizedvoidmethodA(){System.out.println("进入methodA");methodB();// 直接调用,不会死锁}publicsynchronizedvoidmethodB(){System.out.println("进入methodB");}}

为什么需要可重入?
如果没有可重入性,线程在调用methodA获取锁后,调用methodB时发现自己还要申请同一把锁,就会形成死锁。这显然不合理。

实现原理:每个锁关联一个持有线程和一个计数器。线程第一次获取锁时,计数器置为1;同一线程再次获取时,计数器递增;释放一次,计数器递减。直到计数器归零,锁才真正释放。

1.5.2 可见性保证

synchronized不仅能保证原子性,还能保证内存可见性。JMM(Java内存模型)规定:

  • 线程释放锁时,会将工作内存中的共享变量刷新到主内存
  • 线程获取锁时,会清空工作内存,从主内存重新读取共享变量

这就确保了:一个线程修改的共享变量,在释放锁后,其他线程能立即看到最新值。

1.6 第一课时小结

  • synchronized是Java内置的同步关键字,解决原子性、可见性、有序性问题
  • 三种使用方式:实例方法(锁this)、静态方法(锁Class对象)、代码块(锁任意对象)
  • 核心特性:可重入性、可见性、互斥性
  • 不同锁对象决定了不同的同步范围

第二课时:synchronized原理与优化

2.1 从字节码看synchronized的本质

2.1.1 同步代码块的字节码

先看一段简单的同步代码块:

publicclassSyncCodeBlock{publicvoiddoSomething(){synchronized(this){System.out.println("hello");}}}

使用javap -c -v反编译后,关键字节码如下:

 3: monitorenter // 进入同步块,获取监视器锁 4: getstatic // 调用System.out 7: ldc // 加载字符串 9: invokevirtual // 调用println 12: aload_1 13: monitorexit // 退出同步块,释放监视器锁 14: goto 22 // 正常结束跳转 17: astore_2 // 异常处理开始 18: aload_1 19: monitorexit // 异常时也释放锁 20: aload_2 21: athrow // 抛出异常 22: return 

关键发现

  • 同步代码块使用monitorentermonitorexit指令
  • 有两个monitorexit:一个正常退出,一个异常退出,确保锁一定被释放
  • 这就是为什么synchronized即使抛出异常也不会死锁
2.1.2 同步方法的字节码
publicsynchronizedvoidsyncMethod(){System.out.println("hello");}

反编译结果:

public synchronized void syncMethod(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 注意这个标志 Code: 0: getstatic #2 3: ldc #3 5: invokevirtual #4 8: return 

同步方法没有monitorenter/monitorexit指令,而是通过方法表flags中的ACC_SYNCHRONIZED标识。JVM根据这个标识判断是否需要获取锁。

结论:两种实现方式本质上都是获取对象的监视器锁(Monitor Lock),只是表现形式不同。

2.2 Monitor机制深度剖析

2.2.1 什么是Monitor?

Monitor(监视器)是操作系统中用于实现线程同步的机制。Java中的每个对象都与一个Monitor关联,这个关联关系存储在对象头的Mark Word中。

可以把Monitor理解为一个“接待室”,里面有三个关键区域:

 ┌─────────────────────┐ │ Owner │ ← 当前持有锁的线程 ├─────────────────────┤ │ EntryList │ ← 等待获取锁的线程队列 ├─────────────────────┤ │ WaitSet │ ← 调用了wait()的线程队列 └─────────────────────┘ 
2.2.2 Monitor的核心数据结构

在HotSpot虚拟机中,Monitor由C++的ObjectMonitor类实现,关键字段如下:

字段作用
_owner指向当前持有锁的线程
_EntryList等待获取锁的线程队列
_WaitSet调用了wait()的线程队列
_recursions记录锁的重入次数
_count记录线程获取锁的次数
2.2.3 锁获取和释放的完整流程

以两个线程T0和T1竞争锁为例:

步骤1:线程T0尝试获取锁

  • 根据对象头找到对应的ObjectMonitor
  • 检查_owner是否为null
  • 通过CAS操作将_owner设为T0,_count设为1
  • T0获取锁成功,进入同步块执行

步骤2:线程T1尝试获取锁

  • 此时_owner为T0,T1 CAS失败
  • T1会先自旋几次尝试(适应性自旋)
  • 如果T0很快释放锁,T1就成功获取
  • 如果T0未释放,T1进入_EntryList阻塞等待

步骤3:T0释放锁

  • 执行monitorexit_count减1
  • _count变为0,_owner设为null
  • 唤醒_EntryList中的线程(通常是队首线程)
  • T1被唤醒,重新竞争锁

2.3 锁升级:从偏向锁到重量级锁

早期Java的synchronized性能较差,被称为“重量级锁”。JDK 1.6之后引入了一系列优化,让synchronized的性能大幅提升。这就是著名的锁升级机制。

2.3.1 为什么需要锁升级?

想象一下这些场景:

  • 场景A:只有一个线程访问同步代码,根本不需要锁竞争
  • 场景B:两个线程交替访问,几乎没有同时竞争
  • 场景C:多个线程激烈竞争,需要操作系统级别的锁

如果用重量级锁统一处理所有场景,场景A和B会造成不必要的性能开销。锁升级就是让synchronized能够根据竞争激烈程度,动态调整锁的“重量”。

2.3.2 锁的四种状态

从低到高,锁有四种状态:

  1. 无锁状态
  2. 偏向锁
  3. 轻量级锁
  4. 重量级锁

锁可以升级,但不能降级(除了一次GC清理)。

2.3.3 偏向锁(Biased Locking)

适用场景:只有一个线程反复获取同一把锁。

原理

  • 第一次获取锁时,通过CAS将线程ID记录到对象头
  • 之后该线程再来时,只需检查对象头中是否是自己ID
  • 如果是,直接进入,无需任何同步操作

示例代码

publicclassBiasedLockDemo{privatestaticList<Integer> list =newVector<>();// Vector的方法都是同步的publicstaticvoidmain(String[] args){for(int i =0; i <100; i++){ list.add(i);// 同一线程反复获取锁}}}

优点:几乎没有加锁开销,性能极高。
缺点:一旦有其他线程尝试竞争,偏向锁立即撤销并升级。

注意:从JDK 15开始,偏向锁被标记为废弃,未来可能移除。因为在实际应用中,它的性能提升有限,且维护成本高。

2.3.4 轻量级锁(Lightweight Locking)

适用场景:两个线程交替执行同步块,没有真正的竞争。

原理

  • 线程在自己的栈帧中创建Lock Record,存储对象头的拷贝
  • 通过CAS尝试将对象头指向Lock Record
  • 如果成功,获取轻量级锁
  • 如果失败,说明有竞争,升级为重量级锁

轻量级锁的解锁

  • 通过CAS将对象头恢复为原Mark Word
  • 如果恢复成功,解锁完成
  • 如果失败,说明已经升级为重量级锁,走重量级锁解锁流程
2.3.5 重量级锁(Heavyweight Locking)

适用场景:多个线程激烈竞争。

原理

  • 线程进入_EntryList队列阻塞
  • 依赖操作系统Mutex Lock实现
  • 涉及用户态和内核态切换,开销较大
2.3.6 锁升级流程总结
 偏向锁 │ │ 有其他线程尝试获取 ↓ 轻量级锁 │ │ 有真实竞争 ↓ 重量级锁 

2.4 synchronized与volatile的对比

特性synchronizedvolatile
原子性✅ 保证❌ 不保证
可见性✅ 保证✅ 保证
有序性✅ 保证✅ 保证(禁止重排序)
用法修饰方法或代码块修饰变量
性能较重(但有优化)极轻
适用场景复合操作单一变量状态标志

volatile适用示例

publicclassVolatileDemo{privatevolatileboolean flag =true;// 只作为状态标志publicvoidstop(){ flag =false;// 简单写操作}publicvoidrun(){while(flag){// 执行任务}}}

2.5 synchronized与ReentrantLock的对比

2.5.1 对比表格
对比维度synchronizedReentrantLock
实现方式JVM关键字Java API(基于AQS)
锁释放自动释放必须手动unlock
公平性非公平可设置公平/非公平
可中断不支持支持lockInterruptibly()
超时获取不支持支持tryLock(timeout)
尝试获取不支持支持tryLock()
条件变量一个等待集多个Condition
锁状态查询不支持支持查询持有线程等
2.5.2 如何选择?

优先使用synchronized的情况:

  • 同步逻辑简单,不需要高级功能
  • 希望代码简洁不易出错
  • JVM持续优化,性能已很好

选择ReentrantLock的情况:

  • 需要公平锁
  • 需要尝试获取锁或超时获取
  • 需要可中断的锁获取
  • 需要多个条件变量(如生产者-消费者模式)
  • 高竞争场景需要更精细控制
2.5.3 ReentrantLock使用示例
ReentrantLock lock =newReentrantLock(true);// 公平锁Condition notFull = lock.newCondition();Condition notEmpty = lock.newCondition();// 标准使用模式 lock.lock();try{// 临界区代码while(条件不满足){ notEmpty.await();// 等待}// 执行操作 notFull.signal();}finally{ lock.unlock();// 必须释放}

2.6 实战:线程安全的单例模式

综合运用synchronized,实现几种经典的单例模式。

2.6.1 饿汉式(线程安全)
publicclassEagerSingleton{privatestaticfinalEagerSingleton INSTANCE =newEagerSingleton();privateEagerSingleton(){}publicstaticEagerSingletongetInstance(){return INSTANCE;}}
2.6.2 懒汉式(同步方法版)
publicclassLazySingleton{privatestaticLazySingleton instance;privateLazySingleton(){}publicstaticsynchronizedLazySingletongetInstance(){if(instance ==null){ instance =newLazySingleton();}return instance;}}
2.6.3 双重检查锁(DCL)
publicclassDCLSingleton{// volatile保证可见性和禁止重排序privatestaticvolatileDCLSingleton instance;privateDCLSingleton(){}publicstaticDCLSingletongetInstance(){if(instance ==null){synchronized(DCLSingleton.class){if(instance ==null){ instance =newDCLSingleton();}}}return instance;}}

为什么需要volatile?
instance = new DCLSingleton()不是原子操作,可能发生指令重排序。如果不加volatile,其他线程可能拿到一个未初始化完成的对象。这是DCL的关键细节。

2.7 第二课时小结

  • synchronized底层基于Monitor实现,字节码层面使用monitorenter/monitorexit或ACC_SYNCHRONIZED
  • 锁升级机制(偏向锁→轻量级锁→重量级锁)大幅提升了性能
  • synchronized保证原子性、可见性、有序性,是全面的同步工具
  • 与volatile相比,synchronized更重量但功能更全
  • 与ReentrantLock相比,synchronized简单易用,ReentrantLock功能更丰富
  • 实际开发中,根据场景选择合适的同步机制

课程总结

通过两个课时的学习,我们全面掌握了synchronized关键字:

  • 第一课时:从线程安全问题出发,学习了synchronized的三种使用方式、锁的范围、可重入性等基础概念
  • 第二课时:深入底层,剖析了Monitor机制、锁升级流程,并对比了volatile和ReentrantLock

synchronized作为Java最基础的同步工具,虽然简单,但背后的原理并不简单。理解它的实现机制,不仅能帮助我们写出更正确的并发程序,还能在面试中脱颖而出。

课后思考题

  1. 如果一个线程在同步块中抛出异常,锁会自动释放吗?为什么?
  2. 偏向锁在JDK 15中被废弃的原因是什么?谈谈你的理解。
  3. 如何用synchronized实现一个阻塞队列?
  4. 为什么说synchronized是“悲观锁”,而CAS是“乐观锁”?

Read more

【Java 开发日记】我们来说一下无锁队列 Disruptor 的原理

【Java 开发日记】我们来说一下无锁队列 Disruptor 的原理

目录 一、为什么需要 Disruptor?—— 背景与问题 二、核心设计思想 三、核心组件与原理 1. 环形缓冲区(Ring Buffer) 2. 序列(Sequence) 3. 序列屏障(Sequence Barrier) 4. 等待策略(Wait Strategy) 5. 事件处理器(EventProcessor) 6. 生产者(Producer) 四、工作流程示例(单生产者 -> 单消费者) 五、多消费者与依赖关系 六、总结:Disruptor 高性能的秘诀 一、为什么需要 Disruptor?—— 背景与问题 在高并发编程中,传统的队列(如 java.

By Ne0inhk
Java中的反射机制详解:从原理到实践的全面剖析

Java中的反射机制详解:从原理到实践的全面剖析

文章目录 * 摘要 * 第一章 反射机制概述 * 1.1 什么是反射? * 1.2 反射的江湖地位:为何需要它? * 1.3 反射的优缺点 * 第二章 反射的基石:Class类与类加载 * 2.1 万物皆对象:Class对象 * 2.2 获取Class对象的三种方式 * 2.3 类加载的幕后故事 * 第三章 解剖类:反射的核心API * 3.1 操作构造方法(Constructor):创建对象 * 3.2 操作字段(Field):访问与修改属性 * 3.3 操作方法(Method):动态调用 * 第四章 深入进阶:反射的高级特性 * 4.1

By Ne0inhk
【Java 开发日记】为什么要有 time _wait 状态,服务端这个状态过多是什么原因?

【Java 开发日记】为什么要有 time _wait 状态,服务端这个状态过多是什么原因?

目录 为什么要有 TIME_WAIT 状态? 原因一:可靠地终止TCP连接(确保最后的ACK能到达对方) 原因二:让旧连接的重复报文段在网络中自然消失(防止影响新连接) 服务端 TIME_WAIT 状态过多是什么原因? 原因一:服务端使用了短连接,并且是它主动关闭连接 原因二:客户端的非正常行为 原因三:负载均衡器的健康检查 总结 面试回答 为什么要有 TIME_WAIT 状态? TIME_WAIT,俗称2MSL等待状态,是TCP连接主动关闭一方(通常是客户端,但也可能是服务端)在发送最后一次ACK确认报文后,会进入的一个状态。它需要等待2倍的最大报文段生存时间后,才会最终进入CLOSED状态,释放连接资源。 设计TIME_WAIT状态主要有两个核心原因,它们是确保TCP协议可靠性的基石: 原因一:可靠地终止TCP连接(确保最后的ACK能到达对方) 这是最主要的原因。让我们回顾一下TCP四次挥手的正常流程: 1. 主动关闭方(假设为A)

By Ne0inhk
2025 最新|超级智能体开发全攻略:从理论到 Spring AI 落地

2025 最新|超级智能体开发全攻略:从理论到 Spring AI 落地

一、什么是 AI 智能体? AI 智能体(Agent)是一类以大语言模型为核心,集成记忆、知识库、工具调用等能力的智能系统。它能感知环境信息、自主推理决策、制定执行计划,最终达成特定目标,如同拥有自主意识的数字助手。 与普通 AI 大模型相比,智能体的核心优势在于 “自主性”,具体体现在: 1. 多模态环境感知:通过文本、图像、语音等多渠道获取信息,精准理解场景与需求 2. 复杂任务拆解:将大目标拆分为可执行的子任务,规划最优执行路径 3. 工具扩展能力:灵活调用 API、搜索引擎、代码执行器等外部工具,突破模型原生限制 4. 链式推理思维:通过思维链(CoT)逐步分析问题,提升复杂决策准确率 5. 长效记忆机制:留存历史交互数据,实现连贯对话与经验复用 6.

By Ne0inhk