@synchronized的使用和分析

622 阅读4分钟

@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_enterobjc_sync_exit,我们猜想,这个会不会就是@synchronized的加锁和解锁呢?

3.clang的方式分析

下面我们通过clang将main.m文件编绎成main.cpp文件

打开main.cpp文件,找到main函数的实现 我们发现了两上函数objc_sync_enterobjc_sync_exit,说明了确实是这两个函数对@synchronized进行了加锁和解锁。

回到刚刚的汇编,我们将断点打在objc_sync_enter处,按住command点击该函数,再点击Show Quick Help得到如下界面:

由此我们得到两上函数objc_sync_enterobjc_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源码分析

拿到objSyncData并解锁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];
            }
        });
    }
}

END