synchronized是一种防止多线程操作引起不安全问题的锁。使用起来比较方便,而且不用管理它的生命周期,时常会被程序猿用到,但它的性能却是最差的......
为了更好的理解synchronized锁的性能,下面我们来分析下synchronized底层是怎么实现的。通过打印调用栈(bt)、增加符号断点、clang等方式,可以看出synchronized会被编译成objc_sync_enter函数,和objc_sync_exit,objc_sync_enter在libobjc.A.dylib中。
在libobjc.A.dylib中我们找到了函数objc_sync_enter。
这里从源码备注可以知道参数obj就是synchronized传入的需要我们“保护”的数据,同时会创建递归互斥锁recursive mutex,既然是递归锁,那么就有重复调用以及可以重入的特性,锁的是需要“保护”的数据。既然是多线程操作数据,就有可能存在:
1、同一线程访问多次数据
2、多个线程访问多次数据
从objc_sync_enter函数里可知,需要“保护”的数据被封装成SyncData的一种数据,然后再进行加锁。查看SyncData数据:
SyncData里有recursive_mutex_t变量,查看定义可知(以下截图)
recursive_mutex_t就是一把递归锁,封装了lock和unlock的方法,这里引申出另一知识点:atomic内部也会使用os_unfair_recursive_lock。
回到objc_sync_enter函数,我们看下id2data函数是如果处理数据的。
这里增加一个知识点:线程局部存储(Thread Local Storage,TLS) 是操作系统为线程单独提供的私有空间,通常只有有限的容量。
TLS通过kvc能够获得线程。如119行:
SyncData *data = (SyncData *)tls_get_direct(SYNC_DATA_DIRECT_KEY)
通过SYNC_DATA_DIRECT_KEY能够获取到data,SyncData里面的object存储着需要“保护”的数据;
id2data函数的参数why由外部传入,objc_sync_enter时传的ACQUIRE,objc_sync_exit时传的RELEASE,通过代码可知,enter时候就会对lockCount进行+操作,exit时候进行-操作,通过lockCount知道数据被锁了多少次。而threadCount知道有多少个线程。下面我们列举下3种可能出现的多线程访问的情况:
1、第一次进入,没有锁的情况:tls和缓存里没有数据,直接进入202行,首先会在链表里去查找object(207行),如果是第一次进入并且在链表结构里没找到,就会进入223行,进行赋值,如果支持TLS进入256行,通过KVC存入result和lockCount。此时lockCount和threadCount都为1,为了安全和方便下次取数据,再存入缓存中(265行)这里的fetch_cache传入YES后会绑定当前线程,fetch_cache函数如下截图,返回result。
2、非第一次,但是同一个线程进入的情况:如果支持tls会在119行找到数据,找到后会lockCount++(135行),然后再存入tls里,然后返回result。
3、非第一次,不同线程进入:会进入160行,之前通过fetch_cache存入的数据现在就能找到了(第1种情况),然后对lockCount++(175行)后,再break。来到207行,会在链表结构里去查找到相同的需要“保护”的对象,找到后进行赋值,并threadCount++。最后的goto done跟第1种情况流程一样。
链表的查询和缓存的查找都是synchronized耗时的原因。
通过Clang知道,在调用objc_sync_enter后,会调用objc_sync_exit函数(如上图),objc_sync_exit函数也会调用id2data,此时传入RELEASE参数后,会对lockCount--。
总结:通过lockCount和threadCount可以知道需要“保护”的数据加锁了多少次,总共有多少线程访问。
SyncList的hash结构和SyncData的链表结构存储了不同线程、多次访问数据时的数据,包括lockCount和threadCount等数据,每个SyncData代表锁的次数。