资讯动态

深入理解synchronized:Android多线程锁机制原理与实战避坑指南

发布时间:2026/9/9 17:54:41 来源:尧图企业网站定制
1. 从一次线上崩溃说起UI卡顿背后的数据竞争做Android开发这些年要说哪个问题最让人头疼线程安全绝对排得进前三。我印象特别深的一次线上事故是某个版本上线后线上反馈首页偶尔会卡死ANR率直接翻了三倍。当时查了好几天最后定位到的问题特别低级一个全局的配置对象在子线程拉取新配置时直接obj.setXxx()去改写字段而主线程同时正在读取这个对象做UI渲染。在Java里多个线程同时读写同一个可变对象如果没有同步机制轻则读到脏数据重则直接卡死崩溃。Android的ANR本质上就是主线程被阻塞太久而阻塞往往就来源于锁竞争。但很多刚入行的同学对synchronized的理解停留在加个锁就完了完全不理解它到底锁的是什么、锁的粒度怎么控制、什么时候该用什么时候不该用。这篇文章我不想写成一板一眼的API文档而是从一个实际踩过坑的Android开发者的角度把synchronized在Android开发里的底层原理、使用姿势、性能陷阱和替代方案都梳理一遍。无论你是刚接触多线程的新手还是想深入理解锁机制的进阶开发者这篇文章应该都能给你一些参考。先说个可能颠覆认知的结论synchronized在JDK 1.6之后做了大量优化性能已经没那么差了但如果你用错场景、锁错对象、锁的粒度太粗它照样能把你的App拖垮。理解它背后的机制远比背几个用法要点更重要。2. 揭开synchronized的底层面纱对象头与监视器机制要搞懂synchronized光知道它能保证原子性和可见性是不够的。我在面试别人时经常问一个问题synchronized锁的到底是什么能答出锁的是对象不是代码的人基本上就已经超过大半候选人了。2.1 每个Java对象都自带一把锁Java里的每一个对象在内存布局中都有一块区域叫对象头Object Header这里面存储了对象的运行时数据包括哈希码、GC分代年龄、锁状态标志等等。在64位JVM上对象头里的Mark Word占了8个字节其中最后2个bit就是锁状态标志位用来标记这个对象当前处于什么锁状态。这也就是说每个对象天生就是一把锁。synchronized在字节码层面其实就是两条指令monitorenter和monitorexit。当线程执行到monitorenter时会尝试获取对象的监视器Monitor只有持有了监视器才能继续往下执行执行完monitorexit时释放监视器。这个Monitor机制我用一个生活化的类比来解释想象一家只有一个包间的餐厅包间门上挂着一把钥匙。顾客A来了拿到钥匙进去用餐此时顾客B、C、D只能在外面等着。A用完餐出来还钥匙B才能进去。synchronized做的事情就是给你要保护的包间临界区代码加了一把钥匙任何线程想进去必须先拿到钥匙。2.2 锁升级从无锁到重量级的进化之路JDK 1.6之前synchronized确实是个重量级操作因为它的底层依赖于操作系统的互斥量Mutex线程从用户态切到内核态开销巨大。但到了JDK 1.6之后HotSpot团队对它做了大量优化引入了锁升级机制。整个锁的状态流转是这样的锁状态触发条件特点无锁对象刚创建没有线程竞争没有锁开销偏向锁同一个线程多次进入同步块记录线程ID后续进入几乎无开销轻量级锁出现多线程交替执行但不存在竞争使用CAS自旋不阻塞线程重量级锁多个线程同时竞争CAS失败升级为Monitor锁线程阻塞这个升级是单向且不可逆的无锁到偏向锁再到轻量级锁最后到重量级锁。为什么不可逆因为撤销偏向锁、降级锁都需要额外的同步操作成本太高JVM干脆不做逆向。理解了锁升级你就明白为什么我说不要再迷信synchronized性能差了。在低竞争场景下偏向锁和轻量级锁的开销极小甚至比Lock还要高效只有在高竞争场景下它才会升级成重量级锁导致线程阻塞和上下文切换。不过Android上的DVM/ART虚拟机对锁的实现有自己的优化策略但你写代码时不用太纠结这些底层细节只需记住锁升级是自动的JVM/ART会根据竞争情况自动调整。你真正需要操心的是别把锁的粒度搞得太粗别锁住不会变化的字段别在持锁期间做耗时操作。2.3 Monitor的等待队列为什么synchronized天然支持等待唤醒synchronized不仅仅是互斥它还天然集成了一套等待唤醒机制这就是wait()和notify()方法。这两个方法必须在synchronized块内调用否则会抛出IllegalMonitorStateException。核心逻辑是当线程持有了Monitor锁发现条件不满足时调用wait()主动释放锁进入等待队列其他线程获得锁并修改完条件后调用notify()或notifyAll()唤醒等待队列中的线程。这里有个关键点notify()唤醒的线程并不会立即执行它需要重新竞争锁。在Android开发里这套机制最常见的应用场景是生产者-消费者模式。比如一个下载管理器下载线程往队列里塞任务执行线程从队列取任务执行。队列空了执行线程就wait()下载线程往队列塞了任务后notify()执行线程被唤醒继续干活。用Synchronized wait/notify可以很优雅地实现这个逻辑不需要轮询也不会忙等浪费CPU。3. Android主线程模型与锁的实际应用场景聊完了底层原理我们回到Android开发的实际场景。Android的线程模型有一个非常鲜明的特点UI更新只能在主线程UI线程执行而网络请求、数据库操作、文件读写等耗时操作又不能放在主线程。这就导致我们经常要在子线程和主线程之间传递数据而数据的共享和同步就变得格外敏感。3.1 主线程与子线程的同步问题Android的View体系不是线程安全的。你不能在子线程里直接调用TextView.setText()否则会抛出CalledFromWrongThreadException。这就是为什么Android要求UI操作必须回到主线程。那么当主线程需要展示的数据是从子线程计算出来的你该怎么处理最常用的手段是Handler或者runOnUiThread但其实synchronized也在其中扮演着重要角色。举个例子一个音乐播放器的播放进度条。后台播放线程每200ms更新一次播放位置UI线程每16ms刷新一次进度条。这两个线程共享同一个PlaybackState对象。如果不做任何同步UI线程可能会读到半更新状态的数据——比如播放位置已经变了但总时长还是旧的导致进度条比例闪跳。正确的做法是对PlaybackState的读写都用synchronized保护起来确保读操作要么读到更新前的完整状态要么读到更新后的完整状态而不会读到中间状态。用volatile修饰position字段也可以解决可见性问题但如果PlaybackState里还有总时长、播放状态等多个字段需要保持一致volatile就无能为力了必须用锁来保证原子性。3.2 用synchronized保护共享资源的三种写法在Java/Android中synchronized有三种典型的用法每种用法的锁对象不同适用场景也不同。第一种修饰实例方法public synchronized void updatePlaybackState(long position, long duration) { this.position position; this.duration duration; }这种写法锁的是调用该方法的实例对象this。多个线程如果操作的是同一个实例就会互斥如果操作的是不同实例则互不干扰。第二种修饰静态方法public static synchronized PlaybackState getGlobalState() { return globalState; }锁的是当前类的Class对象。这个锁的粒度是全局的不管创建了多少个实例所有调用该静态方法的地方都在竞争同一把锁。第三种同步代码块public void updateState(long position, long duration) { synchronized (this) { this.position position; this.duration duration; } }这是最灵活的方式可以精确控制锁的范围。我强烈推荐在Android开发中用这一种因为它可以尽量缩小锁的范围只在真正需要保护的那几行代码上持锁而不是锁住整个方法。有意思的是方法级syncnronized其实就是同步代码块的语法糖——JVM会自动把整个方法体包在synchronized中锁对象是this实例方法或Class对象静态方法。3.3 三种用法对应Android中的典型场景拿一个实际项目举例你写了一个图片缓存管理器包含一个LruCache对象和一个线程池。多个网络线程下载完图片后要写入缓存UI线程读取缓存来展示图片。在这种场景下读和写是并发的你就需要给缓存的读写操作加锁。但注意你没必要把锁加到整个Manager类上——那会连getImage和putImage这两个不相关的方法也互斥了。更好的方式是public class ImageCacheManager { private final Object cacheLock new Object(); private LruCacheString, Bitmap cache; public Bitmap getImage(String key) { synchronized (cacheLock) { return cache.get(key); } } public void putImage(String key, Bitmap bitmap) { synchronized (cacheLock) { cache.put(key, bitmap); } } }这里有个关键经验用一个专门的对象作为锁而不是用this。为什么因为如果直接用this那么所有用这个实例做lock的地方都会互相竞争而你用一个私有的Object当锁可以让别人无法从外部获取到这个锁对象从设计上就避免了不可控的竞争。类似地ReentrantLock提供的lock()和unlock()方法本身没有任何魔法它依赖的是volatile变量加上CAS操作实现起来比synchronized复杂一些但它支持更灵活的锁获取和释放方式以及超时中断等高级特性。这个我们后面的章节会展开。4. 核心机制深挖原子性、可见性、可重入性synchronized能解决的问题归纳起来就是并行编程的三大顽疾原子性、可见性、有序性。很多人听到这几个概念就头大我用Android开发的场景一个个拆开讲。4.1 原子性操作不可分割原子性指的是一个操作要么全部执行完要么完全不执行中间不会有其他线程插进来。举个经典的例子count这在Java中不是原子操作。它在字节码层面是三步读取count的值、将值加1、将新值写回count。两个线程同时执行这三步就可能出现丢失更新两个线程都读到count10都把它改成11最终count11但实际应该等于12。在Android中一个很常见的非原子操作是TextUtils.isEmpty(text) text.length() 10这种复合判断。虽然每一条语句本身是原子的但两步之间其他线程可能已经修改了text的值导致你拿到的数据前后不一致。用synchronized保护复合操作就能保证这段代码的原子性——线程A执行完整个同步块之前线程B无法插进来执行相同的代码块。但注意synchronized不是唯一的原子性保证手段。volatile能保证可见性和有序性但不能保证复合操作的原子性AtomicInteger通过CAS算法保证了单变量操作的原子性效率比锁更高。所以写代码时要具体情况具体分析如果只是简单变量的自增用AtomicInteger更合适如果是一组数据的复合操作用synchronized保护更靠谱。4.2 可见性修改能及时被其他线程看到Java内存模型JMM规定每个线程都有自己的工作内存寄存器/缓存线程对变量的读写首先在工作内存中进行然后同步回主内存。如果没有同步机制一个线程修改了变量另一个线程可能还在用自己工作内存里的旧值。这就是可见性问题。比较出名的案例就是那个死循环问题boolean running true; // 子线程 new Thread(() - { while (running) { // 执行任务 } }).start(); // 主线程 running false; // 期望子线程退出循环但可能不会生效如果running不加volatile也不加synchronized主线程修改running false后子线程可能因为工作内存中缓存了runningtrue而永远感知不到这个变化导致死循环。synchronized能解决可见性的原理是在JMM中线程释放锁时会把工作内存中的修改强制刷新到主内存线程获取锁时会把主内存中的最新值加载到工作内存。这就是释放锁之前的内存操作对获取锁的线程可见。在实际Android开发中最常见可见性坑就是在onPause里停止子线程的循环标志位但子线程可能感知不到。正确的做法是给标志位加volatile或者在synchronized块中修改和读取标志位。4.3 可重入性同一个线程可以重复获得同一把锁synchronized是可重入锁即同一个线程在已经持有某把锁的情况下可以再次申请获取同一把锁不会造成死锁。这个特性特别重要。想象一个场景一个方法A被synchronized修饰方法A内部又调用了同样被synchronized修饰的方法B。如果synchronized不可重入线程在进入方法B时就会因为无法获取自己已经持有的锁而死锁这显然是荒谬的。在实际项目中可重入性让我们可以放心地在锁内调用其他同步方法而不必担心自我死锁。比如public synchronized void methodA() { // some logic methodB(); // 同一个线程可以重入获取同一个锁 } public synchronized void methodB() { // some logic }这在Android中处理复杂的业务逻辑时很关键可能你在一个同步的入口方法中调用了好几个同步方法如果锁不可重入代码根本跑不起来。所以记住可重入是synchronized的安全网它让你在锁内嵌套调用时不需要刻意规避。5.synchronized在Android项目里的几种常见陷阱网上关于synchronized的用法教程一抓一大把但真正让项目出问题的往往是那些用了锁却还是出错的隐藏陷阱。这一节我把这几年在代码审查中反复见到的错误用法整理出来个个都是实际踩过的坑。5.1 锁对象失效String字面量作为锁的坑有人图省事直接用字符串字面量当锁synchronized (cache_lock) { // 保护缓存操作 }这看起来没问题实际上隐患很大。因为Java中的字符串字面量是常量被JVM内部的字符串常量池缓存着。你能用cache_lock当锁别的库、别的模块如果也用了相同的字符串字面量当锁你们的锁就互相竞争了。更糟糕的是同一把锁可能被毫不相干的业务共享导致性能下降甚至无意义的阻塞。同理不要用Integer、Long等包装类型的常量当锁因为它们的值在[-128, 127]范围内也是缓存的。正确做法是像前面说的用一个私有的final Object作为锁彻底隔离外部影响。5.2 锁的粒度太粗把不需要同步的操作也锁住了Synchronized虽然是线程安全的保障但是滥用锁反而会成为性能瓶颈。最典型的反模式就是把耗时操作放在同步块里public synchronized ListItem loadItems() { // 从网络获取数据耗时2秒 ListItem items networkApi.fetchItems(); // 写入缓存耗时0.5秒 cache.save(items); return items; }这段代码逻辑上没毛病但性能灾难。因为锁会一直持有到方法结束其他线程想访问items相关的同步方法就得白白等2.5秒。在Android里如果主线程恰好调用这个方法直接就是ANR的节奏。正确做法是尽量缩小同步范围。把耗时的网络请求放在锁外面只在读写共享缓存的部分加锁。比如public ListItem loadItems() { // 网络请求 - 不需要加锁 ListItem items networkApi.fetchItems(); // 只保护缓存写入 synchronized (cacheLock) { cache.save(items); } return items; }经验法则锁的范围应该恰好覆盖共享资源的访问而不是整个业务流程。锁的粒度越小并发度越高性能越好。5.3 死锁多把锁交叉等待synchronized是可重入的单把锁的情况下不会死锁。但多把锁之间如果出现循环等待仍然会死锁。来看这个经典场景// 线程A synchronized (lockA) { synchronized (lockB) { // 业务逻辑 } } // 线程B synchronized (lockB) { synchronized (lockA) { // 业务逻辑 } }线程A持有了lockA想获取lockB线程B持有了lockB想获取lockA。两个线程都在等对方释放锁谁也等不到就死锁了。在Android开发中死锁的实际表现通常是某个功能偶尔卡死、ANR、无法响应。排查死锁的手段是用adb shell kill -3 pid抓取ANR trace日志里面有线程栈能看到两个线程各自持有什么锁、正在等待什么锁。避免死锁的几条实用原则锁的顺序要一致所有线程按照相同的顺序获取多把锁。尽量只持有一把锁如果能把逻辑拆分成多个零散的锁就不要用一把大锁保护一切。使用带超时的锁ReentrantLock的tryLock(long timeout, TimeUnit unit)可以在获取不到锁时放弃而不是无限等待。这是Synchronized做不到的synchronized无法中断等待锁的过程。5.4 内存可见性与双重检查锁的坑在Android单例模式中最常用的写法是双重检查锁Double-Checked Lockingprivate static volatile Singleton instance; public static Singleton getInstance() { if (instance null) { // 第一次检查 synchronized (Singleton.class) { if (instance null) { // 第二次检查 instance new Singleton(); // 创建实例 } } } return instance; }这个写法看似完美但有一个历史遗留陷阱在JDK 1.5之前由于指令重排序的存在instance new Singleton()这行代码并不保证原子性——它可能先分配内存、返回引用再调用构造函数。如果另一个线程在构造函数执行完之前读到了非空的instance引用就会拿到一个未初始化完成的对象。好在从JDK 1.5开始volatile的语义得到了增强——保证volatile变量的写入操作不会与它前面的任何读写操作重排序。上面这个例子中instance被声明为volatile所以它在JDK 1.5之后是安全且正确的。但在实际Android项目的代码审查中我见过不少漏掉volatile修饰的双重检查锁实现。一旦漏掉虽然大部分情况下能跑但极端并发场景下就可能出诡异问题。大家在写单例时一定要检查volatile是否加上。5.5 锁与生命周期组件一起使用时的注意点Android的组件有生命周期而锁跟生命周期撞在一起很容易出问题。举个常见的例子Activity的onPause要释放资源但子线程还持有锁在处理数据。如果你在onPause里等待一个子线程结束而这个子线程需要获取锁才能继续主线程等锁、子线程等线程执行完毕就可能互相等待导致ANR。避免方案有两个要么给等待设置超时join(timeout)要么用HandlerLooper切回主线程的机制配合synchronized限制锁的持有时间。说实话锁的生命周期管理是一个权衡问题没有银弹。我的建议是在Android中尽量让锁的持有时间短到微秒级不要跨主线程的同步等待这样生命周期问题自然就少了。6. 从synchronized到协程Android线程安全方案的选型进阶synchronized不是唯一的选择但很多场景下它又是最顺手的选择。那么在Android开发中到底什么时候该用synchronized什么时候该用别的我们逐个对比一下。6.1 synchronized vs volatilevolatile关键字只保证可见性和有序性不保证原子性。它适用于一个线程写、多个线程读的场景例如标志位、配置开关。// 适合volatile的场景一个线程修改其他线程读取 private volatile boolean isPlaying false;而synchronized还保证了原子性。如果多个线程都要改写同一个变量volatile无能为力必须用锁或者原子类。简单记忆你只需要通知其他线程我改了用volatile你需要多个线程之间互斥地读写用synchronized。6.2 synchronized vs LockReentrantLockLock是一个接口最常用的实现是ReentrantLock。相比synchronized它提供了更丰富的功能可中断地获取锁lockInterruptibly()可超时地获取锁tryLock(timeout, unit)多个等待条件newCondition()可以创建多个等待队列公平锁new ReentrantLock(true)可以指定公平模式但它的劣势也很明显必须手动释放锁。如果忘记在finally里unlock()锁永远不会被释放其他线程全部卡死。而synchronized无论是正常返回还是抛出异常JVM都会自动释放锁。我的建议是在Android项目里能用synchronized就用synchronized代码更短、更不容易出错。只有在需要超时获取锁、中断锁等待等高级特性时才选择ReentrantLock。6.3 synchronized vs 原子类AtomicReference等java.util.concurrent.atomic包下的原子类通过CAS比较并交换实现了无锁编程。它们在高并发场景下性能往往比synchronized好因为它们不会阻塞线程而是在硬件层面反复尝试。在Android中比较典型的应用是用AtomicBoolean代替volatile boolean来控制一次性任务的执行private AtomicBoolean taskStarted new AtomicBoolean(false); public void startTask() { if (taskStarted.compareAndSet(false, true)) { // 只有第一次调用会进入这里 doExpensiveTask(); } }但原子类也只适用于单一变量的操作。如果你的临界区涉及多个变量的复合操作原子类帮不上忙synchronized依然是最直接的选择。6.4 Kotlin协程与锁的关系现在Android开发主流已经是Kotlin 协程了。很多人在协程环境中会下意识地忽略线程安全问题这可是个隐含的雷。协程在线程池中调度几个协程如果同时操作一个可变对象照样会有竞态条件。在协程世界里推荐的方案是用Mutex协程版本的锁它的withLock挂起函数不会阻塞线程而是挂起协程效率更高用**不可变数据immutable data**和StateFlow来避免共享可变状态将共享状态收窄到单一协程内处理用actor模式。但即使项目已经迁移到了Kotlin协程老的Java代码库或某些场景下比如ViewModel中对共享资源的并发访问控制synchronized依旧会出现在代码里它的职责并没有完全被替代。6.5 选型决策表不同场景下该用什么场景推荐方案原因单变量可见性标志位volatile开销最小代码简单复合操作的互斥synchronized代码块自动释放锁不易出错需要超时/中断获取锁ReentrantLock支持tryLock(timeout)单一变量高并发自增AtomicIntegerCAS无阻塞性能好协程环境中的互斥Mutex非阻塞挂起适配协程模型完全不共享可变状态不可变数据 消息传递从根本上消除竞态这张表是我平时写代码时的一个快速决策依据。不要一上来就synchronized也不要迷信无锁编程选型的前提永远是具体的并发访问模式。7. 一次实战排查用synchronized修复一个时序错乱Bug理论讲多了来点实在的。前阵子在处理一个IM聊天项目时遇到了一个典型的由线程安全问题引起的时序错乱Bug排查和修复的过程挺有代表性分享给大家。7.1 问题现象用户反馈聊天界面偶尔会出现消息顺序错乱。比如A先发的消息显示在了B消息后面或者一个会话未读计数偶尔跳跃。线上复现比较困难但通过日志发现一个规律错误只出现在发送消息和接收消息同时发生的瞬间。用户正在输入并点击发送同时对方刚好回了一条消息。7.2 排查链路第一步看代码。看到的是这样的结构public class ChatViewModel { private ListMessage messages new ArrayList(); public void addLocalMessage(Message msg) { messages.add(msg); // 发送消息时先本地插入 notifyUI(); } public void addRemoteMessage(Message msg) { messages.add(msg); // 收到远端消息时插入到列表中 notifyUI(); } }表面上看ArrayList在单线程下没问题add方法也确实是原子的。但问题恰恰出在两个方法在不同线程同时执行时。addLocalMessage可能被主线程的点击事件触发addRemoteMessage可能从WebSocket子线程回调上来。两个线程同时调用messages.add()ArrayList内部会检查容量、扩容、赋值整个过程不是线程安全的就会产生错乱——一个线程写入的元素可能被另一个线程的扩容操作覆盖掉。第二步意识到需要的不仅仅是原子性还有消息的排序逻辑。虽然messages.add()本身不复杂但如果两个线程同时插入最终的顺序取决于CPU调度而不是业务逻辑的先后。第三步决定加锁。但因为项目里有些逻辑是在协程里跑的有些是在Thread里跑的为了不影响代码结构我选择了最通用的synchronized来保护消息列表的访问public class ChatViewModel { private final Object messageLock new Object(); private ListMessage messages new ArrayList(); public void addLocalMessage(Message msg) { synchronized (messageLock) { messages.add(msg); notifyUI(); } } public void addRemoteMessage(Message msg) { synchronized (messageLock) { messages.add(msg); notifyUI(); } } public ListMessage getMessagesSnapshot() { synchronized (messageLock) { return new ArrayList(messages); } } }这里要注意我为读取方法也加了锁并且返回的是拷贝后的新列表。这样即使UI线程在遍历消息列表的同时有子线程插入新消息也不会触发ConcurrentModificationException。第四步验证修复效果。通过同事的联调压力测试同时用脚本模拟高频收发消息持续跑了几小时后消息顺序和未读计数都正常了。这个Bug彻底修复。7.3 这个Case带给我的思考其实这个Bug并不是高深的并发难题但它的出现非常典型大部分Android线程安全问题都出在了回调线程和UI线程的交叉路径上。网络回调、DB回调、传感器回调、广播回调这些异步回调天然从不同线程过来如果直接操作共享集合而不做同步出问题只是时间问题。所以我的习惯是只要一个数据类可能被多个线程访问就默认给它加锁保护。即使当时只有一个访问点也要留下一个final的锁对象为未来扩展留余地。这个习惯让我减少了很多线上偶现Bug。8. 写在最后几个关于synchronized的实用心得这可能是全文最实用的一部分都是我多年项目经验沉淀下来的个人习惯分享出来供参考。第一永远用一个私有的final锁对象而不是this和字符串。私有的锁对象从设计上隔离了外部持有锁的可能性避免了不同业务代码之间因为锁对象共享而产生的意外竞争。这个习惯能让你少排查很多并发问题。第二同步块里不要有耗时操作。锁的本质是排队排队就一定等待。网络请求、磁盘IO、数据库操作这类事情能放锁外就放锁外。如果确实需要先在锁内判断某个条件再执行耗时操作也尽量把耗时操作拆到锁外执行。第三读多写少的场景考虑ReadWriteLock或者CopyOnWriteArrayList。synchronized是互斥锁读和读之间也是互斥的。如果你的数据读频率远大于写频率用ReentrantReadWriteLock可以让多个读线程并发执行性能提升明显。但这属于进阶优化常规项目不要过早引入。第四善用ThreadLocal减少锁需求。有些看似共享的变量实际上每个线程只需要自己的副本那就用ThreadLocal。比如SimpleDateFormat不是线程安全的与其加锁不如每个线程一个实例这样连锁都省了。ThreadLocal和锁是解决并发问题的两个相反方向锁是让多线程访问同一份数据而ThreadLocal是让每个线程访问自己的数据。第五Kotlin协程项目也要保持线程安全警觉。协程只是让并发代码更好写了并没有消除并发。Dispatchers.IO和Dispatchers.Main之间共享的数据该加锁还是加锁。在协程中用synchronized虽然不会死锁因为锁是阻塞的而不是挂起的但会阻塞协程所在的线程所以如果性能敏感优先用Mutex。回到最初那个线上崩溃。如果当时写代码的人对synchronized有足够的敬畏——理解它锁的是对象理解它要尽量缩小范围理解它保护的必须是共享可变资源——那个版本大概率不会翻车。多线程的世界里Bug往往是看起来运行很正常直到某个特定时机才暴露而Synchronized的正确使用就是我们拦住这类Bug的第一道防线。希望这篇文章能让你对synchronized的理解从知道提升到会用、敢用、用得对。下次面试官再问Java的synchronized原理是什么你就可以从对象头、Monitor、锁升级一路讲到他喊停为止。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价