第2章 Java与Native线程同步基础:synchronized、ReentrantLock、Condition、Mutex、读写锁

SystemUI作为系统持久层界面,运行空间横跨Java(SystemServer/SystemUI)、Native(SurfaceFlinger/渲染线程)以及JNI中继层。任何一个同步原语使用不当,都可能导致UI线程阻塞、通知面板卡顿甚至系统重启。本章从Java和Native两个维度对比核心同步原语,建立跨语言“翻译”能力,为后续分析Binder调度与线程死锁夯实基础。

本章目标:掌握Java与C++同步原语的映射关系,能够在SystemUI源码中快速识别锁的使用方式,并具备跨语言死锁排查的初步能力。
SystemUI 跨语言同步原语映射体系 SystemUI 线程上下文 UI 主线程 Binder 线程池 RenderThread / 后台线程 Java / Kotlin 运行时 synchronized ReentrantLock Condition ReadWriteLock JNI 桥梁 / 系统服务本地调用 (MonitorEnter/Exit) Native (C++ / Rust) 层 std::mutex / pthread_mutex_t ObjectMonitor (AQS) condition_variable pthread_rwlock_t Linux 内核调度器与同步原语 futex (快速用户空间互斥体) pthread (NPTL) 💡 关键理解:Java Lock 最终依赖操作系统 futex 进行线程阻塞;掌握映射关系有助于跨语言死锁诊断。

2.1 Java层同步原语

SystemUI中大量使用Java同步机制保护通知集合、面板状态等共享对象。理解其底层的“锁升级”过程对性能调优至关重要。

  • synchronized:基于对象Monitor实现。JVM偏向锁→轻量锁(CAS自旋)→重量锁(挂起线程)。适合临界区小、竞争低的场景。
  • ReentrantLock:基于AQS(AbstractQueuedSynchronizer)实现。支持公平/非公平、可中断、多Condition绑定。SystemUI中较复杂的状态保护常用此锁。
  • Condition:必须与ReentrantLock配合使用。经典生产者-消费者模式,提供await()/signal()/signalAll(),比Object.wait()更灵活且支持超时。
  • ReentrantReadWriteLock:读读不互斥,读写/写写互斥。适用于读多写少的场景(如常量配置、缓存数据)。

2.2 Native层同步原语

SystemUI的渲染管线(RenderThread)与部分系统服务(SurfaceFlinger、InputFlinger)运行在Native层。JNI临界区操作也直接依赖这些原语。

  • std::mutex / pthread_mutex_t:C++11标准封装,底层调用futex快速路径。建议配合std::lock_guard或std::unique_lock实现RAII管理。
  • std::condition_variable:必须与std::unique_lock<std::mutex>配合。wait()自动解锁并在唤醒时重新加锁,有效避免信号丢失。
  • pthread_rwlock_t:读写锁Native实现。需注意其偏向写者,在某些实现中可能造成读者饥饿。
性能直觉: Java synchronized 在低竞争时通过偏向锁/轻量锁实现几乎无开销;Native std::mutex 在无竞争时仅需原子操作 + 用户空间CAS,通常在10-100ns级别。高竞争下两者均会陷入内核(futex),SystemUI应尽量避免高竞争锁设计。

2.3 核心对比表:synchronized vs ReentrantLock vs std::mutex

维度Java synchronizedJava ReentrantLockNative std::mutex
可中断不支持(除非使用Lock API)支持 (lockInterruptibly)不支持(除非调时版本)
公平性非公平可配置为公平非公平
条件变量Object.wait/notify多Condition绑定condition_variable
低竞争开销偏向锁 ≈ 10nsCAS ≈ 20ns用户空间CAS ≈ 10ns
高竞争开销重量锁 (内核挂起)AQS队列 + parkfutex (内核挂起)
使用约束JVM自动加解锁必须在finally中解锁RAII或手动解锁
调试友好Thread Dump直接显示依赖AQS工具类ptrace / debuggerd

2.4 跨语言映射与JNI临界区实战

在SystemUI开发中,JNI临界区是跨语言死锁的重灾区。Java通过JNI调用Native函数时,如果Native函数需要获取Java对象的Monitor,就必须使用MonitorEnter/MonitorExit。反之,Native代码回调Java方法时,也容易在Java层造成意外阻塞。

JNI死锁经典陷阱: Native线程A持有Native锁L1,尝试通过JNI获取Java对象M1的Monitor。Java线程B持有M1的Monitor,尝试调用Native方法获取锁L1。此为AB-BA死锁。务必保证跨语言加锁顺序全局一致,或使用JNIEnv::ExceptionCheck检测异常。

Java端典型使用:SystemUI中的任务队列等待

public class TaskCompletionController {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition allTasksDone = lock.newCondition();
    private int pendingTaskCount = 0;

    public void waitForCompletion(long timeoutMs) throws InterruptedException {
        lock.lock();
        try {
            while (pendingTaskCount > 0) {
                if (!allTasksDone.await(timeoutMs, TimeUnit.MILLISECONDS)) {
                    throw new RuntimeException("任务执行超时");
                }
            }
        } finally {
            lock.unlock();
        }
    }

    public void onTaskFinished() {
        lock.lock();
        try {
            if (--pendingTaskCount == 0) {
                allTasksDone.signalAll();
            }
        } finally {
            lock.unlock();
        }
    }
}

Native端等价的实现方式:

class TaskCompletionController {
public:
    void waitForCompletion(std::chrono::milliseconds timeout) {
        std::unique_lock<std::mutex> lock(mtx_);
        while (pending_task_count_ > 0) {
            if (cv_.wait_for(lock, timeout) == std::cv_status::timeout) {
                throw std::runtime_error("任务执行超时");
            }
        }
    }

    void onTaskFinished() {
        std::lock_guard<std::mutex> lock(mtx_);
        if (--pending_task_count_ == 0) {
            cv_.notify_all();
        }
    }
private:
    std::mutex mtx_;
    std::condition_variable cv_;
    int pending_task_count_ = 0;
};

2.5 调试与排查要点

  • Java线程转储:kill -3 PID 或 adb shell dumpsys activity service SystemUIService,直接显示锁等待链。
  • Native奔溃分析:debuggerd -b PID 可打印Native线程堆栈与锁持有信息(需要开启libc.debug.malloc)。
  • Perfetto / Systrace:通过adb shell perfetto追踪futex系统调用,识别高竞争锁热点。
  • 常见日志关键字:Waiting on condition、held by thread、monitor contention、blocked on lock。
稳定性建议: SystemUI中所有可能阻塞主线程的锁操作,建议加超时保护(例如tryLock或await(timeout)),防止线程永久挂起。对于高频访问的共享变量(如面板可见性标志),优先考虑volatile或AtomicBoolean,避免锁膨胀。