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

一文读懂 Skills:什么是 Skills?如何使用?以及如何用 Skill 生成一个 Java 方法

一文读懂 Skills:什么是 Skills?如何使用?以及如何用 Skill 生成一个 Java 方法

一、什么是 Skills? 在 AI 与智能编程工具中,Skill(技能) 可以理解为: 对某一类任务的“能力封装”或“可复用指令单元” 它不是一次性的 Prompt,而是经过抽象、总结、可以**反复使用 **的能力描述。 用一句话解释 Skill * Prompt:一次性提问 * Skill:可以反复调用的“能力模板” 例如: * 生成一个 Java Getter 方法 * 根据接口文档生成 Controller 代码 * 把自然语言需求转换成 SQL * 为已有方法补充 Javadoc 注释 这些都可以被定义为一个 Skill。 二、为什么要使用 Skills? 在实际开发中,常见问题包括: * 每次都要重新描述需求 * AI 输出风格不统一

By Ne0inhk
Java 大视界 -- Java 大数据机器学习模型在金融衍生品复杂风险建模与评估中的应用(244)

Java 大视界 -- Java 大数据机器学习模型在金融衍生品复杂风险建模与评估中的应用(244)

💖亲爱的朋友们,热烈欢迎来到 青云交的博客!能与诸位在此相逢,我倍感荣幸。在这飞速更迭的时代,我们都渴望一方心灵净土,而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识,也期待你毫无保留地分享独特见解,愿我们于此携手成长,共赴新程!💖 全网(微信公众号/ZEEKLOG/抖音/华为/支付宝/微博) :青云交 一、欢迎加入【福利社群】 点击快速加入1:青云交技术圈福利社群(NEW) 点击快速加入2:ZEEKLOG 博客之星 创作交流营(NEW) 二、本博客的精华专栏: 1. 大数据新视界专栏系列:聚焦大数据,展技术应用,推动进步拓展新视野。 2. Java 大视界专栏系列(NEW):聚焦 Java 编程,细剖基础语法至高级框架。展示 Web、

By Ne0inhk
Java各大厂实习面试题面经新鲜出炉!---壹

Java各大厂实习面试题面经新鲜出炉!---壹

🌟 Hello,我是Java学习通! 🌈 在彩虹般绚烂的技术栈中,我是那个永不停歇的色彩收集者。 🦋 每一个优化都是我培育的花朵,每一个特性都是我放飞的蝴蝶。 🔬 每一次代码审查都是我的显微镜观察,每一次重构都是我的化学实验。 🎵 在编程的交响乐中,我既是指挥家也是演奏者。让我们一起,在技术的音乐厅里,奏响属于程序员的华美乐章。 目录 1.MySQL事务机制(阿里巴巴) 2.有做过SQL优化的实现么(阿里巴巴) 3.Nacos底层是如何实现注册中心功能的:(阿里巴巴) 4.RocketMQ如何持久化(阿里巴巴) 5.介绍一下websocket(阿里巴巴) 6.如何判断是http是长连接还是短连接,怎么设置长连接(阿里巴巴) 7.HashMap的实现原理(快手) 8.HashMap承载的元素越来越少,什么时候会退化成链表,为什么两者设置的这个值不对称(快手) 9.mysql和redis的一致性怎么保证的(快手) 10.数据库有哪些隔离级别 默认的隔离级别是什么(快手) 11.缓存击穿

By Ne0inhk
Spring Boot 机制四: AOP 代理机制源码级深度解析(JDK / CGLIB 全链路)

Spring Boot 机制四: AOP 代理机制源码级深度解析(JDK / CGLIB 全链路)

博主社群介绍: ① 群内初中生、高中生、本科生、研究生、博士生遍布,可互相学习,交流困惑。 ② 热榜top10的常客也在群里,也有数不清的万粉大佬,可以交流写作技巧,上榜经验,涨粉秘籍。 ③ 群内也有职场精英,大厂大佬,跨国企业主管,可交流技术、面试、找工作的经验。 进群免费赠送写作秘籍一份,助你由写作小白晋升为创作大佬,进群赠送ZEEKLOG评论防封脚本,送真活跃粉丝,助你提升文章热度。 群公告里还有全网大赛约稿汇总/博客提效工具集/ZEEKLOG自动化运营脚本 有兴趣的加文末联系方式,备注自己的ZEEKLOG昵称,拉你进群,互相学习共同进步。 文章目录 * Spring Boot 机制四: AOP 代理机制源码级深度解析(JDK / CGLIB 全链路) * 目录 * 1. Spring Boot AOP 的本质是什么? * 2. JDK vs CGLIB:

By Ne0inhk