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

Java-synchronized学习总结

面试官​:
“我看你简历里提到熟悉 Java 多线程,能简单说一下 synchronized 的底层原理吗?比如锁是怎么一步步升级的?”

你(回答思路)​​:
“好的,我理解的锁升级过程大概是这样的:
一开始对象刚创建的时候,其实是没有锁的,这时候如果有线程第一次进入同步代码块,JVM 会给这个对象加一个 ​偏向锁,相当于在对象头里贴个标签说‘这个锁现在归我(线程)了’。这样做的好处是,如果之后还是同一个线程反复进出同步代码块,就不需要再走复杂的加锁流程,直接刷脸(线程ID)就能进,省时间。

不过如果这时候有其他线程也想来抢这个锁,JVM 就会发现‘哎,这标签不对啊,不是同一个人’,这时候就会把偏向锁升级成 ​轻量级锁。轻量级锁的竞争有点像‘抢凳子’游戏,线程会通过 CAS 操作(可以理解为一种乐观的抢锁方式)去争抢锁的拥有权,如果抢成功了就继续执行,抢失败了会稍微等一下(自旋),看看对方会不会很快释放锁。

但是如果自旋了一段时间还没抢到,或者同时抢锁的线程太多,JVM 就会说‘这样抢太乱了,得排个队’,这时候锁就会升级成 ​重量级锁,也就是依赖操作系统的机制来管理线程的阻塞和唤醒。不过这个级别的锁开销就比较大了,线程会被挂起,需要等操作系统来调度。”

​(加分话术)​​:
“我之前在写一个图片加载库的缓存模块时,就遇到过多个线程同时读写 LruCache 的场景,当时用了 synchronized 来保证线程安全。后来看性能分析工具发现,在低并发情况下锁基本保持在轻量级状态,但在加载大量图片时确实会升级到重量级锁,这时候就需要考虑优化锁的粒度了。”

扩展追问:

你:"这个我记得去年优化图片缓存时遇到过!比如我们用LruCache做内存缓存,多个加载线程同时操作就需要同步。JVM的锁升级有点像地铁安检——

  1. 偏向锁​:早上人少时(单线程),安检员记住你的脸直接放行(对象头记录线程ID),省去查包步骤
  2. 轻量级锁​:高峰期来了几个人(轻度竞争),大家轮流快速开包检查(CAS自旋),谁先抢到谁先过
  3. 重量级锁​:春运级别的拥挤(高并发),只能排队等叫号(线程进入阻塞队列),由安检系统统一调度

实际在缓存模块里,低并发时锁保持在轻量级,但当用户快速滑动图片墙时,锁会升级到重量级。我们用Systrace抓帧发现卡顿,后来把大锁拆分成多个Bucket锁(比如按图片URL哈希分片),竞争减少后性能提升了40%"


面试官​:
“你刚才提到 CAS,能具体说说它的缺点吗?比如有没有遇到过什么问题?”

你(回答思路)​​:
“CAS 虽然高效,但确实有坑。比如经典的 ​ABA 问题​:假设我银行卡余额是 100 块,这时候我要转账 50 块,系统先去读余额是 100,然后计算新余额应该是 50。但在计算的时候,如果有个线程先转出 100 又转回 100(比如充值退款),这时候余额看起来还是 100,但实际已经变过两次了。如果用普通的 CAS 操作,就会误认为余额没变,直接扣款,导致资金错误。

解决办法的话,可以用 AtomicStampedReference 这个类,它会给数据加一个版本号,就像快递单号一样,每次修改版本号都会变,这样就能识别出数据是否被‘偷偷改过’。”

​(情景化举例)​​:
“比如我之前用 CAS 实现过一个下载任务队列,多个线程同时操作队列头尾节点。有一次测试发现任务状态偶尔错乱,后来发现就是因为没有处理 ABA 问题,加上版本号后才解决。”


场景一:CAS的ABA问题
面试官:"你说用AtomicInteger优化过计数器,遇到过ABA问题吗?"

你:"真踩过这个坑!做消息未读数时,曾发现计数偶尔错乱。比如:

  • 线程A读取未读数是10
  • 线程B先减到9,又加回10
  • 线程A的CAS操作误以为数值没变,直接覆盖

后来改用AtomicStampedReference,给数值加了个版本号,就像快递单号一样。每次修改版本号+1,这样即使数值相同,版本号变了也能识别出来。代码大概是这样的:"

val atomicRef = AtomicStampedReference(0, 0)
// 更新时同时校验值和版本号
atomicRef.compareAndSet(expectValue, newValue, expectStamp, expectStamp + 1)

"现在消息模块上线两年,再没出现过计数异常"


场景三:ReentrantLock实战
面试官:"什么时候会用ReentrantLock代替synchronized?"

你:"比如我们做文件下载管理时,需要更灵活的控制:

  1. 超时机制​:尝试获取锁最多等500ms,防止死锁
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {try { /* 写文件 */ } finally { lock.unlock() }
} else {Log.w("下载超时,跳过本次写入");
}
  1. 公平性​:多个下载任务按请求顺序获取锁(虽然性能差些但更公平)
  2. 条件变量​:暂停/继续下载时用Condition做线程通信

对比synchronized,ReentrantLock就像手动挡汽车——控制更精细,但需要自己处理换挡(lock/unlock)。我们有个下载队列模块,用Condition实现了流量控制:当移动网络下同时下载数超过3个,其他线程会进入等待状态"


场景三:Android主线程同步
面试官:"主线程更新UI也要考虑线程安全吗?"

你:"当然!虽然Android规定UI操作必须在主线程,但像SurfaceView这种特殊控件,如果在onDraw里被多个线程修改绘图数据,依然会崩溃。我们项目里封装过一个游戏引擎组件,遇到这样的问题:

  • 现象​:快速切屏时偶现Canvas空指针
  • 分析​:子线程渲染完数据,主线程绘制时数据被另一个线程清除
  • 解决​:用双重锁校验+volatile保证可见性
private volatile Bitmap currentFrame;void updateFrame(Bitmap newFrame) {synchronized (this) {if (newFrame != null) {currentFrame = newFrame; // volatile写}}
}// 主线程onDraw
protected void onDraw(Canvas canvas) {Bitrame localFrame = currentFrame; // volatile读if (localFrame != null) {canvas.drawBitmap(localFrame, ...);}
}

"后来还加了个渲染队列,用Handler的MessageQueue天然顺序执行特性,避免加锁开销"


场景四:死锁排查案例
面试官:"遇到死锁你们怎么快速定位?"

你:"去年支付模块上线后,客服反馈有些订单卡在支付中状态。我们用adb shell dumpsys thread导出线程状态,发现两个线程互相持有对方需要的锁:

  • 线程A:先锁支付订单对象 → 等数据库连接锁
  • 线程B:先锁数据库连接 → 等订单对象锁

解决三步走​:

  1. 统一锁顺序​:所有业务必须先拿数据库锁再拿业务对象锁
  2. 设置超时​:用tryLock(2, SECONDS)替代裸锁,超时后回滚事务
  3. 添加监控​:在锁入口埋点,当等待时间>1s触发告警

这个案例让我明白:​锁顺序的重要性不亚于锁本身,就像交通规则要规定靠左还是靠右行驶"


场景五:Handler中的锁应用
面试官:"Handler机制里有用到锁吗?"

你:"当然!MessageQueue的enqueueMessage方法就有synchronized锁:

boolean enqueueMessage(Message msg, long when) {synchronized (this) { // 保证同一时间只有一个线程入队//...}
}

这解释了为什么Handler能在多线程下安全切换消息。我们做视频帧渲染时,子线程通过Handler发Bitmap到主线程,synchronized保证了帧顺序不会错乱。但要注意:

  • 避免在同步块内做耗时操作(比如解码视频),否则会阻塞其他消息
  • 必要时用AsyncTask+锁机制做双缓冲:后台线程写备用缓冲区,主线程交换缓冲区时加锁"

​​面试官​:
“如果让你优化一个高并发的列表滚动加载功能,怎么避免 synchronized 带来的性能问题?”

你(回答思路)​​:
“首先我会分析锁的竞争情况。如果是读多写少的场景,可以考虑用 ReentrantReadWriteLock 替换,让读操作共享锁,写操作独占锁。比如列表滚动时频繁读取缓存数据,用读锁可以大幅提升并发能力。

另外,如果是针对某个共享变量(比如当前加载的页码),可以用 AtomicInteger 这种原子类代替锁,它的底层就是 CAS 机制,性能更高。

最后还可以尝试缩小锁的范围,比如把同步代码块从整个数据加载方法缩小到只包裹真正需要线程安全的操作(比如更新列表数据的代码段),避免长时间持锁阻塞其他线程。”

​(体现思考)​​:
“不过这些优化都需要结合具体场景,比如之前我做过一个消息列表的预加载功能,一开始用 synchronized 导致快速滑动时卡顿,后来改成 ReentrantReadWriteLock 后帧率提升了 30%。”


面试官​:
“在 Java 中,synchronized 可以修饰静态方法、实例方法和代码块,你能具体说说它们的区别和应用场景吗?”

​:
“好的。synchronized 的不同用法其实对应不同的锁对象和作用范围。

  1. 静态方法加锁​:比如 public static synchronized void method() { ... },这时候锁的是类的 Class 对象。比如一个工具类中的全局配置更新方法,需要保证多线程下只能有一个线程修改配置,这时候静态同步方法就能避免并发问题。
  2. 实例方法加锁​:比如 public synchronized void method() { ... },锁的是当前对象实例。比如电商系统中一个订单对象的库存扣减方法,多个线程操作同一个订单时,实例锁可以保证库存正确性。
  3. 代码块加锁​:比如 synchronized(obj) { ... },锁的是指定对象。这种用法更灵活,比如在数据库连接池中,只需要对‘获取连接’这个关键操作加锁,而不是整个方法,这样可以减少锁的持有时间,提升性能。”

​(结合项目经验)​
“之前我做过一个支付系统的对账功能,对账时需要遍历订单列表并更新状态。最初对整个方法加实例锁,结果在高并发时性能很差。后来改成只锁遍历和更新状态的代码块,并单独用一个 Object 作为锁对象,吞吐量提升了40%。”

面试官​:

“在开发过程中,如果遇到多个线程互相等待对方释放锁的情况,也就是死锁,你们通常会怎么避免?”

​(结合项目经验):
“死锁确实是个头疼的问题,我们之前在数据库连接池里遇到过。比如线程A拿着连接1等连接2,线程B拿着连接2等连接1,结果大家都卡死。后来我们用了几个方法解决:

  1. 顺序加锁​:规定所有线程必须按固定顺序获取锁。比如统一先拿连接1,再拿连接2,这样就不会出现循环等待了。
  2. 超时释放​:用 ReentrantLock 的 tryLock(1, TimeUnit.SECONDS) 方法,如果1秒内拿不到锁就放弃,并回滚之前的操作。
  3. 工具检测​:定期用JStack导出线程堆栈,分析是否有BLOCKED状态的线程链。

比如有一次线上服务卡死,我们用JStack发现是订单和库存服务的锁顺序不一致导致的,调整后问题就解决了。”


面试官追问​:

“提到锁,你们有用过CAS吗?比如AtomicInteger这种原子类,它的原理是什么?”

​(类比生活场景):
“CAS就像超市的自助结账机。假设你看中一包标价10元的薯片,扫码时系统会检查价格是否还是10元。如果是,就扣款成功;如果中途有人改了价格,系统会提示你重新扫码。

AtomicInteger内部就是用这种机制。比如 incrementAndGet() 方法,它会循环尝试把值从旧值+1,直到成功为止。不过要注意 ​ABA问题​:比如你离开柜台又回来,发现薯片还是10元,但其实中间可能被人买走又补货了一次。这时候用 AtomicStampedReference 加个版本号就能解决。”


面试官深入​:

“AQS这个概念听起来很高大上,它在实际开发中有什么用?比如ReentrantLock是怎么依赖它的?”

​(拆解复杂概念):
“AQS可以理解为游乐园的排队系统。比如过山车一次只能坐10个人(state=10),排队的人(线程)按顺序进(FIFO队列)。

ReentrantLock 内部就是通过AQS管理这个排队过程的。

  • 非公平锁​:新来的游客可以插队,直接问‘现在有空位吗?’有就进去,没空再排队。
  • 公平锁​:新来的必须老老实实排到队尾。

比如我们做秒杀系统时,为了防止黄牛脚本抢购,用了公平锁让请求按顺序处理,虽然性能略有损失,但更公平。”


基础知识扩展: 

CAS(Compare-And-Swap)基础知识

1. 什么是CAS?​

CAS(Compare-And-Swap)是一种无锁编程的核心技术,用于实现多线程环境下的原子操作。它通过硬件指令(如x86的CMPXCHG)保证操作的原子性,无需传统锁机制。

2. CAS的操作原理

CAS操作包含三个参数:

  • 内存位置(V)​​:需要更新的变量。
  • 预期原值(A)​​:线程认为当前内存中的值。
  • 新值(B)​​:希望设置的新值。

执行流程​:

  1. 检查内存位置V的值是否等于A
  2. 如果相等,将V的值更新为B
  3. 如果不相等,不进行任何操作。

整个过程是一个原子操作,不会被其他线程中断。

3. CAS的应用场景
  • 原子类​:如AtomicIntegerAtomicReference
    AtomicInteger count = new AtomicInteger(0);
    count.incrementAndGet(); // 内部通过CAS实现自增
  • 无锁数据结构​:如无锁队列、栈。
  • 乐观锁​:数据库版本控制、缓存一致性。
4. CAS的缺点
  • ABA问题​:
    线程1读取值A,线程2将值改为B再改回A,此时线程1的CAS操作仍会成功,但中间状态可能已变化。
    解决方案​:使用带版本号的原子类AtomicStampedReference
    AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(100, 0);
    ref.compareAndSet(100, 101, stamp, stamp + 1); // 检查值和版本号
  • 自旋开销​:CAS失败时会循环重试,长时间竞争可能导致CPU资源浪费。
  • 单变量限制​:只能保证一个共享变量的原子操作。

AQS(AbstractQueuedSynchronizer)基础知识

1. 什么是AQS?​

AQS是Java并发包(java.util.concurrent)的核心框架,用于构建同步器​(如ReentrantLockSemaphore)。它通过一个volatilestate变量和一个FIFO线程等待队列,实现资源分配和线程调度。

2. AQS的核心组成
  • 状态变量(state)​​:
    表示资源的状态(如锁的持有次数、信号量的许可数量)。
    // ReentrantLock中state的含义:
    // state=0:锁未被占用;state>0:锁被占用,且可重入。
  • 等待队列​:
    双向链表结构(CLH队列变种),存储等待获取资源的线程。
3. AQS的工作模式
  • 独占模式(Exclusive)​​:
    资源只能被一个线程持有(如ReentrantLock)。
    • 核心方法​:
      protected boolean tryAcquire(int arg); // 尝试获取资源
      protected boolean tryRelease(int arg); // 尝试释放资源
  • 共享模式(Shared)​​:
    资源可被多个线程共享(如SemaphoreCountDownLatch)。
    • 核心方法​:
      protected int tryAcquireShared(int arg); // 尝试获取共享资源
      protected boolean tryReleaseShared(int arg); // 尝试释放共享资源
4. AQS的实现示例:自定义非重入锁
public class SimpleLock extends AbstractQueuedSynchronizer {@Overrideprotected boolean tryAcquire(int arg) {if (compareAndSetState(0, 1)) { // CAS修改statesetExclusiveOwnerThread(Thread.currentThread());return true;}return false;}@Overrideprotected boolean tryRelease(int arg) {if (getState() == 0) throw new IllegalMonitorStateException();setExclusiveOwnerThread(null);setState(0); // 无需CAS,只有持有线程能释放锁return true;}public void lock() { acquire(1); }public void unlock() { release(1); }
}
5. AQS的底层机制
  • 队列管理​:
    • 线程获取资源失败时,会被封装为Node节点加入队列尾部。
    • 通过LockSupport.park()LockSupport.unpark()阻塞和唤醒线程。
  • 公平性控制​:
    • 公平锁​:严格按照队列顺序分配资源。
    • 非公平锁​:新线程可插队尝试获取资源(通过CAS直接修改state)。

http://www.xdnf.cn/news/9048.html

相关文章:

  • 目标检测 TaskAlignedAssigner 原理
  • leetcode617.合并二叉树:递归思想下的树结构融合艺术
  • 拥塞控制算法cubic 和bbr
  • Day3 记忆内容:map set 高频操作
  • 2025年Google I/O大会上,谷歌展示了一系列旨在提升开发效率与Web体验的全新功能
  • Uniapp 串口通信原生插件开发指南(零基础版)
  • LSTM+Transformer混合模型架构文档
  • SWOT分析:MCP(Model Context Protocol)与传统编程解决方案
  • 精益数据分析(85/126):营收阶段的核心指标与盈利模型优化——从数据到商业决策的落地
  • Prompt Tuning:优化提示调优全攻略
  • 前端内容黑白处理、轮播图、奇妙的头像特效
  • Android开发namespace奇葩bug
  • 鸿蒙OSUniApp 开发实时天气查询应用 —— 鸿蒙生态下的跨端实践#三方框架 #Uniapp
  • Git 初次推送远程仓库
  • NL2SQL代表,Vanna
  • 【笔记】解决启动Anaconda Toolbox报错ModuleNotFoundError: No module named ‘pysqlite2‘
  • 从万有引力到深度学习,认识模型思维
  • ADS学习笔记(五) 谐波平衡仿真
  • 身份认证: JWT和Session是什么?
  • 深入解析 BlockingQueue:并发编程面试中的高频考点!
  • SDL2常用函数:SDL_RendererSDL_CreateRendererSDL_RenderCopySDL_RenderPresent
  • 数据库工程师备考
  • 第三届京麒CTF Web
  • ClickHouse性能优化技术深度解析与实践指南
  • (4)-Fiddler抓包-会话面板和HTTP会话数据操作
  • 多模态大语言模型arxiv论文略读(九十三)
  • Odoo 自动化规则全面深度解析
  • 探秘谷歌Gemini:开启人工智能新纪元
  • 基于树莓派的贪吃蛇游戏机
  • 【科研绘图系列】R语言绘制气泡图(bubble plot)