同步机制:synchronized实现、Lock与Condition、CAS与自旋锁

说到Android Runtime里的同步机制,我其实挺感慨的。早年做性能优化时,经常被各种锁问题搞得焦头烂额。你想想看,一个多线程环境里,如果同步没做好,轻则数据错乱,重则直接死锁,整个App卡死在那。今天我就把这块的核心内容掰开揉碎了讲清楚。

一、synchronized的实现原理

synchronized是Java里最基础的同步手段。但很多人只是会用,不知道它底层怎么工作的。我习惯把它理解成「JVM级别的锁」,在ART里,它的实现其实经历了好几轮优化。

核心要点:synchronized在ART中基于monitor实现,每个对象都有一个monitor与之关联。当线程进入synchronized块时,会尝试获取对象的monitor锁。

具体来说,synchronized的字节码层面是通过monitorentermonitorexit两条指令完成的。但ART在运行时做了大量优化,比如偏向锁、轻量级锁、重量级锁的升级过程。

// 伪代码展示synchronized的底层逻辑
Monitor::Monitor() {
    owner = nullptr;
    recursion = 0;
    wait_set = nullptr;
    lock = new Mutex();
}

void Monitor::Enter(Thread* self) {
    if (owner == self) {
        // 重入
        recursion++;
        return;
    }
    // 尝试获取锁
    lock->Lock();
    owner = self;
    recursion = 1;
}

我在项目中遇到过一个问题:某个模块大量使用synchronized,导致UI线程频繁阻塞。后来发现是因为锁竞争太激烈,锁升级到了重量级,每次都要进入内核态。嗯,这里要注意,synchronized不是不能用,但要控制临界区的大小。

个人经验:我建议在锁竞争不激烈的场景下优先使用synchronized,代码简洁且ART对它做了充分优化。但如果锁竞争激烈,或者需要更灵活的锁控制,就要考虑Lock了。

二、Lock与Condition

Lock接口是Java 5引入的,相比synchronized,它提供了更丰富的功能。说白了,synchronized能做的Lock都能做,但Lock还能做synchronized做不到的事。

特性 synchronized Lock
可中断等待 不支持 支持
超时获取 不支持 支持
公平性 非公平 可配置
条件变量 wait/notify Condition

Condition是Lock的搭档。它解决了传统wait/notify的一些痛点。比如,一个锁可以关联多个Condition,实现更精细的线程唤醒。

// 使用Lock和Condition实现生产者-消费者
class BoundedBuffer {
    private final Lock lock = new ReentrantLock();
    private final Condition notFull = lock.newCondition();
    private final Condition notEmpty = lock.newCondition();
    private final Object[] items = new Object[100];
    private int putIndex, takeIndex, count;

    public void put(Object x) throws InterruptedException {
        lock.lock();
        try {
            while (count == items.length)
                notFull.await();
            items[putIndex] = x;
            if (++putIndex == items.length) putIndex = 0;
            count++;
            notEmpty.signal();
        } finally {
            lock.unlock();
        }
    }

    public Object take() throws InterruptedException {
        lock.lock();
        try {
            while (count == 0)
                notEmpty.await();
            Object x = items[takeIndex];
            if (++takeIndex == items.length) takeIndex = 0;
            count--;
            notFull.signal();
            return x;
        } finally {
            lock.unlock();
        }
    }
}

我曾经在做一个图片加载框架时,就用Condition实现了请求队列的精细控制。当缓存满了,生产者线程等待notFull条件;当缓存空了,消费者线程等待notEmpty条件。这样比用synchronized+wait/notify清晰多了。

避坑指南:我曾经在finally块里忘记调用unlock(),导致线上出现死锁。记住,Lock必须手动释放,而且最好在finally里释放。synchronized是自动释放的,这是它的一大优势。

三、CAS与自旋锁

CAS(Compare And Swap)是硬件级别的原子操作。它做的事情很简单:比较内存中的值是否等于预期值,如果是则更新为新值,否则不操作。整个过程是原子的。

你想想看,如果没有CAS,我们要实现一个线程安全的计数器,就得加锁。有了CAS,就可以用更轻量的方式实现。

// Java中的AtomicInteger底层就是CAS
public class AtomicInteger {
    private volatile int value;
    
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }
    
    private final native boolean compareAndSet(int expect, int update);
}

自旋锁是基于CAS实现的。它的思想很简单:线程在获取锁时,如果锁被占用,就循环等待(自旋),而不是立即挂起。这样做的好处是避免了线程切换的开销。

关键理解:自旋锁适合锁持有时间很短的场景。如果锁持有时间长,自旋会浪费CPU资源。ART内部很多轻量级同步就用了自旋锁。

我习惯把自旋锁和互斥锁做个对比:

  • 自旋锁:不放弃CPU,循环等待。适合短时间锁竞争。
  • 互斥锁:放弃CPU,线程挂起。适合长时间锁竞争。
  • 自适应自旋锁:根据上次自旋成功与否动态调整自旋次数。ART里就有这种实现。

ART的同步机制里,CAS和自旋锁被大量使用。比如在对象头里做锁升级时,就是用CAS来修改锁状态的。我记得有一次调试一个性能问题,发现某个热点路径上自旋次数太多,后来调整了自旋阈值,性能提升了20%。

个人建议:在Android开发中,如果你需要实现一个简单的线程安全计数器或者状态标记,优先用AtomicXXX类。它们基于CAS实现,比synchronized轻量得多。但要注意ABA问题,虽然大多数场景下不影响。

四、知识体系总览

下面这张图总结了ART同步机制的核心脉络,从底层硬件到上层API,你可以对照着理解:

ART同步机制知识体系 硬件基础层 CAS指令(Compare And Swap) | 内存屏障 | 缓存一致性协议 JVM/ART实现层 对象头(Mark Word) | 锁升级:偏向锁 → 轻量级锁 → 重量级锁 Monitor机制 | 自旋锁 | 自适应自旋 Java API层 synchronized(隐式锁) | Lock接口(显式锁) | Condition条件变量 AtomicInteger/AtomicLong | ReentrantLock | ReadWriteLock 应用场景 线程安全集合 | 生产者-消费者 | 计数器 | 状态同步 | 锁分段

从这张图可以清楚看到,ART的同步机制是层层递进的。底层硬件提供CAS指令,ART在此基础上实现锁升级和自旋策略,再往上封装成synchronized和Lock API,最终服务于各种并发场景。

我个人觉得,理解同步机制的关键不在于记住API怎么用,而在于理解每一层解决了什么问题。比如,为什么要有偏向锁?因为大多数情况下锁只有单个线程访问。为什么要有自旋锁?因为线程切换的成本比自旋几圈高得多。

好了,同步机制这块就讲到这里。记住,没有银弹,每种同步方式都有它的适用场景。选对工具,比会用工具更重要。