浅谈iOS线程原子锁属性atomic
Objective-C
中,在声明属性时,通常会不假思索的使用nonatomic
来修饰属性,但去面试的时候,总是被问到atomic
,使我们不知从何说起,或者说只能简单的回答一句,atomic
修饰的属性是「原子性」的,不能同时读写等。本篇文章就来探索一下atomic
背后的原理。
1.寻找入口
因为声明属性这个语法是OC
特有的,基本能够确定,对属性的操作是在objc
源码中实现,所以,为了方便探索,我们直接在objc4-756.2
源码中来寻找处理属性的代码。
打开objc-756.2
源码,尝试全局搜索setproperty
关键字:

跟踪其实现,发现所有为属性赋值的方法最终都会走到reallySetProperty()
这个函数:

在工程中新建一个Person
类,为了方便研究,Person
只有一个name
属性,注意它是atomic
的:
// Person.h
@interface Person : NSObject
@property (atomic, copy) NSString *name;
@end
然后在main()
函数中实例化一个Person
并为name
属性赋值:
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
p.name = @"Objc";
}
return 0;
}
然后在reallySetProperty()
这个函数入口打上断点,运行程序,验证一下设置属性时是否会走到这个函数:

可以看到,确实从-[Person setName:]
调用到了reallySetProperty()
这个函数。
2.源码分析
2.1 赋值
reallySetProperty()
的源码:
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
if (offset == 0) {
object_setClass(self, newValue);
return;
}
id oldValue;
id *slot = (id*) ((char*)self + offset);
if (copy) {
newValue = [newValue copyWithZone:nil];
} else if (mutableCopy) {
newValue = [newValue mutableCopyWithZone:nil];
} else {
if (*slot == newValue) return;
newValue = objc_retain(newValue);
}
if (!atomic) {
oldValue = *slot;
*slot = newValue;
} else {
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
oldValue = *slot;
*slot = newValue;
slotlock.unlock();
}
objc_release(oldValue);
}
参数解析:
self
:这里就是main()
函数中实例化的Person
对象p
-cmd
:这里就是setName:
newValue
:这就是要赋的新值"Objc"
offset
:指针偏移量。对象p
的指针是它第一个成员isa
的地址,而我们的Person
只有一个属性,系统会根据这个p
的指针 +offset
来找到属性对应成员变量_name
的内存地址。isa
是个指针,所以这里offset = 8
atomic
:是否是原子性的,此处为YES
copy
:是否拷贝,此处为YES
mutableCopy
:是否可变类型拷贝,此处为NO
源码分析:
- 如果
offset = 0
,就是修改isa
,因此调用object_setClass()
- 根据偏移量获取属性成员地址
slot
- 根据是否
copy
或mutableCopy
来拷贝newValue
或者objc_retain(newValue)
- 记录
oldValue
,在赋值完新值之后释放旧值objc_release(oldValue)
- 如果是非
atomic
的,直接对属性赋值
- 如果是
atomic
的,先加锁,在赋值,再解锁
其中,加锁使用的spinlock_t
在上篇文章中提到过,本文不再赘述。
可见,所谓atomic
修饰的属性,只是在赋值的时候加了一把锁。
2.2 取值
取值调用的是objc_getProperty()
:
id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, BOOL atomic) {
if (offset == 0) {
return object_getClass(self);
}
// Retain release world
id *slot = (id*) ((char*)self + offset);
if (!atomic) return *slot;
// Atomic retain release world
spinlock_t& slotlock = PropertyLocks[slot];
slotlock.lock();
id value = objc_retain(*slot);
slotlock.unlock();
// for performance, we (safely) issue the autorelease OUTSIDE of the spinlock.
return objc_autoreleaseReturnValue(value);
}
- 如果不是
atomic
的,直接根据指针和偏移量取到值返回
- 如果是
atomic
的,依然是「加锁获取」,并且进行了objc_retain()
操作,返回时返回的是自动释放对象objc_autoreleaseReturnValue()
3.注意事项
atomic
修饰属性加的锁,仅仅是在setter
和getter
内部加的锁,保证取值/赋值时的线程安全性,但是如果像下面这样使用并不能保证线程安全:
@interface RootViewController ()
@property (atomic) NSInteger number;
@end
@implementation RootViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 线程1
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 1000; i++) {
self.number = self.number + 1;
NSLog(@"number: %ld", self.number);
}
});
// 线程2
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i = 0; i < 1000; i++) {
self.number = self.number + 1;
NSLog(@"number: %ld", self.number);
}
});
}
@end
属性是atomic
修饰的,按理说应该是线程安全的,两个线程各对number
做了1000次循环+1
,最终numer
的值应该是2000
,但输出的值却不是期望的2000
。
// 最后的几行输出
2020-03-07 22:37:21.713683+0800 TestObjC[23813:2171198] number: 1986
2020-03-07 22:37:21.714004+0800 TestObjC[23813:2171198] number: 1987
2020-03-07 22:37:21.714267+0800 TestObjC[23813:2171198] number: 1988
2020-03-07 22:37:21.714541+0800 TestObjC[23813:2171198] number: 1989
2020-03-07 22:37:21.714844+0800 TestObjC[23813:2171198] number: 1990
2020-03-07 22:37:21.715027+0800 TestObjC[23813:2171198] number: 1991
2020-03-07 22:37:21.715442+0800 TestObjC[23813:2171198] number: 1992
以上是输出的最后几行,最终的值只加到1992
。这是因为两个线程在并发的调用setter
和getter
,在setter
和getter
内部是加了锁,但是在做+1
操作的时候并没有加锁,导致在某一时刻,线程一调用了getter
取到值,线程2恰好紧跟着调用了getter
,取到相同的值,然后两个线程对取到的值分别+1
,再分别调用setter
,使得两次setter
其实赋值了相等的值。
- 因此使用
atomic
修饰属性时对属性的操作是否是线程安全的,虽然在内部加了锁,但并不能保证绝对的线程安全。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 记一次.NET内存居高不下排查解决与启示
· DeepSeek 开源周回顾「GitHub 热点速览」
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了