4-8.【OC】【锁】为什么 @synchronized(nil) 不起作用?

5 阅读2分钟

这个问题直指 @synchronized 底层的实现逻辑。简单来说,因为它遵循了 Objective-C 的 “向 nil 发送消息不执行操作” 以及 “基于对象地址查找锁” 的原则。

如果传入的是 nil,加锁流程在第一步就断掉了。


1. 源码层面的“空指针检查”

objc-sync.mm 源码中,objc_sync_enterobjc_sync_exit 的第一行逻辑几乎都是一样的:

C++

int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) { // <--- 关键就在这里
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();
    } else {
        // 如果 obj 是 nil,什么也不做
        if (DebugNilSync) {
            _objc_inform("NIL SYNC ENTER");
        }
    }

    return result;
}

发生了什么?

  • 不加锁: 当你执行 @synchronized(nil) 时,obj 为假,if 分支直接跳过。
  • 不报错: 它不会像 Java 那样抛出 NullPointerException,而是保持 Objective-C 的传统:对 nil 的操作保持沉默(No-op)。
  • 失去保护: 既然 objc_sync_enter 没有执行任何加锁逻辑,那么接下来的代码块(临界区)就处于完全没有同步保护的状态,多个线程可以同时进入。

2. 为什么不能为 nil 分配一个“全局锁”?

你可能会问:“既然是 nil,系统能不能分配一个默认的全局锁给它?”

苹果的设计者没有这样做,原因有二:

  1. 歧义性: 如果所有 @synchronized(nil) 都共享同一个全局锁,那么原本互不相关的业务逻辑会因为这个全局锁产生严重的性能瓶颈(虚假竞争)。
  2. 契约精神: @synchronized 的本意是**“以该对象为令牌进行同步”**。如果你连令牌(对象)都没有,那么“同步”这个行为本身在逻辑上就是不成立的。

3. 实战中的隐患:对象在执行中变 nil

这是一个非常经典的 Bug 场景:

Objective-C

@synchronized(self.someData) {
    // 处理数据
}

如果 self.someData 在多线程环境下被另一个线程置为了 nil

  1. 线程 A 进来时 someData 有值,成功加锁。
  2. 线程 B 进来时 someData 刚好被清空变成了 nil
  3. 后果: 线程 B 会绕过锁,直接冲入临界区,与线程 A 产生数据竞争。

最佳实践: 永远不要对一个可能变为 nil 的属性(Property)加锁。如果你需要保护一个资源,应该使用一个永远存在的专用锁对象

Objective-C

// 正确做法:使用一个私有的、生命周期稳定的变量作为锁
@synchronized(_lockHandle) { ... }

总结

@synchronized(nil) 不起作用是因为底层源码里有一个显式的 if (obj) 检查。它不抛错、不阻塞、也不加锁。

这让它成了一个“透明”的开关:有对象时是锁,没对象时是空气。