新闻详情

新闻详情

首页 / 资讯中心 / 详情

Android线程安全实战:synchronized底层原理、锁升级与最佳实践

发布时间:2026/9/9 21:52:12来源:尧图网络
Android线程安全实战:synchronized底层原理、锁升级与最佳实践
写Android这几年只要涉及到多线程访问共享数据synchronized几乎就是默认选项。面试被问“synchronized底层原理”的人很多但真正在项目里把synchronized用得干净利落、不留下暗坑的人反而没那么多。这篇文章我想从实际开发的角度把synchronized在Android线程安全里怎么理解、怎么用、怎么排查问题讲透。它适合那些已经被并发问题坑过、或者正在写多线程业务但心里没底的Android开发者当然也适合准备面试时想把synchronized彻底搞明白的人。我不是来讲教科书概念的就按我日常debug和写代码时真实的思考路径来聊。synchronized说到底就是一把内置锁它的核心价值是让一段临界区代码在同一时刻只有一个线程能进去执行。听起来简单但它在JVM和ART虚拟机里玩出的花样、在Android业务代码里能触发的各种问题绝对值得花十分钟系统梳理一遍。1. 线程安全问题的本质与synchronized的设计思路1.1 竞态条件、原子性、可见性、有序性到底指的是什么先看一个最常见的错误示例。单线程里写int count 0; count;不会有任何问题但两个线程同时执行count最终结果很可能不是2而是1。原因在于count从来都不是一个原子操作它在字节码层面至少包含三条指令读取count的当前值、执行加1、写回新值。两个线程可能同时读到0然后各自加1再写回这个过程中有一次更新被另一个线程覆盖了。这就叫竞态条件。除了原子性还有两个比原子性更隐蔽的问题。第一个是可见性线程A修改了一个共享变量线程B不一定能立刻看到这个变化。因为每个线程在工作时会把变量拷贝到自己的工作内存对应到CPU的寄存器、L1/L2缓存如果一个线程改了缓存里的副本但没有同步到主存另一个线程从主存里读到的仍然是旧值。第二个是有序性编译器在保证单线程语义不变的前提下可能会对指令做重排。单线程下重排不会影响最终结果但多线程下另一个线程可能看到的是重排后的执行顺序结果就很容易出问题。这三个问题——“原子性、可见性、有序性”——是并发编程里所有幺蛾子的根源。Java的内存模型JMM就是为了在语言层面规范这三个问题的规则而synchronized是其中最能打的一件兵器。1.2 synchronized是如何同时解决三大并发问题的从JMM的角度看synchronized其实做了三件配套动作。第一是互斥执行。这是它最直观的能力一个线程进入同步代码块前必须先获得锁锁被持有时其他线程只能在入口处阻塞等待直到锁被释放。这保证了临界区代码的原子性count在同步块里执行时读、加、写三步不可分割。第二是可见性。JMM规定锁的获取和释放会建立happens-before关系解锁线程对共享变量的所有修改对后续加锁线程是完全可见的。换句话说线程A在释放锁之前写下的所有变量值一旦线程B获取同一把锁它读到的必然是A写完后的最新值。这就把“缓存副本不可见”的问题解决了。第三是有序性。在同步代码块内部JVM不会做可能破坏同步语义的指令重排这意味着临界区内的代码是严格按照逻辑顺序来执行的不会出现诡异的乱序。所以synchronized不是只解决了一个问题它是把并发编程中最基本的三个问题一揽子打包处理了。这也是为什么它比volatile更全面——volatile只保证可见性和有序性不能保证原子性。1.3 为什么不是volatile、Lock或原子类Android里解决线程安全的工具不止一种volatile、AtomicInteger、ReentrantLock、ConcurrentHashMap新项目里还有协程。但在我个人的判断里synchronized依然是大多数业务场景的第一选择原因非常现实。第一它由JVM/ART虚拟机层面支持和持续优化。JDK 1.6之后引入的锁升级机制让synchronized在竞争不那么激烈的时候开销非常小甚至接近于无锁。很多人对“synchronized性能差”的印象还停留在远古时代这个观点早就过时了。第二语法上足够简洁异常自动释放锁。用ReentrantLock时如果在临界区抛了异常又忘了在finally里unlock这个锁可能永远不被释放。synchronized没有这个问题不管正常退出还是异常退出锁都会被自动释放少了一个让代码review抓狂的隐患。第三语义清晰、可读性好。一行synchronized (lock) { ... }就能让所有人明白这里是临界区不需要额外解释。在团队协作里简单直接的代码往往比花哨的方案更容易维护。当然也不是说synchronized就万能。高竞争场景下它的性能不如Lock需要公平锁、超时中断等高级语义时也得换成Lock但这些都是少数场景。日常业务里用synchronized已经覆盖了90%以上的线程安全问题。2. synchronized的三种用法与锁的底层机制2.1 实例方法、静态方法、同步代码块锁的到底是什么synchronized有三种基本用法很多人背过但实际写起来容易混淆。第一种修饰实例方法锁住的是当前对象thispublic synchronized void increase() { count; }第二种修饰静态方法锁住的是当前类的Class对象。这一点很关键它和实例方法锁的不是同一个对象所以一个静态同步方法和一个实例同步方法之间并不互相阻塞public static synchronized void doSomething() { // 锁住的是Xxx.class对象 }第三种同步代码块锁对象可以自己在代码里指定写法最灵活public void increase() { synchronized (lock) { count; } }我见过不止一次同事把静态方法和实例方法搞混觉得两个方法都被synchronized修饰了就一定互斥。实际上完全不是这样——锁的是对象而不是方法本身。如果两个同步方法分别锁在this和Class对象上它们各走各的锁同时执行一点问题都没有。所以在设计的时候如果你希望同一份数据的所有操作都互斥就一定要确保所有的入口都用同一个锁对象。这是自查代码时最该先确认的一点。2.2 锁升级机制从偏向锁到重量级锁性能开销怎么变化synchronized的底层实现在JDK 1.6以后做了巨大优化引入了锁升级的路径偏向锁 - 轻量级锁 - 重量级锁。偏向锁的思路是如果同一个线程反复获取同一把锁那没必要每次都做同步操作直接在对象头的Mark Word里记录持有锁的线程ID后续这个线程再次进入时只需要比对一下ID就行开销几乎为零。当第二个线程也来竞争时偏向锁被撤销升级为轻量级锁。轻量级锁通过CAS自旋来获取锁不涉及线程阻塞和操作系统内核态切换。如果自旋一段时间还拿不到锁说明竞争真的激烈了就升级为重量级锁未获得锁的线程会被挂起阻塞此时涉及用户态和内核态的切换成本相对较高。对Android开发者来说有一点需要额外注意Android上老版本是Dalvik新版本是ART虚拟机它们的锁实现和HotSpot并不完全一样。但总体的思路类似锁在竞争不激烈时开销是可以忽略的。我实测过在比较新的Android设备上单线程反复进出同一个synchronized块几乎没有性能感知。哪怕是轻量级的短临界区竞争也就是微秒级别的阻塞。真正的性能杀手是重量级锁导致的线程阻塞和唤醒而这种情况通常是锁粒度设计不合理不是synchronized本身的锅。2.3 锁对象的选择与wait/notify的配合锁对象是synchronized最容易踩坑的地方。我见过一个线上问题两段完全无关的业务代码用了内容相同的String字符串常量做锁对象结果无端产生了锁竞争两个功能互相拖慢。Java里String有常量池机制内容相同的字符串很可能指向同一个对象你以为自己锁的是局部变量实际锁住了全JVM共享的一个字符串常量池对象。所以我的原则是锁对象用private final Object lock new Object()是最安全、最可控的。能不用this就不用this因为this可能在你不知道的地方被别人拿去当锁用了能用同一把锁就尽量集中在一个地方避免锁对象满天飞。wait()和notify()和synchronized是强绑定的。wait()会释放当前持有的锁notify()会唤醒一个在条件变量上等待的线程这两个方法都必须在同步块或同步方法内调用否则会抛IllegalMonitorStateException。一个经典的生产者-消费者模式可以这样写private final Object lock new Object(); private final QueueString queue new LinkedList(); private static final int MAX_SIZE 16; public void produce(String message) throws InterruptedException { synchronized (lock) { while (queue.size() MAX_SIZE) { lock.wait(); } queue.offer(message); lock.notifyAll(); } } public String consume() throws InterruptedException { synchronized (lock) { while (queue.isEmpty()) { lock.wait(); } String message queue.poll(); lock.notifyAll(); return message; } }注意判断条件用的是while而不是if。这是因为存在“虚假唤醒”的可能用while可以让线程被唤醒后重新检查条件只有条件真正满足才继续往下走。这是官方文档明确建议的写法理解之后就会明白它的必要性。3. Android实战单例、内存缓存、线程通信3.1 双重检查锁单例的正确写法volatile一个都不能少Android里synchronized最常见的应用场景就是写单例。很多人刚入门时写的懒汉式单例是方法级同步public static synchronized Singleton getInstance() { if (instance null) { instance new Singleton(); } return instance; }这个方法本身没问题但每次调用都要经过同步入口虽然竞争不激烈时有偏向锁兜底性能还可以但代码观感上不够漂亮。所以更常见的写法是双重检查锁Double Check Lockingprivate static volatile Singleton instance; public static Singleton getInstance() { if (instance null) { synchronized (Singleton.class) { if (instance null) { instance new Singleton(); } } } return instance; }这里有一个特别容易被忽略的细节instance必须用volatile修饰。原因在于instance new Singleton()不是原子操作它在底层大致分三步分配内存、调用构造函数初始化对象、把引用指向这块内存。如果没有volatile在高并发下第二步和第三步可能被重排也就是引用先指向了一块尚未初始化完成的内存另一个线程在第一个if (instance null)判断时发现instance不为null就直接返回了这个没有初始化完成的对象。这个问题的标准解法就是用volatile禁止指令重排。理解了原理你就明白网上有些去掉volatile的写法是有隐患的哪怕测试跑一万次不出问题也不能保证线上不会偶发。如果想彻底绕开锁也可以用静态内部类方式实现单例借助类加载机制天然保证线程安全。但在日常代码里volatile double check依然是很高效、很直观的方案。3.2 一个线程安全的内存缓存封装锁粒度怎么设计实际项目里经常遇到一个需求手写一个简单的内存KV缓存要求多线程读写安全。最直觉的方案是给put和get都加上synchronizedpublic class MemoryCacheK, V { private final LinkedHashMapK, V map new LinkedHashMap(); private final int maxSize; public MemoryCache(int maxSize) { this.maxSize maxSize; } public synchronized V put(K key, V value) { V old map.put(key, value); if (map.size() maxSize) { K eldest map.keySet().iterator().next(); map.remove(eldest); } return old; } public synchronized V get(K key) { return map.get(key); } }这是最简单可靠的方案适合缓存操作不频繁的场景。但如果get是高频操作、put是低频操作这种全量同步的方案就不太划算了。更好的方案是底层用ConcurrentHashMap再配合淘汰策略。注意这里不能用ConcurrentHashMap.size()来精确判断淘汰时机因为它的size在并发下是近似值。这种情况下要么接受近似淘汰要么给淘汰逻辑单独加一把轻量级锁。这也是一个典型的设计取舍你得先搞清楚自己的业务到底更看重什么。另外AndroidX自带的LruCache其实本身已经是线程安全的它的内部实现就是LinkedHashMap加synchronized。所以很多时候不用自己造轮子但读懂LruCache的源码理解它是怎么用锁的对排查线上问题非常有帮助。3.3 synchronized与Handler、线程池、主线程的配合Android开发里绕不开的一个场景是子线程算完数据回主线程更新UI。通常的写法是new Handler(Looper.getMainLooper()).post(...)或者用协程的withContext(Dispatchers.Main)。但我发现不少人对这里的线程安全边界是模糊的——数据在子线程算完了不等于发到主线程用的时候就不用操心同步了。我踩过的一个典型坑是子线程在临界区里持锁做了耗时操作比如网络请求或数据库查询此时主线程也在等待同一把锁。主线程一旦被锁阻塞时间过长就会直接触发ANR。日志里能看到主线程阻塞在锁上的堆栈下一步就是用户感受到卡死。这个问题的解决办法不是让锁自动变得高效而是要从设计上规避synchronized临界区里千万不要做耗时操作尤其是网络IO、磁盘IO这类可能几十毫秒甚至更久的操作。锁只用来保护内存数据和轻量计算这是铁律。如果确实需要在临界区里等待某个异步结果那一定是设计有问题需要重构而不是靠调大超时时间糊弄过去。4. 锁粒度、性能分析与死锁排查4.1 锁粒度怎么划最合理一致性边界优先于并发度锁粒度是synchronized使用中两极分化最严重的一环。有的人图省事把整个方法锁住导致并发度极低有的人为了追求并发性能把锁拆得七零八碎结果数据一致性出了问题。我的判断标准是先画清楚哪些变量属于同一组不可分割的业务状态这一组状态的所有读写必须使用同一把锁。比如一个电商订单订单状态和库存扣减是关联数据如果你把库存的锁和订单状态的锁分开就可能出现余额扣了但订单状态还没更新另一个线程查到的是中间状态产生脏读。确定了一致性边界之后再在这个边界内尽量缩小临界区。同步块应该尽量小但至少要覆盖需要保持一致性的最小操作集合。我见到的很多bug都是先把锁拆小了然后发现数据对不上了最后又回去把锁恢复大。如果一开始就按一致性边界来设计往往能省掉不少返工。4.2 synchronized与Lock、并发容器、原子类的取舍这是经常被问到的选择题什么场景用synchronized什么场景换ReentrantLock我的实践经验是这样诉求推荐方案原因简单互斥、临界区短、竞争不激烈synchronized语法简单、异常自动释放锁、有锁升级优化需要超时获取锁、可中断、公平锁ReentrantLock提供tryLock、lockInterruptibly、公平策略读多写少ReadWriteLock或StampedLock读锁可以共享吞吐量更高计数器、数值累加AtomicInteger / AtomicLongCAS自旋无阻塞性能远优于锁并发MapConcurrentHashMap内部CAS synchronized锁桶分段互不阻塞并发List读多写少CopyOnWriteArrayList写时复制读操作无锁生产者-消费者队列ArrayBlockingQueue / LinkedBlockingQueue内部已封装好锁和条件队列值得多说一句的是ConcurrentHashMap在JDK 8里已经大量使用了synchronized锁桶的思路它没有排斥synchronized反而是把锁的粒度拆小到了单个桶。这种“尽量缩小锁覆盖范围”的思路才是并发编程的精髓而不是纠结选哪个锁工具。4.3 死锁与ANR的定位思路从Thread Dump开始死锁是线程安全问题里最头痛的一种。四个必要条件互斥、持有并等待、不可剥夺、循环等待。在Android上死锁往往最终表现为ANR或App彻底卡住。定位手段有三板斧。第一板斧是抓线程堆栈连接设备后执行adb shell kill -3 pid系统会生成ANR trace文件也可以用Android Studio的Profiler直接看线程状态。第二板斧是看关键字在堆栈中搜索held by thread和waiting to lock一般能直接看到两个线程互相等待的完整链路。第三板斧是复盘锁顺序如果两个线程各自持有一把锁又要去获取对方手里的锁就形成了循环等待解决办法是统一锁获取顺序让所有线程都严格按照同样的顺序获取多把锁。我处理过一个真实案例线程A持锁后调用了一个方法这个方法内部又需要获取线程B持有的锁而线程B的临界区反过来又要等线程A的锁两个线程直接锁死。后来把两层锁合并成一把锁问题立刻消失。有时候最简单的解法就是把锁减少而不是增加。5. 常见问题速查表与我的几点心得5.1 一份synchronized相关问题的排查速查表现象可能原因解决方案IllegalMonitorStateExceptionwait/notify在synchronized块外调用把wait/notify放到同步块或同步方法内数据偶尔不一致静态同步方法和实例同步方法用了不同锁统一锁对象或用同一个private final锁性能卡顿、CPU飙高临界区内做了耗时操作或锁竞争激烈缩小锁粒度把耗时操作移出临界区ANRtrace显示waiting to lock主线程等锁且锁被其他线程长期持有避免主线程竞争锁精简锁内代码两个线程卡死互相等待死锁统一锁获取顺序用tryLock做兜底拿String或Integer做锁对象常量池缓存导致意外的全局锁竞争改成private final Object锁用Lock忘记unlock没有配套try-finally换synchronized或严格用try-finally包裹5.2 我对synchronized的三个使用原则第一条锁的对象要少而精。一个模块内尽量只用一个锁或者一组关联操作使用同一个锁。锁对象越分散越容易出现不一致和死锁的隐患。第二条同步代码块越小越好。锁保护的是代码不是整个方法。能保护局部变量就不要锁整个类能让锁覆盖的代码短一点就不要把无关逻辑包进去。第三条能不上锁就不上锁。多线程场景下优先考虑用并发安全的容器和原子类把可变状态封装到一个比较小的范围内然后集中给这个范围上锁。现代并发编程的思路是减少共享可变状态而不是把所有操作都包在锁里。5.3 一个日常用到的线程安全工具类模板最后分享一个在Android项目里我经常用来做兜底的线程安全计数器模板既能当参考也能直接改public class ThreadSafeCounter { private final Object lock new Object(); private int count 0; public void increment() { synchronized (lock) { count; } } public int get() { synchronized (lock) { return count; } } }看起来很基础但真正在项目里用的时候我会再加一层设计把count相关的读写全部封装在类内部外部线程只能调用increment和get方法根本拿不到count这个变量本身。这就是“可变状态最小化”的落地——让锁去保护的范围尽可能小同时也让外部代码没有机会绕过锁直接访问数据。我在实际项目中调试过不少偶发性的数据错乱问题绕来绕去最后多半都是回到synchronized的基本功上锁对象选对了没有临界区切得准不准有没有在锁里做不该做的事。把这三点想清楚Android开发里大部分线程安全问题都能迎刃而解。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

从冒泡到哈希:排序与查找算法实战全解析 2026/9/9 22:34:26

从冒泡到哈希:排序与查找算法实战全解析

排序和查找算法,是所有写代码的人绕不开的两座山。从大学期末考到社招技术面,从给Excel里的IP地址排个序到在上亿条日志里定位一条记录,背后翻来覆去就是这么几个经典套路。我从2013年开始正经写项目,到现在手写过冒泡排序、快速排…

阅读更多 →
CRMEB送礼功能实战:从订单链路到运营玩法全拆解 2026/9/9 22:34:26

CRMEB送礼功能实战:从订单链路到运营玩法全拆解

最近圈子里的电商同行之间,聊得最多的除了直播带货就是“送礼”了。尤其是CRMEB这套开源系统更新了送礼功能之后,好几个人来问我:这个送礼到底跟代付有什么区别?真能拉动销量吗?我的回答是:区别大了。代付解…

阅读更多 →
STM32与ATSHA204A加密芯片的挑战-响应认证实战 2026/9/9 22:34:26

STM32与ATSHA204A加密芯片的挑战-响应认证实战

简介:一份面向嵌入式开发者的ATSHA204加密芯片中文开发资料包,配套STM32F103例程demo,重点解决芯片选型评估、手册查阅与快速上手的实际需求。压缩包共550个文件,大小仅14.95MB,核心内容包括PDF中文手册、Keil工程文件…

阅读更多 →
2026大规模投票平台怎么选?万人高并发稳定承载能力实测 2026/9/9 22:34:26

2026大规模投票平台怎么选?万人高并发稳定承载能力实测

办一场大规模投票活动,很多主办方最担心的问题不是选手够不够多,而是—— 人来了,页面崩了。 几百人甚至上千人参赛、全校师生或全公司员工同时涌入投票页面——图片加载不出来、点投票没反应、页面直接卡死。前期所有的策划和宣传&#xff0…

阅读更多 →
排序与查找算法全解析:从复杂度权衡到工程实战选型 2026/9/9 22:34:26

排序与查找算法全解析:从复杂度权衡到工程实战选型

排序和查找算法,说穿了就是两件事:把一堆乱序数据整理出规律,然后在有规律的数据里快速找到目标。这两件事几乎是所有程序的基础操作,无论是数据库索引、搜索引擎、日志分析,还是你手机里的通讯录排序,背后…

阅读更多 →
KUKA机器人仿真与离线编程实战:SIM PRO与OfficeLite应用指南 2026/9/9 22:31:26

KUKA机器人仿真与离线编程实战:SIM PRO与OfficeLite应用指南

简介:KUKA库卡机器人仿真软件KUKA.SIM PRO资源包,面向机器人工程师、自动化集成人员及职业院校师生,用于在虚拟环境中完成工作站搭建、离线编程与运动仿真,可在不占用真实产线的情况下验证节拍与干涉问题。包内共2000个文件&#…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞