八大锁分析

synchronized分析

我们先来看个题目:

- (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 < 10; i++) {
            [self saleTicket];
        }
    });
}

- (void)saleTicket{
    if (self.ticketCount > 0) {
        self.ticketCount--;
        sleep(0.1);
        NSLog(@"当前余票还剩:%ld张",self.ticketCount);
    }else{
        NSLog(@"当前车票已售罄");
    }
}

然后我们调用上面的方法

self.ticketCount = 20;
[self lg_testSaleTicket];

请问上面的代码设计是否有问题呢?
当然有问题,会存在多个线程操作一个数据ticketCount,导致数据不安全的问题。执行完成后剩余的票数可能不会为0。
既然是多线程导致的数据不安全问题,我们就可以加锁进行解决。

- (void)saleTicket{
    // 枷锁 - 线程安全
    @synchronized (self) {
        if (self.ticketCount > 0) {
            self.ticketCount--;
            sleep(0.1);
            NSLog(@"当前余票还剩:%ld张",self.ticketCount);
        }else{
            NSLog(@"当前车票已售罄");
        }
    }
}

我们对卖票的操作部分加上了@synchronized,这样同时只能有一个线程操作ticketCount,从而保证了数据的安全。
下面我们来探究下@synchronized。

appDelegateClassName = NSStringFromClass([AppDelegate class]);
@synchronized (appDelegateClassName) {
}

在synchronized的地方打上断点,然后汇编调试。


image.png

在synchronized的汇编调试代码中,我们看有objc_sync_enter和objc_sync_exit成对的出现。所以这一对函数应该是和synchronized的底层实现相关的。
然后我们就可以通过符号断点,针对objc_sync_enter打个符号断点


objc_sync_enter符号断点

这样我们可以看到objc_sync_enter位于libobjc.A.dylib动态库中,然后我们就可以去open.souce上下载这个源码了。

下载objc源码,然后搜索objc_sync_enter

// 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;
}

synchronizing是种互斥锁。首先判断objc,如果不存在的话走objc_sync_nil(),也就是什么都不做。所以在使用@synchronized(obj)进行加锁的时候,如果obj为nil,就是无效的,不会进行加锁。

下面我们看下objc不为空的情况:
构建了SyncData,看下SyncData的结构

typedef struct alignas(CacheLineSize) SyncData {
    struct SyncData* nextData;
    DisguisedPtr<objc_object> object;
    int32_t threadCount;  // number of THREADS using this block
    recursive_mutex_t mutex;
} SyncData;

里面有个nextData,应该是指向了下一个节点。所以好多这样的节点组成了一个链表似的结构;里面还有个递归锁mutex(递归锁属于互斥锁的一种)。

static SyncData* id2data(id object, enum usage why)
{
    spinlock_t *lockp = &LOCK_FOR_OBJ(object);
    // ....... 
}

在id2data函数中通过LOCK_FOR_OBJ函数获取到lockp,LOCK_FOR_OBJ函数的定义如下

#define LOCK_FOR_OBJ(obj) sDataLists[obj].lock
static StripedMap<SyncList> sDataLists;

可以看到sDataLists实际上是一个哈希表,表中存在一个个的SyncList对象,SyncList对象的结构中有data和lock。

struct SyncList {
    SyncData *data;
    spinlock_t lock;

    constexpr SyncList() : data(nil), lock(fork_unsafe_lock) { }
};

@synchronized底层是封装的互斥锁pThread。

synchronized使用注意点

下面代码可以正常运行吗?

- (void)lg_crash{
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            _testArray = [NSMutableArray array];
        });
    }
}

执行这段代码会导致野指针crash。
GCD里面的_testArray = [NSMutableArray array];这句代码是在创建新的Array赋值给_testArray,然后释放了旧值。如果此时多个线程同时暂存了旧值,然后就会导致多次释放同一个旧值,从而产生野指针崩溃。
我们可以进行加锁处理。像下面的这样加锁处理可以吗?

- (void)lg_crash{
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            @synchronized (_testArray) {
                _testArray = [NSMutableArray array];
            }
        });
    }
}

答案是会产生同样的野指针crash。因为在过程中_testArray可能为空,使用@synchronized锁的对象如果为空的话,相当于不锁。所以会得到同样的crash。此时我们可以将锁的对象_testArray换成self,这样就可以解决问题。但是@synchronized底层需要对哈希表进行处理,过程比较复杂,所以效率低。这里我们可以使用NSLock来进行加锁处理。

- (void)lg_crash{
    NSLock *lock = [[NSLock alloc] init];
    for (int i = 0; i < 200000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            [lock lock];
            _testArray = [NSMutableArray array];
            [lock unlock];
        });
    }
}

NSLock分析

下面的代码可以正常执行吗?

NSLock *lock = [[NSLock alloc] init];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
    static void (^testMethod)(int);
    testMethod = ^(int value){
        [lock lock];
        if (value > 0) {
            NSLog(@"current value = %d",value);
            testMethod(value - 1);
        }
        [lock unlock];
    };
    testMethod(10);
})

答案是只打印出一个10,就会卡死。
因为递归调用了testMethod,就会多次进行lock加锁,在一个lock锁定的区域内递归调用再次进行加锁,就会导致堵塞。
因为是递归调用,此时我们应该讲NSLock换成递归锁NSRecursiveLock,就能正常的打印出10 9 8 7 6 5 4 3 2 1 了。

我们在上面代码的最外层再加一个for循环,还可以正常执行吗?

NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];
for (int i= 0; i<100; i++) {
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        static void (^testMethod)(int);
        testMethod = ^(int value){
            [lock lock];
            if (value > 0) {
              NSLog(@"current value = %d",value);
              testMethod(value - 1);
            }
            [lock unlock];
        };
        testMethod(10);
    });
}

这样就会导致死锁的问题。多个线程进行加锁,互相等待,导致死锁。此时我们只需要将递归锁换成@synchronized就可以解决死锁问题了。因为@synchronized的底层的实现,如果已经锁过一次了就会从缓存中取,而不会再次加锁了。
总结:普通的线程安全可以使用NSLock;如果存在递归调用,使用NSRecursiveLock;如果内部存在递归,外部存在循环或者有其他线程影响,使用@synchronized。

条件锁:NSCondition

调用下面的lg_testConditon方法,会有问题吗?

- (void)lg_testConditon{
    //创建生产-消费者
    for (int i = 0; i < 50; i++) {
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_producer];
        });
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
            [self lg_consumer];
        });
    }
}

- (void)lg_producer{
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
}

- (void)lg_consumer{
    while (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
    }
    
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
}

上面的代码因为在多线程中,不能保证数据安全。我们需要加锁处理?这里NSCondition就最合适了。使用NSCondition当消费到ticketCount为0的时候,调用wait等待。当生产一个ticket后,调用signal发送信号,让等待的可以继续执行。代码实现如下:


- (void)lg_producer{
    [_testCondition lock];
    self.ticketCount = self.ticketCount + 1;
    NSLog(@"生产一个 现有 count %zd",self.ticketCount);
    [_testCondition signal];
    [_testCondition unlock];
}

- (void)lg_consumer{
    // 线程安全
    [_testCondition lock];
    while (self.ticketCount == 0) {
        NSLog(@"等待 count %zd",self.ticketCount);
        // 保证正常流程
        [_testCondition wait];
    }
    //注意消费行为,要在等待条件判断之后
    self.ticketCount -= 1;
    NSLog(@"消费一个 还剩 count %zd ",self.ticketCount);
    [_testCondition unlock];
}

首先锁住生产和消费的代码,然后在消费的时候如果发现ticketCount为0,就wait等待。生产后发送signal,让等待的继续执行消费。

条件锁:NSConditionLock

// 信号量
NSConditionLock *conditionLock = [[NSConditionLock alloc] initWithCondition:2];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
   [conditionLock lockWhenCondition:1];
   NSLog(@"线程 1");
   [conditionLock unlockWithCondition:0];
});

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
   [conditionLock lockWhenCondition:2];
   NSLog(@"线程 2");  
   [conditionLock unlockWithCondition:1];
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
   [conditionLock lock];
   NSLog(@"线程 3");
   [conditionLock unlock];
});

首先创建一个NSConditionLock条件锁,并且设置condition为2。
[conditionLock lockWhenCondition:1];的意思是如果此时的condition为1,并且没有其他线程获取锁,那么就可以获取锁执行下面的代码。[conditionLock unlockWithCondition:0];的意思是释放锁,并且将条件置为0。[conditionLock lock];的意思是不受condition条件的影响。
了解了NSConditionLock后,我们可以知道,线程2肯定是在线程1之前执行。

下面我们来使用汇编来探索一下NSCondition的实现,首先在[conditionLock lockWhenCondition:1];的地方打上断点,然后开启汇编调试

image.png

进入到汇编后我们来到objc_msgSend的地方,这里是调用方法的地方,我们通过lldb命令查看x0和x1的值。可以得到x0是NSConditionLock,x1为lockWhenCondition。也就是我们外面的[conditionLock lockWhenCondition:1];这行代码的调用。
image.png

我们怎么继续跟踪[conditionLock lockWhenCondition:1];这个方法实现呢?此时我们可以通过符号断点的方式,定位到lockWhenCondition方法的具体执行。添加符号断点-[NSConditionLock lockWhenCondition:]。然后我们点击继续就会断点在lockWhenCondition的实现。在lockWhenCondition的实现汇编代码中又定位到一个objc_msgSend。这里一定是调用了其他的方法。我们打印出方法的执行者和方法名称

image.png

方法的执行者是NSConditionLock,方法名称为lockWhenCondition:beforeDate:。我们在苹果的官方文档也找到了这个方法。我们继续打符号断点追踪这个方法的实现。

image.png

lockWhenCondition:beforeDate:这个方法中定位到一个objc_msgSend。然后打印方法的执行者和方法名,竟然发现是调用了NSCondition的lock方法。也就是说NSConditionLock的底层是通过NSCondition来实现加锁的。
然后我们继续往下看,发现有个cmp对比x8和x21,如果相等就跳转0x18d5cc040,否则继续往下执行。
image.png

打印x8和x21的值,分别为2和1。这个不就是我们在外面使用NSConditionLock设置的条件吗
image.png

继续往下走调用了一个"waitUntilDate:"方法。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,295评论 6 512
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,928评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,682评论 0 357
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,209评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,237评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,965评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,586评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,487评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,016评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,136评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,271评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,948评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,619评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,139评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,252评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,598评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,267评论 2 358

推荐阅读更多精彩内容

  • 目录:1.为什么要线程安全2.多线程安全隐患分析3.多线程安全隐患的解决方案4.锁的分类-13种锁4.1.1OSS...
    二斤寂寞阅读 1,188评论 0 3
  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,521评论 0 6
  • 前言 在多线程开发中,常会遇到多个线程访问修改数据。为了防止数据不一致或数据污染,通常采用加锁机制来保证线程安全。...
    赵梦楠阅读 948评论 0 5
  • 锁是最常用的同步工具。一段代码段在同一个时间只能允许被有限个线程访问,比如一个线程 A 进入需要保护代码之前添加简...
    没八阿哥的程序阅读 778评论 0 0
  • 儿子七岁了,上了一年级,男孩子调皮是天性,犯错了事基本上都是害怕的望着你,让人生气,又让人心疼 中午像往常一样骑着...
    阿标正传阅读 510评论 0 0