在 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;
-
线程 A 执行
getter拿到count为 0。 -
线程 B 执行
getter拿到count为 0。 -
线程 A 计算
0 + 1,执行setter将count设为 1。 -
线程 B 计算
0 + 1,执行setter将count设为 1。结果:两次加法后
count是 1 而不是 2。因为getter和setter之间存在空窗期,锁已经被释放了。
B. 无法保证对象内部状态安全
atomic 只保护“指针本身”的替换。
如果属性是一个 NSMutableArray:
-
线程 A 调用
[self.list addObject:@"item"](这不属于 Setter/Getter)。 -
线程 B 调用
[self.list removeAllObjects]。这种情况下,
atomic完全起不到保护作用,程序依然会因为多线程竞争导致崩溃。
C. 外界手动干预
如果一个属性被声明为 atomic,但你在代码中直接通过成员变量 _count = 10 来修改,这绕过了 Setter 方法,也就绕过了锁,线程安全荡然无存。
3. atomic 与 nonatomic 的对比
| 特性 | atomic (默认) | nonatomic |
|---|---|---|
| 读写安全性 | 保证 Getter/Setter 完整性(不会读到垃圾值) | 不保证(高并发下可能导致崩溃或数据错乱) |
| 性能 | 较慢(涉及获取全局锁、内存屏障等) | 较快(直接内存读写) |
| 线程安全 | 仅保证原子读写 | 不安全 |
4. 总结与开发建议
- 不要依赖
atomic实现线程安全:如果你需要真正的线程安全,应该使用@synchronized、NSLock、dispatch_queue或os_unfair_lock来手动管理临界区。 - iOS 开发首选
nonatomic:在移动端,CPU 资源珍贵,atomic的锁开销(特别是全局锁池的竞争)会降低性能。绝大多数场景下,我们会在业务层通过 GCD 队列来管理并发,因此属性通常声明为nonatomic。