多线程的作用
多线程的主要目的是提高 CPU 的复用率。即使只有一个 CPU,多线程也能通过时间分片的方式实现并发执行,从而在同一时间段内处理多个任务。然而,多线程也带来了数据一致性和可见性问题,这就需要 Java 内存模型(JMM)和锁机制来解决。
Android 开发中的 锁机制 和 内存模型 是保证多线程安全和高性能的关键。以下分两部分详细讲解:
一、Android 锁机制
锁用于多线程环境下控制对共享资源的访问,防止数据竞争和不一致。Android 基于 Java,支持多种锁机制:
1. synchronized 关键字
- 作用:通过对象监视器(Monitor)实现同步,确保同一时刻只有一个线程执行临界区代码。
- 底层原理:
- 每个对象关联一个监视器锁(Monitor),通过
monitorenter和monitorexit字节码指令实现。 - 在 Android ART 运行时中,锁可能经历 偏向锁 → 轻量级锁(自旋锁)→ 重量级锁 的优化过程,减少上下文切换开销。
- 每个对象关联一个监视器锁(Monitor),通过
- 锁的优化:
- 锁膨胀:从无锁状态到偏向锁、轻量级锁,最终膨胀为重量级锁。
- 偏向锁:只有一个线程访问时,锁会偏向该线程,减少同步开销。
- 轻量级锁:多个线程竞争时,通过 CAS(Compare-And-Swap)自旋尝试获取锁。
- 重量级锁:竞争激烈时,锁会升级为重量级锁,涉及操作系统层面的同步。
- 锁粗化:将连续的同步代码块合并,减少加锁和解锁的次数。
- 锁消除:JVM 检测到不需要同步时,会自动移除锁。
- 锁膨胀:从无锁状态到偏向锁、轻量级锁,最终膨胀为重量级锁。
- 示例:
public synchronized void syncMethod() { /* 临界区代码 */ } // 或同步代码块 synchronized (lockObject) { /* 临界区代码 */ }
2. ReentrantLock 与显式锁
- 特点:
- 更灵活:支持可中断锁、超时获取锁、公平锁(避免线程饥饿)。
- 必须手动释放锁(
lock()和unlock()配对)。
- 优势:细粒度控制锁的获取和释放。
- 示例:
ReentrantLock lock = new ReentrantLock(true); // 公平锁 lock.lock(); try { // 临界区代码 } finally { lock.unlock(); }
3. 读写锁(ReentrantReadWriteLock)
- 适用场景:读多写少的场景,允许多个读线程同时访问,写线程独占。
- 示例:
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); rwLock.readLock().lock(); // 获取读锁 rwLock.writeLock().lock(); // 获取写锁
4. volatile 关键字
- 作用:确保变量的可见性(直接读写主内存),禁止指令重排序。
- 局限性:
- 不保证原子性(例如
volatile int i; i++仍需同步)。 - 无法解决复合操作的线程安全问题。
- 不保证原子性(例如
- 适用场景:适用于状态标志、单次写入多次读取的场景。
5. CAS(Compare-And-Swap)
- 原理:通过比较并替换的方式实现无锁同步。
- 问题:存在 ABA 问题(即一个值从 A 变为 B 又变回 A,CAS 无法察觉)。
- 解决方案:使用版本号或标记位来避免 ABA 问题。
6. Android 特有机制
Handler与消息队列:通过单一线程的消息循环(如主线程的Looper)实现隐式同步。Atomic原子类:如AtomicInteger,基于 CAS(Compare-And-Swap)实现无锁线程安全操作。
二、Android 内存模型
Android 内存模型基于 Java 内存模型(JMM),但需要考虑 Dalvik/ART 虚拟机和设备硬件的特性。
1. Java 内存模型(JMM)核心概念
- 主内存与工作内存:
- 所有变量存储在主内存,线程操作的是工作内存中的副本。
- 线程间通信需通过主内存(
volatile变量直接读写主内存)。
- Happens-Before 原则:
- 定义操作间的可见性顺序,如锁释放先于锁获取、
volatile写先于读等。
- 定义操作间的可见性顺序,如锁释放先于锁获取、
2. volatile 的内存语义
- 写操作:刷新到主内存,并使其他线程的工作内存中该变量失效。
- 读操作:从主内存重新加载。
3. Final 字段的特殊处理
- 正确构造的对象,其
final字段对其他线程立即可见(无需同步)。
4. Android 内存管理的特殊性
- ART 的优化:
- AOT(Ahead-Of-Time)编译优化可能影响内存访问顺序。
- 垃圾回收(GC)可能导致线程暂停(Stop-The-World)。
- 低内存设备限制:
- 需避免内存泄漏(如未注销的监听器),防止 OOM。
5. 多线程编程实践
- 避免锁竞争:
- 缩小同步块范围,使用
ConcurrentHashMap等并发容器。 - 避免在同步块中调用外部方法(如网络请求)。
- 缩小同步块范围,使用
- 内存可见性:
- 优先使用
volatile或原子类替代锁。 - 使用
ThreadLocal存储线程私有数据。
- 优先使用
三、常见问题与解决方案
1. 死锁
- 条件:互斥、持有并等待、不可剥夺、循环等待。
- 解决:按固定顺序获取锁,使用超时锁(
tryLock())。
2. ANR(应用无响应)
- 原因:主线程被同步锁或耗时操作阻塞。
- 解决:异步任务使用
AsyncTask、协程或ExecutorService。
3. 内存泄漏
- 场景:非静态内部类持有外部类引用(如 Handler)。
- 解决:使用静态内部类 + 弱引用(
WeakReference)。
四、对象头与锁
Java 对象在内存中的布局分为三部分:
- 对象头:
- Mark Word:存储对象的哈希码、GC 分代年龄、锁标志等信息。
- Klass Pointer:指向对象的类元数据。
- 数组长度(如果是数组对象)。
- 实例数据:存储对象的字段数据。
- 对齐填充:确保对象大小为 8 字节的倍数。
锁的状态信息存储在对象头的 Mark Word 中,根据锁的状态(无锁、偏向锁、轻量级锁、重量级锁),Mark Word 的内容会有所不同。
五、工具与调试
- Android Profiler:监控线程状态和锁竞争。
- StrictMode:检测主线程中的磁盘/网络操作。
- 锁的替换策略:在低竞争场景用
ReentrantLock,高竞争场景用读写锁。