iOS 之 使用@synchronized加锁的坑

error_of_using_synchronized

Posted by Elliot on June 23, 2017

版权声明:本文为博主原创文章,未经博主允许不得转载;如需转载,请保持原文链接。

使用@synchronized加锁的坑

首先让我们来复习一下@synchronized互斥锁的内部实现

首先一个简单的测试代码

- (void) testddd{
	@synchronized (arr) {

	}
}

然后看下汇编实现:

可以看到synchronized会对加锁的对象进行retain,但是这个不是重点,关键看objc_sync_enterobjc_sync_exit这两个函数,去源码里面找找:

// Begin synchronizing on 'obj'.
// Allocates recursive mutex associated with 'obj' if needed.
// Returns OBJC_SYNC_SUCCESS once lock is acquired.  
int objc_sync_enter(id obj)
{
    int result = OBJC_SYNC_SUCCESS;

    if (obj) {
        SyncData* data = id2data(obj, ACQUIRE);
        assert(data);
        data->mutex.lock();
    } else {
        // @synchronized(nil) does nothing
        if (DebugNilSync) {
            _objc_inform("NIL SYNC DEBUG: @synchronized(nil); set a breakpoint on objc_sync_nil to debug");
        }
        objc_sync_nil();
    }
    return result;
}

// End synchronizing on 'obj'.
// Returns OBJC_SYNC_SUCCESS or OBJC_SYNC_NOT_OWNING_THREAD_ERROR
int objc_sync_exit(id obj)
{
    int result = OBJC_SYNC_SUCCESS;
    if (obj) {
        SyncData* data = id2data(obj, RELEASE);
        if (!data) {
            result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
        } else {
            bool okay = data->mutex.tryUnlock();
            if (!okay) {
                result = OBJC_SYNC_NOT_OWNING_THREAD_ERROR;
            }
        }
    } else {
        // @synchronized(nil) does nothing
    }
    return result;
}

可以看出:

1、synchronized是使用的递归mutex来做同步。@synchronized(nil)不起任何作用(这样就避免了多次嵌套@synchronized导致死锁)

2、只对同一对象加锁,如果对象不同,则不起作用,这样就会有个问题,如果我在@synchronized里面对该对象重新赋值了,其他线程还是会进来,不会起到同步阻塞作用;

至于为什么是对同一个对象加锁,看源码可以知道,@synchronized(token) 中的 token 通过 hash 算法存储到了一份手动维护的 cache 中,cache 的 key 使用的是 token 的内存地址。hash 算法恰能以 O(1)的时间复杂度,以 token 为 key 取出对应的锁。即内容与位置之间的快速映射关系,也即是一个地址对应一个锁;@synchronized 使用多了之后,快速的通过 token 取出对应的锁,能够达到优化多线程的性能的作用;

ok,看到这里基本可以知道我要说的问题了,就不继续深入了;

回到主题上,首先来看下问题代码,我把该代码简化了: 看代码:

	for (int i = 0; i < 10000; i++) {
		dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
		[self testddd];
		});
	}

- (void)testddd{
	if (selectorAndClassArray.count==0) {
		NSMutableArray *emergencyArray = [NSMutableArray array];
		selectorAndClassArray = emergencyArray;

	}
}

上面的代码,这种多线程赋值操作很容易出现野指针Crash,我加了10000次循环,几乎野指针Crash是必现的,具体是因为对象的多次释放导致的,这里不细讲,可以去看下这篇博客;

那么该问题处理办法就是加锁了,重点来了

- (void) testddd{
	if (selectorAndClassArray.count==0) {
		NSMutableArray *emergencyArray = [NSMutableArray array];
		@synchronized (selectorAndClassArray) {
			selectorAndClassArray = emergencyArray;

		}
	}
}

我这样加锁之后,发现并没有什么用,还是会出现野指针Crash,只能慢慢调试了,下断点在@synchronized中,发现有很多线程还是进来了,如下图:

回到我们开头所讲,发现原来如此,因为@synchronized只会锁住同一对象,而@synchronized内部改变之前标记的对象,所以就相当于没有加锁一样其他线程都可以进去了,这样就达不到阻塞同步代码的作用,还是和上面没加锁的代码一样会引起野指针Crash;

找到原因之后就好修改了,直接使用NSLock就行,代码如下:

- (void) testddd{
	if (selectorAndClassArray.count==0) {
		NSMutableArray *emergencyArray = [NSMutableArray array];
		[self.lock lock];
		selectorAndClassArray = emergencyArray;
		[self.lock unlock];
	}
}

总结:需要慎用@synchronized,在加锁的场景下要考虑清楚该用什么锁,不然一不留神就给自己挖了个坑; 本文完;