第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 Java | C++ (NDK标准库) |
|---|---|---|
| 互斥锁 | ReentrantLock / synchronized | std::mutex,std::recursive_mutex |
| 持有锁的RAII包装 | try{}finally{unlock} 手动 | std::lock_guard / std::unique_lock |
| 信号量 | java.util.concurrent.Semaphore | std::counting_semaphore (C++20) |
| 条件变量 | Condition (从Lock获得) | std::condition_variable |
| 超时获取锁 | tryLock(time,unit) | try_lock_for / try_lock_until |
| 线程局部对象 | ThreadLocal | thread_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适用示例 |
|---|---|---|
| 减小临界区 | 锁内只做必要操作,移除日志/IO | RTL方向表更新时仅赋值,不上报日志 |
| 读写锁分离 | 读多写少时使用ReadWriteLock | TalkBack线程频繁读取,排版线程偶尔写入 |
| 无锁数据结构 | 利用AtomicReference或CAS更新 | 单个RTL状态标记用AtomicBoolean替代锁 |
| 避免嵌套锁 | 分级加锁或使用调用顺序文档 | 统一锁定顺序:先UI锁后读线程锁 |
稳定性要点:在C++中如果
condition_variable::wait抛出异常(如系统内存不足),必须捕获并清理锁状态,否则锁永远无法释放。Java的ReentrantLock在异常时需确保finally中调用unlock。建议使用tryLock(timeout)防止因锁竞争导致ANR或严重延迟。