4-25.【OC】【锁】atomic property 的实现是否真正线程安全?为什么?

4 阅读2分钟

在 Objective-C 中,atomic 属性的实现并不是绝对意义上的“线程安全”。

简单来说,atomic 只能保证属性的 Setter 和 Getter 操作本身是原子的(即读写完整),但无法保证更高层级的逻辑业务安全。

1. atomic 的底层实现原理

在 Runtime 源码(objc-accessors.mm)中,atomic 属性的访问是通过一套全局的锁机制实现的:

  • Setter 实现:调用 reallySetProperty,内部会根据属性是否为 atomic 来决定是否加锁。
  • Getter 实现:调用 objc_getProperty,同样会进行加锁操作。

Runtime 内部维护了一个全局的 PropertyLocks(一个 StripedMap<spinlock_t>os_unfair_lock 的变体)。当你读写 atomic 属性时,系统会根据对象地址计算出一个索引,从锁池中取出一把锁来保证该操作的原子性。

2. 为什么它不是“真正”的线程安全?

A. 逻辑组合不安全(Race Condition)

这是最常见的问题。即使 Setter 和 Getter 是原子的,组合操作也不是。

假设有一个 atomic 的整数属性 count

Objective-C

// 线程 A
self.count = self.count + 1;

// 线程 B
self.count = self.count + 1;
  1. 线程 A 执行 getter 拿到 count 为 0。

  2. 线程 B 执行 getter 拿到 count 为 0。

  3. 线程 A 计算 0 + 1,执行 settercount 设为 1。

  4. 线程 B 计算 0 + 1,执行 settercount 设为 1。

    结果:两次加法后 count 是 1 而不是 2。因为 gettersetter 之间存在空窗期,锁已经被释放了。

B. 无法保证对象内部状态安全

atomic 只保护“指针本身”的替换。

如果属性是一个 NSMutableArray

  • 线程 A 调用 [self.list addObject:@"item"](这不属于 Setter/Getter)。

  • 线程 B 调用 [self.list removeAllObjects]

    这种情况下,atomic 完全起不到保护作用,程序依然会因为多线程竞争导致崩溃。

C. 外界手动干预

如果一个属性被声明为 atomic,但你在代码中直接通过成员变量 _count = 10 来修改,这绕过了 Setter 方法,也就绕过了锁,线程安全荡然无存。


3. atomicnonatomic 的对比

特性atomic (默认)nonatomic
读写安全性保证 Getter/Setter 完整性(不会读到垃圾值)不保证(高并发下可能导致崩溃或数据错乱)
性能较慢(涉及获取全局锁、内存屏障等)较快(直接内存读写)
线程安全仅保证原子读写不安全

4. 总结与开发建议

  • 不要依赖 atomic 实现线程安全:如果你需要真正的线程安全,应该使用 @synchronizedNSLockdispatch_queueos_unfair_lock 来手动管理临界区。
  • iOS 开发首选 nonatomic:在移动端,CPU 资源珍贵,atomic 的锁开销(特别是全局锁池的竞争)会降低性能。绝大多数场景下,我们会在业务层通过 GCD 队列来管理并发,因此属性通常声明为 nonatomic