第2章 线程同步基础:锁、信号量、条件变量适用场景与Java/C++原语对比

在多语言RTL(Right-to-Left)环境下,TalkBack的读屏线程与UI渲染线程、计算线程之间需要精确同步,否则会导致“抢读”、“跳读”、朗读顺序错乱甚至ANR。理解锁(Lock)、信号量(Semaphore)、条件变量(Condition Variable)的适用场景,以及Android Java与C++(NDK)同步原语的差异,是实现线程安全RTL引擎的基石。

关键认知:同步原语不是银弹。锁保障互斥,信号量控制许可数量,条件变量用于线程间等待/通知。选错原语会导致死锁、性能退化或逻辑错误。RTL场景尤其需要关注读线程被阻塞时不允许持有锁时间过长。

2.1 核心原理与适用场景

三种基元解决不同维度的并发问题:

原语解决的问题经典适用场景
锁(Mutex / ReentrantLock)互斥访问共享资源保护RTL排版数据、朗读缓存区;同一时刻只允许一个线程修改全文方向标记表。
信号量(Semaphore)控制对资源池的并发访问数量限制RTL文本分页中的字形提取线程数;控制异步朗读任务的并发度。
条件变量(ConditionVariable)线程等待某个条件成立时再继续执行TalkBack发音线程等待渲染线程完成RTL布局更新后再朗读;避免在“脏”文字上发声。
RTL专有提示:条件变量常配合while(condition)循环使用,防止虚假唤醒。在TalkBack中,若UI线程使用signal()通知RTL重排完成,读屏线程必须在while(!reflowDone)内等待。

2.2 实现流程与代码示例

2.2.1 Android Java 实现

锁(ReentrantLock + tryLock)

import java.util.concurrent.locks.ReentrantLock;

final ReentrantLock rtlLock = new ReentrantLock();
boolean acquired = false;
try {
    acquired = rtlLock.tryLock(200, TimeUnit.MILLISECONDS);
    if (acquired) {
        // 修改RTL方向标记表
    } else {
        // 上报超时,不阻塞主线程
        Log.w("RTL", "尝试获取锁超时,跳过本次更新");
    }
} finally {
    if (acquired) rtlLock.unlock();
}

信号量(Semaphore)

import java.util.concurrent.Semaphore;

final Semaphore glyphExtractors = new Semaphore(2); // 最多2个线程同时提取字形
glyphExtractors.acquire(); // 可能阻塞
try {
    // 提取该段RTL字符字形
} finally {
    glyphExtractors.release();
}

条件变量(Condition + ReentrantLock)

import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;

final ReentrantLock lock = new ReentrantLock();
final Condition reflowDone = lock.newCondition();
volatile boolean isReflowComplete = false;

// 渲染线程:
lock.lock();
try {
    // 完成RTL重排
    isReflowComplete = true;
    reflowDone.signalAll();
} finally {
    lock.unlock();
}

// TalkBack朗读线程:
lock.lock();
try {
    while (!isReflowComplete) {
        reflowDone.await(); // 释放锁并等待信号
    }
    // 继续朗读
} finally {
    lock.unlock();
}

2.2.2 C++ NDK 实现

锁(std::mutex + std::lock_guard / std::unique_lock)

#include <mutex>

std::mutex rtl_mutex;
void update_rtl_table() {
    std::lock_guard<std::mutex> guard(rtl_mutex);
    // 更新RTL方向表
}

信号量(无标准实现,可用 std::counting_semaphore C++20 或自定义)

#include <semaphore> // C++20

std::counting_semaphore<2> glyph_sem(2); // 最大并发2
void extract_glyph() {
    glyph_sem.acquire();
    // 提取字形
    glyph_sem.release();
}

条件变量(std::condition_variable + std::unique_lock)

#include <condition_variable>
#include <mutex>

std::mutex cv_mtx;
std::condition_variable cv;
bool reflow_ready = false;

// 渲染线程
{
    std::lock_guard<std::mutex> lk(cv_mtx);
    reflow_ready = true;
    cv.notify_all();
}

// TalkBack线程
{
    std::unique_lock<std::mutex> lk(cv_mtx);
    cv.wait(lk, []{ return reflow_ready; });
    // 继续
}

2.3 Android Java 与 C++ 同步原语对比

功能Android JavaC++ (NDK标准库)
互斥锁ReentrantLock / synchronizedstd::mutex,std::recursive_mutex
持有锁的RAII包装try{}finally{unlock} 手动std::lock_guard / std::unique_lock
信号量java.util.concurrent.Semaphorestd::counting_semaphore (C++20)
条件变量Condition (从Lock获得)std::condition_variable
超时获取锁tryLock(time,unit)try_lock_for / try_lock_until
线程局部对象ThreadLocalthread_local 关键字
事件通知Object.wait/notify/notifyAll无原生等价,常用条件变量
常见错误:Java中Object.wait()必须在同步块内调用;C++中wait()必须持有unique_lock且条件谓词优先写在lambda中,避免丢失唤醒。在RTL场景中,误用synchronized与非公平锁可能导致优先级反转——低优先级的UI线程长期占据锁,高优先级的TalkBack线程饥饿。

2.4 调试与性能优化

调试技巧

  • 启用死锁检测:Java中可使用ThreadMXBean.findDeadlockedThreads();C++中使用AddressSanitizer + ThreadSanitizer。
  • 统计锁竞争:Android Studio Profiler的Lock Contention视图显示等待时间;C++中可包装std::mutex并记录阻塞时长。
  • 信号量泄漏:确保release()放在finally或RAII作用域内,否则资源数逐渐耗尽。

性能优化策略

策略说明RTL适用示例
减小临界区锁内只做必要操作,移除日志/IORTL方向表更新时仅赋值,不上报日志
读写锁分离读多写少时使用ReadWriteLockTalkBack线程频繁读取,排版线程偶尔写入
无锁数据结构利用AtomicReference或CAS更新单个RTL状态标记用AtomicBoolean替代锁
避免嵌套锁分级加锁或使用调用顺序文档统一锁定顺序:先UI锁后读线程锁
稳定性要点:在C++中如果condition_variable::wait抛出异常(如系统内存不足),必须捕获并清理锁状态,否则锁永远无法释放。Java的ReentrantLock在异常时需确保finally中调用unlock。建议使用tryLock(timeout)防止因锁竞争导致ANR或严重延迟。

2.5 同步原语适用决策图

同步原语适用决策图 选择同步原语决策树 需要同步吗? 互斥访问共享资源? 读多写少 → ReadWriteLock 写频繁 → 普通Mutex 控制并发访问数量? 使用 Semaphore 线程需要等待条件? 使用 ConditionVariable RTL适配提示 • 读频繁(TalkBack每帧读RTL方向表)→ 优先ReadWriteLock或Atomic变量,避免热点锁。 • 并发字形提取任务 → 信号量限制线程池大小,防止过度并行OOM。 • TalkBack等待UI完成重排 → 条件变量+超时等待,防止永久挂起。