如果你曾经使用Objective-C做过并发编程,那你肯定见过@synchronized这个结构。@synchronized这个结构发挥了和锁一样的作用:它避免了多个线程同时执行同一段代码。和使用NSLock进行创建锁、加锁、解锁相比,在某些情况下@synchronized会更方便、更易读。

如果你从来没有使用过@synchronized,具体如何使用可以参考下面的实例。本文的将围绕我对@synchronized的原理的探究进行讲述。

使用@synchronized的例子

假如要用Objective-C实现一个线程安全的队列,我们大概会这样写:

@implementation ThreadSafeQueue {
NSMutableArray *_elements;
NSLock *_lock;
} - (instancetype)init {
self = [super init];
if (self) {
_elements = [NSMutableArray array];
_lock = [[NSLock alloc] init];
}
return self;
} - (void)push:(id)element {
[_lock lock];
[_elements addObject:element];
[_lock unlock];
} @end

ThreadSafeQueue这个类首先有一个init方法,这里初始化了两个变量:一个_elements数组和一个NSLock。另外,有一个需要获取这个锁以插入元素到数组中然后释放锁的push:方法。许多线程会同时调用push:方法,然而[ _elements addObject:element];这行代码也只能同时被一条线程访问。这个流程应该是这样的:

  1. 线程A调用push:方法
  2. 线程B调用push:方法
  3. 线程B调用[_lock lock],因为没有其他线程持有这个锁,因此线程B取得了这个锁
  4. 线程A调用[_lock lock],但是此时这个锁被线程B所持有,所以这个方法调用并没有返回,使线程A暂停了执行
  5. 线程B添加了一个元素到_elements中,然后调用[ _lock unlock]方法。此时,线程A的[ _lock unlock]方法返回了,接着继续执行线程A的元素插入操作

使用@synchronized,我们可以更简洁明了的实现刚才的功能:

@implementation ThreadSafeQueue {
NSMutableArray *_elements;
} - (instancetype)init {
self = [super init];
if (self) {
_elements = [NSMutableArray array];
}
return self;
} - (void)increment {
@synchronized (self) {
[_elements addObject:element];
}
} @end

这个@synchronized的代码块和前面例子中的[ _lock unlock][ _lock unlock]的作用相同作用效果。你可以把它理解成把self当作一个NSLock来对self进行加锁。在运行{后的代码前获取锁,并在运行}后的其他代码前释放这个锁。这非常的方便,因为这意味着你永远不会忘了调用unlock

你也可以在任何Objective-C的对象上使用@synchronized。因此,同样的我们也可以像下面的例子里一样,使用@synchronized(_elements)来代替@synchronized(self),这两者的效果是一致的。

回到我的探究上来

我对@synchronized的实现很好奇,于是我在谷歌搜索了它的一些细节。我找到了关于这个的一些回答 @synchronized是如何加锁/解锁的 在@synchronized中改变加锁的对象 Apple的文档,但没有一个答案能给我足够深入的解释。传入@synchronized的参数和这个锁有什么关系?@synchronized是否持有它所加锁的对象?如果传入@synchronized代码块的对象在代码块里被析构了或者被置为nil了会怎么样?这些都是我想问的问题。在下文中,我会分享我的发现。

关于@synchronized的Apple的文档中提到,@synchronized代码块隐式地给被保护的代码段添加了一个异常处理块。这就是为什么在给某个对象保持同步的时候,如果抛出了异常,锁就会被释放。

在 stackoverflow的一个回答中提到,@synchronized块会转化成一对objc_sync_enterobjc_sync_exit的函数调用。我们并不知道这些函数都干了什么,但是根据这个我们可以推断,编译器会像这样转化代码:

@synchronized(obj) {
// do work
}

转换成大概像这样的:

@try {
objc_sync_enter(obj);
// do work
} @finally {
objc_sync_exit(obj);
}

具体什么是objc_sync_enterobject_sync_exit以及它们是如何实现的,我们通过Command+点击这两个函数跳转到了<objc/objc-sync.h>里,这里有我们要找的两个函数:

// Begin synchronizing on 'obj'.
// Allocates recursive pthread_mutex associated with 'obj' if needed.
int objc_sync_enter(id obj) // End synchronizing on 'obj'.
int objc_sync_exit(id obj)

在文件的最后,有一个苹果工程师也是人的提示;)

// The wait/notify functions have never worked correctly and no longer exist.
int objc_sync_wait(id obj, long long milliSecondsMaxWait);
int objc_sync_notify(id obj);

总之,关于objc_sync_enter的文档告诉了我们:@synchronized是基于一个递归锁[1] 来传递一个对象的。什么时候分配内存、如何分配内存的?如何处理nil值?幸运的是,Objective-C运行时是开源的,所以我们可以阅读它的源码找到答案。

你可以在这里查看所有objc-sync的源码,但是我会领你在更高的层面通读这些源码。我们先从文件顶部的数据结构看起。我会为你解释下面的源码因此你不必花时间来尝试解读这些代码。

typedef struct SyncData {
id object;
recursive_mutex_t mutex;
struct SyncData* nextData;
int threadCount;
} SyncData; typedef struct SyncList {
SyncData *data;
spinlock_t lock;
} SyncList; static SyncList sDataLists[16];

首先,我们看到了结构体struct SyncData的定义。这个结构体包含了一个object(传入@synchronized的对象)还有一个关联着这个锁以及被锁对象的recursive_mutex_t。每个SyncData含有一个指向其他SyncData的指针nextData,因此你可以认为每个SyncData都是链表里的一个节点。最后,每个SyncData含有一个threadCount来表示在使用或者等待锁的线程的数量。这很有用,因为SyncData是被缓存的,当threadCount == 0时,表示一个SyncData的实例能被复用。

接着,我们有了struct SyncList的定义。正如我在前文中所提到的,你可以把一个SyncData当作链表中的一个节点。每个SyncList结构都有一个指向SyncData链表头部的指针,就像一个用于避免多线程并发的修改该链表的锁一样。

这个代码块的最后一行之上是一个sDataLists的定义,这是一个SyncList结构的数组。刚开始可能看起来不太像,但这个sDataList数组是一个哈希表(类似NSDictionary),用于把Objectice-C对象映射到他们对应的锁。

当你调用objc_sync_enter(obj)的时候,它通过一个记录obj地址的哈希表来找到对应的SyncData,然后对其加锁。当你调用objc_sync_exit的时候,它以同样的方式找到对应的SyncData并将其解锁。

很好!现在我们知道了@synchronized是如何关联一个锁和那个被加同步锁的对象,接下来,我会讲讲当一个对象在@synchronized代码块中被析构或者被置nil会发生什么。

如果你看源码的话,你会发现objc_sync_enter里面并没有retains或者release。因此,它并不会持有传入的对象,或者也有可能是因为它是在arc中编译的。我们可以通过以下的代码来进行测试:

NSDate *test = [NSDate date];
// This should always be `1`
NSLog(@"%@", @([test retainCount])); @synchronized (test) { // This will be `2` if `@synchronized` somehow
// retains `test`
NSLog(@"%@", @([test retainCount]));
}

对于每个的持有数,输出总为1。因此objc_sync_enter不会持有传入的对象。这很有意思。如果你需要同步的对象呗析构了,然后可能另外一个新的对象被分配到了这个内存地址上,很可能其他线程正尝试同步那个有着和原对象有着相同地址的新的对象。在这种情况下,其他线程会被阻塞直到当前线程完成了自己的同步代码块。这似乎没什么毛病。这听起来像这种实现是已被知晓的而且也没什么问题。我并没有看到其他更好的替代方案。

那如果这个对象在@synchronized代码块中被设成nil会怎样呢?再来看看我们的实现:

NSString *test = @"test";
@try {
// Allocates a lock for test and locks it
objc_sync_enter(test);
test = nil;
} @finally {
// Passed `nil`, so the lock allocated in `objc_sync_enter`
// above is never unlocked or deallocated
objc_sync_exit(test);
}

调用objc_sync_enter的时候传入test,调用objc_sync_exit的时候传入nil。若objc_sync_exit传入nil的时候什么都不做,那么也不再会有人去释放这个锁。这很糟糕。

Objective-C会那么轻易的被这种问题影响吗?下面的代码把一个会被置nil的指针传入@synchronized。然后在后台线程中往@synchronized中传入一个指向同一对象的指针。如果在@synchronized中把一个对象置为nil让这个锁处于加锁的状态,那么在第二个@synchronized中的代码将永远不会被运行。在控制台中我们应该什么都看不到。

NSNumber *number = @(1);
NSNumber *thisPtrWillGoToNil = number; @synchronized (thisPtrWillGoToNil) {
/**
* Here we set the thing that we're synchronizing on to `nil`. If
* implemented naively, the object would be passed to `objc_sync_enter`
* and `nil` would be passed to `objc_sync_exit`, causing a lock to
* never be released.
*/
thisPtrWillGoToNil = nil;
} dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^ { NSCAssert(![NSThread isMainThread], @"Must be run on background thread"); /**
* If, as mentioned in the comment above, the synchronized lock is never
* released, then we expect to wait forever below as we try to acquire
* the lock associated with `number`.
*
* This doesn't happen, so we conclude that `@synchronized` must deal
* with this correctly.
*/
@synchronized (number) {
NSLog(@"This line does indeed get printed to stdout");
} });

当我们运行上述代码时,这行代码却的确被打印到控制台上了!因此可以证明,Objective-C能很好的处理这种情况。我打赌这种情况是被编译器处理过的,大概如下:

NSString *test = @"test";
id synchronizeTarget = (id)test;
@try {
objc_sync_enter(synchronizeTarget);
test = nil;
} @finally {
objc_sync_exit(synchronizeTarget);
}

有了这种实现,传入objc_sync_enterobjc_sync_exit的对象总是相同的。当传入nil的时候他们什么都不会做。这引出了一个很棘手的debug场景:如果你往@synchronized里传入nil,那么相当于你并没有进行过加锁操作,同时你的代码将不再是线程安全的了!如果你被莫名其妙的问题困扰着,那么先确保你没有把nil传入你的@synchronized代码块。你可以通过给objc_sync_nil设置一个符号断点来检查,objc_sync_nil是一个空方法,会在往objc_sync_enter传入nil的时候调用,这会让调试方便的多。

现在,我的问题得到了回答。

  1. 对于每个加了同步的对象,`Objective-C的运行时都会给其分配一个递归锁,并且保存在一个哈希表中。
  2. 一个被加了同步的对象被析构或者被置为nil都是没有问题的。然而文档中并没有对此进行什么说明,所以我也不会在任何实际的代码中依赖这个。
  3. 注意不要往@synchronized代码块中传入nil!这会毁掉代码的线程安全性。通过往objc_sync_nil加入断点你可以看到这种情况的发生。

探究的下一步是研究synchronized代码块转成汇编的代码,看看是否和我前面的例子相似。我打赌synchronized代码块转换的汇编代码不会和我们猜想的任何Objective-C代码相似,上述的代码例子只是@synchronized实现的模型而已。你能想到更好的模型吗?或者在我的这些例子中哪里有瑕疵?请告诉我。

-完-

[1] 递归锁,是一种在已持有锁的线程重复请求锁却不会发生死锁的锁。你可以在这里找到一个相关的例子。有个很好用的类NSRecursiveLock,它能实现这种效果,你可以试试。

关于@synchronized 比你想知道的还多的更多相关文章

  1. 既然synchronized是"万能"的,为什么还需要volatile呢?

    在我的博客和公众号中,发表过很多篇关于并发编程的文章,之前的文章中我们介绍过了两个在Java并发编程中比较重要的两个关键字:synchronized和volatile 我们简单回顾一下相关内容: 1. ...

  2. 死磕Synchronized底层实现,面试你还怕什么?

    关于synchronized的底层实现,网上有很多文章了.但是很多文章要么作者根本没看代码,仅仅是根据网上其他文章总结.照搬而成,难免有些错误:要么很多点都是一笔带过,对于为什么这样实现没有一个说法, ...

  3. 【转】深圳FAE,想拿高薪还缺什么?

    原文网址:http://www.eefocus.com/KTHR_IC/blog/11-05/222793_e04c8.html KT老胡您好! 我07年本科毕业在一家医疗民营企业从事了3年多的嵌入式 ...

  4. 不想eject,还咋修改create-react-app的配置?

    一.先抛问题 许多刚开始接触create-react-app框架的同学,不免都会有个疑问:如何在不执行eject操作的同时,修改create-react-app的配置.今天胡哥就来带大家一起来看看这个 ...

  5. synchronized锁自旋

    http://www.jianshu.com/p/5dbb07c8d5d5 原理 通常说的synchronized在方法或块上加锁,这里的锁就是对象锁(当然也可以在类上面),或者叫重量锁,在JVM中又 ...

  6. Java多线程初学者指南(11):使用Synchronized块同步方法

    synchronized关键字有两种用法.第一种就是在<使用Synchronized关键字同步类方法>一文中所介绍的直接用在方法的定义中.另外一种就是synchronized块.我们不仅可 ...

  7. java多线程中 volatile与synchronized的区别-阿里面试

    volatile 与 synchronized 的比较(阿里面试官问的问题) ①volatile轻量级,只能修饰变量.synchronized重量级,还可修饰方法 ②volatile只能保证数据的可见 ...

  8. JAVA多线程之volatile 与 synchronized 的比较

    一,volatile关键字的可见性 要想理解volatile关键字,得先了解下JAVA的内存模型,Java内存模型的抽象示意图如下: 从图中可以看出: ①每个线程都有一个自己的本地内存空间--线程栈空 ...

  9. 使用Synchronized块同步方法

    synchronized关键字有两种用法.第一种就是在<使用Synchronized关键字同步类方法>一文中所介绍的直接用在方法的定义中.另外一种就是synchronized块.我们不仅可 ...

随机推荐

  1. <Android 应用 之路> MPAndroidChart~ScatterChart

    简介 MPAndroidChart是PhilJay大神给Android开发者带来的福利.MPAndroidChart是一个功能强大并且使用灵活的图表开源库,支持Android和IOS两种,这里我们暂时 ...

  2. js判断是手机还是PC端

    有时接触一些手机上的适应,需要知道是pc 还是移动端 function IsPC() { var userAgentInfo = navigator.userAgent; var Agents = [ ...

  3. Java学习笔记(2)----散列集/线性表/队列/集合/图(Set,List,Queue,Collection,Map)

    1. Java集合框架中的所有实例类都实现了Cloneable和Seriablizable接口.所以,它们的实例都是可复制和可序列化的. 2. 规则集存储的是不重复的元素.若要在集合中存储重复的元素, ...

  4. ContentProvider与ContentResolver

    使用ContentProvider共享数据: 当应用继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据.虽然使用其他方法也可以对外共享数据,但数据 ...

  5. seacms 6.45 命令执行漏洞分析

    前言 这是一个比较老的漏洞了,不过漏洞原理还是挺有意思的. 正文 漏洞位于 search.php 文件中. 首先包含了 common.php, 这个文件里面做了一些初始化工作,其中最重要的是对提交参数 ...

  6. vue3.0端口号修改

    module.exports = { // 基本路径 baseUrl: '/', // 输出文件目录 outputDir: 'dist', // 生产环境是否生成 sourceMap 文件 produ ...

  7. vim常用快捷汇总

    移动光标的方法 h 或 向左箭头键(←) 光标向左移动一个字符 j 或 向下箭头键(↓) 光标向下移动一个字符 k 或 向上箭头键(↑) 光标向上移动一个字符 l 或 向右箭头键(→) 光标向右移动一 ...

  8. SSH 无法启动的原因分析及解决方法

    简介 Secure Shell(缩写为 SSH),由 IETF 的网络工作小组(Network Working Group)所制定:SSH 为一项创建在应用层和传输层基础上的安全协议,为计算机上的 S ...

  9. C# winform Visual Studio Installer打包教程

    C# winform  Visual Studio Installer打包教程 软件在功能开发实现后,其实并没有真正的完成.对于一个专业的开发者来说,Release软件应该是以安装版本的形式进行发布的 ...

  10. python字典的排序

    # -*- coding:UTF-8 -*- def dict_sort(): # 按照value的值从大到小的顺序进行排序 dic = {'a': 31, 'bc': 5, 'c': 3, 'asd ...