Android锁

引言 🔒

在 Android 应用的开发过程中,随着业务需求的复杂度不断提升,多线程并发场景层出不穷。为了保证数据一致性与线程安全,锁(Lock)成为了不可或缺的工具。本篇博客将深入剖析 Android 中常用的锁机制、使用场景与最佳实践,并配以精炼的示例代码与示意图,帮助你快速掌握锁的精髓。


一、锁的基本概念

1. 什么是锁?

锁是一种同步机制,用于控制多个线程对共享资源的访问,确保在同一时刻只有一个线程能够进入临界区(Critical Section),从而避免数据竞争与不一致。

2. 锁的作用

  • 互斥访问:保证同一资源在同一时刻仅被一个线程修改。
  • 可见性保证:在释放锁后,修改对其他线程可见。
  • 阻塞与等待:线程无法获取锁时,会被挂起或进入等待队列,直至锁可用。

3. 为什么在 Android 开发中需要锁?

  • UI 线程与后台线程交互需保证同步;

  • 缓存、数据库、网络调用等资源共享需防止数据竞争;

  • 多进程组件(ContentProvider、AIDL)之间的同步需求。


二、线程与并发基础知识回顾 🧵

在深入锁之前,先回顾 Android 常用的线程与并发工具:

  1. Thread :直接创建线程,使用 new Thread(runnable).start()
  2. Handler & Looper:用于在特定线程(通常是主线程)中传递消息与任务。
  3. HandlerThread:封装了 Looper 的后台线程。
  4. ExecutorService(线程池) :包括 ThreadPoolExecutorScheduledThreadPoolExecutor 等,可复用线程,控制并发度。
java 复制代码
// 简单线程池示例
ExecutorService pool = Executors.newFixedThreadPool(4);
pool.submit(() -> {
    // 后台任务
});
pool.shutdown();

🔍 图示:线程模型示意图(伪代码注释提示)

复制代码
[UI线程] <--Handler--> [HandlerThread] ---> ThreadPool

三、Java 中的锁机制 🧩

3.1 synchronized

  • 定义:Java 语言关键字,隐式获取对象或类的监视器锁。
  • 语法:方法锁、代码块锁。
java 复制代码
// 方法锁
public synchronized void doWork() { ... }

// 块锁
synchronized (lockObject) {
    // 临界区
}

✅ 优点:语法简洁,隐式释放锁;

⚠️ 缺点:不可中断、不支持公平锁、功能有限。

3.2 ReentrantLock

  • 定义 :Java java.util.concurrent.locks 包中的可重入锁。
  • 特性 :支持公平锁、可中断锁、尝试加锁(tryLock())。
java 复制代码
ReentrantLock lock = new ReentrantLock(true); // fair = true
try {
    lock.lockInterruptibly();
    // 临界区
} finally {
    lock.unlock();
}

🔄 可重入性:同一线程可多次获取锁;

⏰ 可中断:获取锁时可响应中断。

3.3 ReadWriteLock

  • 定义:读写分离锁,允许多个读线程并发,写线程独占。
java 复制代码
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();

readLock.lock();
try { ... } finally { readLock.unlock(); }

writeLock.lock();
try { ... } finally { writeLock.unlock(); }

📖 场景:读多写少的数据访问,如缓存读取。

3.4 Semaphore

  • 定义:计数信号量,控制同时访问某资源的线程数。
java 复制代码
Semaphore sem = new Semaphore(3);
sem.acquire();
try { ... } finally { sem.release(); }

🎛️ 场景:限制并发连接数、线程池实现原理。

3.5 CountDownLatch

  • 定义:线程间等待工具,等待其他线程执行完毕后继续。
java 复制代码
CountDownLatch latch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
    new Thread(() -> {
        // 任务
        latch.countDown();
    }).start();
}
latch.await(); // 等待所有子线程完成

🔐 场景:并行准备+主线程等待。

3.6 CyclicBarrier

  • 定义:可循环使用的屏障,等待一组线程到达同一点后再继续。
java 复制代码
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    // 全部线程到达后执行
});

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 工作
        barrier.await();
    }).start();
}

🚧 场景:多线程分阶段计算。


四、锁在 Android 开发中的典型应用场景 🧠

4.1 多线程更新 UI 的同步

  • 问题:Android UI 只能在主线程更新,后台线程需切换。
  • 方案 :使用 runOnUiThread()Handlersynchronized 等保证顺序。
java 复制代码
synchronized (uiLock) {
    runOnUiThread(() -> {
        // 更新 UI
    });
}

💡 示例图:UI线程与后台线程交互时序图。

4.2 数据缓存的并发访问控制

  • 场景:内存缓存如 LruCache,多线程同时读取和写入。
  • 方案ReadWriteLock 分离读写;或 ConcurrentHashMap
java 复制代码
private final ReadWriteLock cacheLock = new ReentrantReadWriteLock();

public Bitmap get(String key) {
    cacheLock.readLock().lock();
    try { return cache.get(key);}
    finally { cacheLock.readLock().unlock(); }
}

4.3 多进程同步

  • 场景 :使用 ContentProvider 或 AIDL 实现进程间通信(IPC),需要同步访问共享资源。
  • 方案 :在 ContentProvider 中使用数据库事务锁;在 AIDL 服务端使用 synchronizedReentrantLock

📦 图示:AIDL 服务多进程调用时序图。

4.4 网络请求和数据库操作的线程安全处理

  • 网络请求:使用 OkHttp 自带连接池,限制并发;
  • 数据库:Room 默认支持多线程读写,事务内操作自动加锁;
  • 手动加锁 :对执行顺序敏感时,可使用 Semaphore 控制并发量。
java 复制代码
Semaphore netSem = new Semaphore(10);
netSem.acquire();
try {
    Response resp = okHttpClient.newCall(request).execute();
} finally {
    netSem.release();
}

五、synchronized vs ReentrantLock 对比 📦

特性 synchronized ReentrantLock
公平性 不可选公平 可选公平(fair 参数)
可中断 不可中断 支持 lockInterruptibly()
尝试加锁 不支持 支持 tryLock()
条件变量 wait/notify Condition 对象
性能 JDK 自动优化 需要手动释放锁

六、死锁与避免技巧 🔁

6.1 死锁的形成条件

  1. 互斥条件;
  2. 占有且等待;
  3. 不可剥夺;
  4. 循环等待;

6.2 常见案例

java 复制代码
synchronized(a) {
    synchronized(b) { ... }
}

synchronized(b) {
    synchronized(a) { ... }
}

6.3 预防方法

  • 锁顺序:统一的资源加锁顺序;
  • 定时锁尝试tryLock(timeout)
  • 资源分离:减少互斥区域;
  • 死锁检测:借助工具(MAT、ThreadMXBean)。

七、最佳实践建议 💡

  1. 尽量避免锁 :优先使用无锁数据结构(ConcurrentHashMapAtomicXXX);
  2. 控制粒度:最小化同步块;
  3. 读写分离 :多读少写场景优先 ReadWriteLock
  4. 使用线程池:减少线程创建销毁成本;
  5. 文档与注释:明确锁的作用与风险;
  6. 监控与调优:借助工具监测锁竞争;

八、实用示例代码 🧪

8.1 高性能缓存示例

java 复制代码
public class SafeCache<K, V> {
    private final Map<K, V> cache = new HashMap<>();
    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    public V get(K key) {
        lock.readLock().lock();
        try { return cache.get(key);}
        finally { lock.readLock().unlock(); }
    }

    public void put(K key, V value) {
        lock.writeLock().lock();
        try { cache.put(key, value);}
        finally { lock.writeLock().unlock(); }
    }
}

注释:读写分离,保证多线程并发读取高效。

8.2 定时死锁尝试示例

java 复制代码
ReentrantLock lockA = new ReentrantLock();
ReentrantLock lockB = new ReentrantLock();

void safeOperation() {
    try {
        if (lockA.tryLock(500, TimeUnit.MILLISECONDS)) {
            try {
                if (lockB.tryLock(500, TimeUnit.MILLISECONDS)) {
                    try {
                        // 安全操作
                    } finally { lockB.unlock(); }
                }
            } finally { lockA.unlock(); }
        }
    } catch (InterruptedException ignored) {}
}

九、高级内容拓展 🚀

9.1 Kotlin 协程与锁机制对比

  • 协程调度:轻量级线程,挂起而非阻塞;
  • Mutex :协程专用锁,支持 lockwithLock
kotlin 复制代码
val mutex = Mutex()
suspend fun safeWrite() {
    mutex.withLock {
        // 协程内安全写操作
    }
}

📊 对比表:传统锁 vs 协程 Mutex

9.2 使用 Channel 替代锁

  • 场景:消息传递代替共享内存;
kotlin 复制代码
val channel = Channel<Int>()
launch {
    for (msg in channel) { println(msg) }
}
launch {
    channel.send(42)
}
相关推荐
檀越剑指大厂1 小时前
容器化 Android 开发效率:cpolar 内网穿透服务优化远程协作流程
android
MiyamuraMiyako2 小时前
从 0 到发布:Gradle 插件双平台(MavenCentral + Plugin Portal)发布记录与避坑
android
NRatel3 小时前
Unity 游戏提升 Android TargetVersion 相关记录
android·游戏·unity·提升版本
叽哥5 小时前
Kotlin学习第 1 课:Kotlin 入门准备:搭建学习环境与认知基础
android·java·kotlin
风往哪边走6 小时前
创建自定义语音录制View
android·前端
用户2018792831676 小时前
事件分发之“官僚主义”?或“绕圈”的艺术
android
用户2018792831676 小时前
Android事件分发为何喜欢“兜圈子”?不做个“敞亮人”!
android
Kapaseker7 小时前
你一定会喜欢的 Compose 形变动画
android
QuZhengRong8 小时前
【数据库】Navicat 导入 Excel 数据乱码问题的解决方法
android·数据库·excel
zhangphil9 小时前
Android Coil3视频封面抽取封面帧存Disk缓存,Kotlin(2)
android·kotlin