@synchronized的使用
1.@synchronized的使用
@synchronized是iOS开发中常用的锁,下简单介绍一下它的使用。
如下代码,在多线程中同时操作调用一个saleTicket方法,并且修改属性self.ticketCount。
- (void)lg_testSaleTicket{
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 5; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 3; i++) {
[self saleTicket];
}
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10; i++) {
[self saleTicket];
}
});
}
- (void)saleTicket{
@synchronized (self) {
if (self.ticketCount > 0) {
self.ticketCount--;
sleep(0.1);
NSLog(@"当前余票还剩:%ld张",self.ticketCount);
}else{
NSLog(@"当前车票已售罄");
}
}
}
当不使用@synchronized打印的结果是错乱无序的
当使用@synchronized打印结果是有序的
@synchronized使用小结
以上对@synchronized的使用例子说明了@synchronized确定可以解决多线程数据安全的问题,它对数据操作起到了加锁的作用。
@synchronized分析
分析思路
我们如果来对@synchronized进行分析呢?
1.断点方式
我们在使用到@synchronized的地方打个断点,打印堆栈信息如下:
通过打印堆栈信息我们什么也没有发现???尴尬!!!!
2.开启汇编方式
在刚刚打下断点断住的条件下,打开汇编,方式如下: Xcode -> Debug -> Debug Workflow -> Always Show Disassembly。
然后得到如下的界面:
查看汇编的代码我们发现了两上函数
objc_sync_enter和objc_sync_exit,我们猜想,这个会不会就是@synchronized的加锁和解锁呢?
3.clang的方式分析
下面我们通过clang将main.m文件编绎成main.cpp文件
打开main.cpp文件,找到main函数的实现
我们发现了两上函数
objc_sync_enter和objc_sync_exit,说明了确实是这两个函数对@synchronized进行了加锁和解锁。
回到刚刚的汇编,我们将断点打在objc_sync_enter处,按住command点击该函数,再点击Show Quick Help得到如下界面:
由此我们得到两上函数
objc_sync_enter和objc_sync_exit的源码实现在objc库中。
@synchronized源码分析
打开一份从Apple Open source下载的objc4-781的objc源码
objc_sync_enter源码分析
全局搜索‘objc_sync_enter’找到其源码实现
SyncData* data = id2data(obj, ACQUIRE);根据obj得到一个SyncData的实例data;data->mutex.lock();函数进行加锁;objc_sync_nil();对obj为空的处理
objc_sync_nil分析
objc_sync_nil的底层实现如下:
# define BREAKPOINT_FUNCTION(prototype) \
OBJC_EXTERN __attribute__((noinline, used, visibility("hidden"))) \
prototype { asm(""); }
针对
obj为空的情况直接不处理。
SyncData的数据结构
nextData指向下一个SyncData,说明@synchronized锁的对象是采用链表的形式存储的;threadCount记录当前有多少个线程使用了@synchronized;
由
mutex可知,@synchronized所加的锁是recursive_mutex_t类型的互斥锁。
SyncData的获取方法'id2data'函数分析
当查询到object对应的SyncData的data存在时的分析
拿到objc对应SyncData当前锁的数量
lockCount,在ACQUIRE情况下进行加加,在RELEASE情况下进行减减;并保存。
当查询到有缓存的情况下的分析
拿到objc对应SyncData当前缓存的锁的数量
lockCount,在ACQUIRE情况下进行加加,在RELEASE情况下进行减减;并更新缓存.
当没有查询到也没有缓存时的分析
- 首先进行加锁;
- 遍历整个链表,打到尾结点;
- 如果没有找到尾结点,将objcet的数据记录到result中;
- 最后goto done.
创建一个新的SyncData并保存到list中,threadCount=1。
Done的分析
将上面步骤得到的
result存到暂存和缓存中,并返回result。
至此,objc_sync_enter的分析结束。
objc_sync_exit源码分析
拿到
obj的SyncData并解锁data->mutex.tryUnlock().
总结
@synchronized维护了一个hash表,hash表中保存了每条线程使用@synchronized的情况,用threadCount记录线程数,用lockCount记录每个线程下的加锁数量。所以@synchronized才能支持多线程和嵌套使用。
@synchronized坑点
性能问题
首先附上iOS八大锁的性能对比图
通过上面对@synchronized原理的分析我们发现,@synchronized底层维护了一个hash表加链表的形式去存储加锁的对象,并且使用了大量的代码来关现暂存和缓存。不管是hash表和链表的查询还是暂存和缓存的实现都是需要大量的计算的,这也就导致了@synchronized的性能大大降低.
@synchronized(id)参数问题
如下代码执行后会出现什么问题?
- (void)lg_crash{
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
_testArray = [NSMutableArray array];
});
}
}
- 以上代码实现了在多线程中多次对_testArray进行初始化;
- _testArray = [NSMutableArray array];本质是调用setter方法;而setter 方法需要对旧值的releae,对新值的retain;在多线程操作_testArray的setter方法,而不保证线程安全的情况下就会出现对同一个旧值进行了多次的release,从而导致野指针的出现,从而导致程序崩溃。
那么我们作出如下修改后会怎么样呢?
- (void)lg_crash{
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (_testArray) {
_testArray = [NSMutableArray array];
}
});
}
}
如上面分析的,_testArray在某个时刻可能会变为nil,所以通过之前对@synchronized原理的分析,加上多线程下的处理,当我们在使用@synchronized的过程中,需要对_testArray先加锁再解锁,而当_testArray变为nil时,多线程下的操作会导致@synchronized传入的参数为nil而调用objc_sync_nil函数,也就没有了加锁的效果了,从而导致崩溃的出现。
所以我们在使用@synchronized确保多线程数据安全的时候,要保证传入的绝对不能如上例子为nil的情况。修改成如下则不会再有问题:
- (void)lg_crash{
for (int i = 0; i < 200000; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@synchronized (self) {
_testArray = [NSMutableArray array];
}
});
}
}