同步机制:synchronized实现、Lock与Condition、CAS与自旋锁
说到Android Runtime里的同步机制,我其实挺感慨的。早年做性能优化时,经常被各种锁问题搞得焦头烂额。你想想看,一个多线程环境里,如果同步没做好,轻则数据错乱,重则直接死锁,整个App卡死在那。今天我就把这块的核心内容掰开揉碎了讲清楚。
一、synchronized的实现原理
synchronized是Java里最基础的同步手段。但很多人只是会用,不知道它底层怎么工作的。我习惯把它理解成「JVM级别的锁」,在ART里,它的实现其实经历了好几轮优化。
核心要点:synchronized在ART中基于monitor实现,每个对象都有一个monitor与之关联。当线程进入synchronized块时,会尝试获取对象的monitor锁。
具体来说,synchronized的字节码层面是通过monitorenter和monitorexit两条指令完成的。但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指令,ART在此基础上实现锁升级和自旋策略,再往上封装成synchronized和Lock API,最终服务于各种并发场景。
我个人觉得,理解同步机制的关键不在于记住API怎么用,而在于理解每一层解决了什么问题。比如,为什么要有偏向锁?因为大多数情况下锁只有单个线程访问。为什么要有自旋锁?因为线程切换的成本比自旋几圈高得多。
好了,同步机制这块就讲到这里。记住,没有银弹,每种同步方式都有它的适用场景。选对工具,比会用工具更重要。