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

synchronized到底锁的是谁?一文看懂Java对象锁的核心原理

一、核心结论速览

synchronized可以修饰方法,也可以修饰代码块,锁的对象取决于具体用法:

使用方式锁的对象
修饰方法非静态方法当前实例对象(方法调用者)
静态方法方法所在的Class对象
修饰代码块synchronized括号中传入的对象

二、辅助案例1:非静态方法 —— 锁的是方法调用者

2.1 基础代码(无 synchronized)

public class SynchronizedTest1 { public static void main(String[] args) { Data data = new Data(); Thread t1 = new Thread() { @Override public void run() { data.fun1(); } }; t1.start(); TimeUnit.SECONDS.sleep(1); Thread t2 = new Thread() { @Override public void run() { data.fun2(); } }; t2.start(); } } class Data { public void fun1() { TimeUnit.SECONDS.sleep(3); System.out.println("1...."); } public void fun2() { System.out.println("2...."); } }

输出结果:先输出2,约2秒后输出1。

执行流程:

  1. main 线程启动,main方法的栈帧入栈。

  2. 程序首次使用Data类,进行类加载,在堆中创建Data对象,main栈帧中的局部变量data存储该对象的引用地址。

  3. 通过匿名内部类的方式创建线程t1并加入到就绪队列中,等到t1获得CPU执行权后,调用data.fun1()方法,但fun1()需要等待3秒才输出。

  4. 与此同时,主线程继续执行,休眠1秒后,通过匿名内部类的方式创建线程t2并加入到就绪队列中。

  5. 此时,由于t1在休眠,CPU是空闲的,所以线程t2获得执行权,调用data.fun2()方法并直接输出。

  6. 3秒后,t1的sleep时间到,进入就绪状态。再次获得CPU执行权后,继续执行后续代码,进行输出。


2.2 场景1:fun1() 和 fun2() 都加 synchronized

class Data { public synchronized void fun1() { ... } public synchronized void fun2() { ... } }

输出结果:等待3秒,输出1,紧接着输出2。

执行流程:

  1. main线程启动,main方法的栈帧入栈。

  2. 程序首次使用Data类,进行类加载,在堆中创建Data对象,main栈帧中的局部变量data存储该对象的引用地址。

  3. 通过匿名内部类的方式创建线程t1并加入到就绪队列中,等到t1获得CPU执行权后,t1尝试获取方法调用者data的锁,此时未被占用,t1成功获取锁,开始执行。在休眠3s时,t1并不会释放锁。

  4. 与此同时,主线程休眠1秒后,通过匿名内部类的方式创建线程t2并加入到就绪队列中。

  5. 此时,由于t1在休眠,CPU是空闲的,所以线程t2获得执行权,但是锁仍在t1中,t2无法获取,所以进入阻塞状态,等待t1释放锁后重新竞争。

  6. 3s后,t1休眠时间到,进入就绪队列,再次获得CPU执行权后,继续执行后续代码,进行输出1,完成之后释放锁,此时t2被唤醒,成功获得锁,执行输出2,完成后释放锁。


2.3 场景2:fun1() 有 synchronized,fun2() 没有 synchronized

class Data { public synchronized void fun1() { ... } public void fun2() { ... } }

输出结果:先输出2,约2秒后输出 1。

原因:fun2()没有被synchronized修饰,调用时无需获取锁,直接执行,不受t1持有的锁影响。


2.4 场景3:两个Data对象,t1调用data1.fun1(),t2调用data2.fun2(),两个方法都有 synchronized

Data data1 = new Data(); Data data2 = new Data(); // t1 → data1.fun1() // t2 → data2.fun2()

输出结果:先输出2,约2秒后输出1。

执行流程:

  1. main线程启动,main方法的栈帧入栈。

  2. 程序首次使用Data类,进行类加载,在堆中创建data1对象和data2对象,main栈帧中的局部变量data1和data2分别存储各自对象的引用地址。

  3. 通过匿名内部类的方式创建线程t1并加入到就绪队列中。等到t1获得CPU执行权后,t1尝试获取方法调用者data1的锁,此时未被占用,t1成功获取锁,开始执行。在休眠3s时,t1并不会释放锁。

  4. 与此同时,主线程休眠1秒后,通过匿名内部类的方式创建线程t2并加入到就绪队列中。

  5. 此时,由于t1在休眠,CPU是空闲的,所以线程t2获得CPU执行权,并尝试获取方法调用者data2的锁,此时未被占用,t2成功获取到锁,执行并直接输出,完成后释放锁。

  6. 3s后,t1休眠时间到,进入就绪队列,再次获得CPU执行权后,继续执行后续代码,进行输出1,完成之后释放锁。

结论:t1和 t2锁的是不同的对象(data1和data2),互不干扰。这证明了synchronized修饰非静态方法时,锁的是方法调用者(当前实例对象),而不是类。

三、辅助案例2:静态方法 —— 锁的是 Class 对象

3.1 Scenario 4: fun1() is static synchronized, fun2() is regular synchronized

class Data { public static synchronized void fun1() { ... } public synchronized void fun2() { ... } } Data data1 = new Data(); Data data2 = new Data(); // t1 → data1.fun1() // t2 → data2.fun2()

输出结果:先输出2,约2秒后输出1。

原因:fun1()是静态方法,锁的是Data.class对象;fun1()是普通方法,锁的是data2实例对象。这是两把完全独立的锁,不存在资源竞争。


3.2 场景5:两个线程都调用 static synchronized fun1()

class Data { public static synchronized void fun1() { sleep(3); System.out.println("1...."); } } Data data1 = new Data(); Data data2 = new Data(); // t1 → data1.fun1() // t2 → data2.fun1()

输出结果:等待3秒,输出1(来自 t1),再等待3秒,输出1(来自 t2)。

原因:因为synchronized修饰fun1静态方法锁的是Data.class对象,虽然两个线程中方法调用者不同,但所有实例共享同一把类锁,所以说会发生资源争抢。线程t2需要等待t1执行完毕释放锁之后才能拿到锁,执行输出。

结论:虽然两个线程调用的是不同的实例(data1和data2),但static synchronized方法锁的是Data.class 类对象,所有实例共享同一把类锁,因此必须排队执行。这证明了synchronized修饰静态方法时,锁的是类,而不是方法调用者。

四、辅助案例3:同步代码块 —— 锁的是括号中传入的对象

4.1 基础代码:无synchronized的5个线程

class Data2 { public void fun() { System.out.println("start"); TimeUnit.SECONDS.sleep(3); System.out.println("end"); } } // main 中启动 5 个线程,共享同一个 Data2 对象 for (int i = 0; i < 5; i++) { new Thread(() -> data.fun()).start(); }

输出结果:5次 "start" → 间隔约3秒 → 5次 "end"。

执行流程:

  1. main线程启动,main方法的栈帧入栈。

  2. 程序首次使用Data类,进行类加载,在堆中创建Data对象,main栈帧中的局部变量data存储该对象的引用地址。

  3. 进入for循环,i=0时,通过匿名内部类的方式创建线程t1并加入到就绪队列中,等待CPU调度。

  4. 循环继续,i=1~4时,依次创建t2、t3、t4、t5并启动,它们都进入就绪状态,等待CPU调度。

  5. CPU会快速依次调度5个线程执行run()方法。

  6. 每个线程执行data.fun()时。先输出 "start"(5个线程几乎同时输出),然后调用Thread.sleep(3000),5个线程几乎同时进入休眠状态。

  7. 3秒后,5个线程的sleep时间几乎同时到期,它们又转为就绪状态。

  8. CPU依次调度这5个线程,每个线程继续执行后续代码,输出 "end"(5个"end"几乎同时输出)。


4.2 场景6:synchronized 代码块锁 Integer 缓存对象

Integer num = 1; // 指向 Integer 缓存池中的对象 class Data3 { public void fun(Integer num) { synchronized (num) { // 锁的是 num 对象 System.out.println("start"); TimeUnit.SECONDS.sleep(3); System.out.println("end"); } } } // main 中启动 5 个线程,传入同一个 num for (int i = 0; i < 5; i++) { new Thread(() -> data.fun(num)).start(); }

输出结果:start → 3s → end → start → 3s → end ...(重复 5 轮,总耗时约 15 秒)

流程分析:

  1. main线程启动,main方法的栈帧入栈。Integer num = 1指向的是Integer缓存池中的对象(-128~127范围内的值会被缓存),因此5个线程传入的是同一个Integer对象

  2. 进入for循环,i=0时,创建 data3 对象,通过匿名内部类的方式创建线程t1并加入到就绪队列中,等待CPU调度。

  3. 循环继续,i=1~4时,依次创建data3 对象并创建启动线程t2、t3、t4、t5,它们都进入就绪状态,等待CPU调度。

  4. t1获得CPU,执行data.fun(num),此时t1获取num锁,先输出 "start"(5个线程几乎同时输出),然后调用Thread.sleep(3000)进入休眠状态。但此时t1持有num锁且不释放。

  5. 与此同时,线程t2、t3、t4、t5依次获得CPU,执行data.fun(num),获取num锁时发现被t1持有,所以线程阻塞,等待锁释放。

  6. 3秒后,t1的sleep时间到期,转为就绪状态,获得CPU后输出 "end",释放锁。

  7. 接下来。t2线程被唤醒,成功获得锁,输出“start”,3秒sleep结束后,输出“end”,释放锁。
    t3到t5线程以此类推。

关键注意:虽然每轮循环都new Data3(),但锁的对象是num,而不是Data3实例。由于num是同一个 Integer 缓存对象,所有线程竞争的是同一把锁,因此串行执行。


五、总结

synchronized 的锁对象可以归纳为以下三点:

  1. 修饰非静态方法→ 锁的是方法调用者(当前实例对象)

  2. 修饰静态方法→ 锁的是Class对象(类锁)

  3. 修饰代码块→ 锁的是括号中传入的对象

加锁原则:加锁会带来性能损耗,因此锁的范围应尽可能小、尽可能精准,只锁需要同步的代码块,避免锁住整个方法。

http://www.jsqmd.com/news/1301158/

相关文章:

  • Continue开源AI编程助手:架构深度解析与技术设计哲学
  • 泾县快速打井公司推荐,快速钻井公司哪家好?2026避坑指南:4个常见坑+5条硬标准 - geo88
  • 基于PatchTST模型的多变量时序多步预测 可一键替换Excel数据集(完整Python代码数据)
  • 智能写作工具PaperXie:提升本科毕业论文效率与质量
  • 2026年7月青岛全屋定制品牌5大推荐:从高定整案到品质优选,一份给挑剔者的决策指南 - 优企甄选
  • 唐山路北区室内除异味、甲醛检测治理机构甄选指南,雅馨居环保专业度全面解析 - 专注室内空气检测治理
  • 外贸定制建站哪家强?专业对比分析帮你选对方向 - 米諾
  • 2026年7月广州全屋定制品牌推荐:从豪宅到平层,五大精选深度对比 - 优企甄选
  • SpringBoot设计师约稿平台开发与架构设计
  • 苏州小微企业获客难,GEO帮你打造长期稳定自然流量
  • 技术专访|GEO高级优化师、落地工程师罗长才:以太阳马戏、Franco Dragone工程体系参照,解构GEO全域落地的标准化工程范式
  • `try-finally` 里的 `return`:为什么 `finally` 会悄悄改掉返回值、吞掉异常
  • 猫抓浏览器扩展:5个核心功能揭秘,让你成为网页资源管理大师
  • 如何用indoor3D库快速构建专业级室内3D地图:完整指南
  • 3个触控板手势:在Mac上实现Windows风格的Alt-Tab窗口切换终极指南
  • 解锁iOS应用自由:AltStore如何让你在不越狱的情况下玩转第三方软件
  • 实战指南:GoogleTranslate_IPFinder 一键解决谷歌翻译访问难题
  • 2026年北京实验室认可咨询机构排行 多维度适配各类实验室需求 - 产品推荐官
  • 2026年澳洲集运行业现状与企业采购选型专业指南 - 产品推荐官
  • 行测图形推理一笔画问题:奇点判定与秒杀技巧
  • 2026年国内主流不锈钢管厂家实测维度评测汇总 - 起跑123
  • GLM-5.1大模型架构优化与工业级部署实践
  • 当AI开始学会欺骗
  • Lilian Weng 离职创业公司重返 OpenAI:健康危机下的 AI 安全人才争夺战
  • 资质再扩容!金领玮业新增老年人能力评估师、医疗护理员、保健按摩师等三项职业技能认定资质 - 全域品牌推荐
  • 探索Zotero-OCR:如何用智能文字识别重新定义学术文献管理体验
  • 小升初数学思维训练:94集视频+PDF材料系统提升代数几何能力
  • 3步破解:Windows ADB Fastboot驱动安装的终极解决方案
  • 【物理AI】智慧园区数字孪生能解决什么问题?
  • 2026年全国固化地坪厂家排行 助力精准选品决策 - 产品推荐官