iOS多线程之线程锁

在多线程开发中,不可忽视的一个问题就是多个线程同时访问同一个资源时,会造成脏数据等预想不到的结果,为了避免这种现象,我们需要在访问资源的时候添加线程锁,来控制访问。

添加线程锁的方式在这主要讲三种方式:

  • @synchronized隐式创建锁对象
  • NSLock
  • GCD的dispatch_semaphore_t信号机制
一、@synchronized( )

@synchronized()内的对象设定为锁的唯一标识,只有标识相同时,才满足互斥

-(void)testSynchronized
{
    GCDcreate *someone = [GCDcreate new];
    //线程A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (someone) {
            NSLog(@"线程A=%@",[NSThread currentThread]);
            someone.name = @"我是线程A";
            [NSThread sleepForTimeInterval:5];
        }
    });
    //线程B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        @synchronized (someone) {
            NSLog(@"线程B=%@",[NSThread currentThread]);
            someone.name = @"我是线程B";
        }
    });
}
二、NSLock

NSLock的lock和unlock必须在同一线程;同一线程lock后,未unlock前再lock会导致永久性死锁。

-(void)testNSLock
{
    GCDcreate *someone = [GCDcreate new];
    //创建锁对象
    NSLock *alock = [[NSLock alloc] init];
    //线程A
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        if ([alock tryLock]) //上锁 或 [alock lock]
        {
            NSLog(@"线程A=%@",[NSThread currentThread]);
            someone.name = @"我是线程A";
            [NSThread sleepForTimeInterval:5];
            //开锁
            [alock unlock];
        }
    });
    //线程B
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        if ([alock tryLock]) //上锁 或 [alock lock]
        {
            NSLog(@"线程B=%@",[NSThread currentThread]);
            someone.name = @"我是线程B";
            //开锁
            [alock unlock];
        }
    });
}
三、dispatch_semaphore_t

在上一篇GCD用法详解中讲解到dispatch_semaphore_t信号量的用法,它的用法很灵活,其中一种就是可以当成线程锁来用。

这里拿一个现实中的例子来解释一下它的用法,再买火车的时候,会有很多窗口或代售点出售火车票,那么火车票的余票数是保存在数据库中的,每个出售的地方都会去读写这个余票数。如果不加锁的话,就会出现多个窗口买同一张票的情况。比如:

//初始化火车票数量、卖票窗口、并开始卖票
- (void)testGCD {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    self.ticketSurplusCount = 50;
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("com.jzsec.GCDtest", DISPATCH_QUEUE_CONCURRENT);
    //queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("com.jzsec.GCDtest1", DISPATCH_QUEUE_CONCURRENT);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

//售卖火车票
- (void)saleTicketSafe {
    while (1) {
        if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { //如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");
            break;
        }
    }   
}

结果比较长就不全贴出来了,其中一段就可以看出问题:

...
2019-08-09 18:36:32.036600+0800 GCDtest[2616:300126] 剩余票数:40 窗口:<NSThread: 0x600001b8c080>{number = 3, name = (null)}
2019-08-09 18:36:32.036600+0800 GCDtest[2616:300123] 剩余票数:39 窗口:<NSThread: 0x600001b87f80>{number = 4, name = (null)}
2019-08-09 18:36:32.237564+0800 GCDtest[2616:300123] 剩余票数:38 窗口:<NSThread: 0x600001b87f80>{number = 4, name = (null)}
2019-08-09 18:36:32.237565+0800 GCDtest[2616:300126] 剩余票数:38 窗口:<NSThread: 0x600001b8c080>{number = 3, name = (null)}
2019-08-09 18:36:32.442719+0800 GCDtest[2616:300123] 剩余票数:37 窗口:<NSThread: 0x600001b87f80>{number = 4, name = (null)}
2019-08-09 18:36:32.442719+0800 GCDtest[2616:300126] 剩余票数:37 窗口:<NSThread: 0x600001b8c080>{number = 3, name = (null)}
2019-08-09 18:36:32.644508+0800 GCDtest[2616:300123] 剩余票数:36 窗口:<NSThread: 0x600001b87f80>{number = 4, name = (null)}
2019-08-09 18:36:32.644508+0800 GCDtest[2616:300126] 剩余票数:35 窗口:<NSThread: 0x600001b8c080>{number = 3, name = (null)}
...

这里出现两个37和两个38,就是因为两个线程同时读写self.ticketSurplusCount余票数造成的,如果线程更多的话,后果不堪设想。

所以我们对程序加上线程锁,改造如下:

/**
 * 线程安全:使用 semaphore 加锁
 * 初始化火车票数量、卖票窗口(线程安全)、并开始卖票
 */
- (void)testGCD {
    NSLog(@"currentThread---%@",[NSThread currentThread]);  // 打印当前线程
    NSLog(@"semaphore---begin");
    
    semaphoreLock = dispatch_semaphore_create(1);

    self.ticketSurplusCount = 50;    
    // queue1 代表北京火车票售卖窗口
    dispatch_queue_t queue1 = dispatch_queue_create("com.jzsec.GCDtest", DISPATCH_QUEUE_CONCURRENT);
    //queue2 代表上海火车票售卖窗口
    dispatch_queue_t queue2 = dispatch_queue_create("com.jzsec.GCDtest1", DISPATCH_QUEUE_CONCURRENT);
    
    __weak typeof(self) weakSelf = self;
    dispatch_async(queue1, ^{
        [weakSelf saleTicketSafe];
    });
    
    dispatch_async(queue2, ^{
        [weakSelf saleTicketSafe];
    });
}

/**
 * 售卖火车票(线程安全)
 */
- (void)saleTicketSafe {
    while (1) {
        // 相当于加锁
        dispatch_semaphore_wait(semaphoreLock, DISPATCH_TIME_FOREVER);
        
        if (self.ticketSurplusCount > 0) {  //如果还有票,继续售卖
            self.ticketSurplusCount--;
            NSLog(@"%@", [NSString stringWithFormat:@"剩余票数:%d 窗口:%@", self.ticketSurplusCount, [NSThread currentThread]]);
            [NSThread sleepForTimeInterval:0.2];
        } else { //如果已卖完,关闭售票窗口
            NSLog(@"所有火车票均已售完");   
            // 相当于解锁
            dispatch_semaphore_signal(semaphoreLock);
            break;
        }   
        // 相当于解锁
        dispatch_semaphore_signal(semaphoreLock);
    }   
}
2019-08-09 18:41:29.843021+0800 GCDtest[2660:303158] currentThread---<NSThread: 0x600002f86a00>{number = 1, name = main}
2019-08-09 18:41:29.843230+0800 GCDtest[2660:303158] semaphore---begin
2019-08-09 18:41:29.844289+0800 GCDtest[2660:303249] 剩余票数:49 窗口:<NSThread: 0x600002fd6440>{number = 3, name = (null)}
2019-08-09 18:41:30.048023+0800 GCDtest[2660:303248] 剩余票数:48 窗口:<NSThread: 0x600002fd7400>{number = 4, name = (null)}
2019-08-09 18:41:30.252102+0800 GCDtest[2660:303249] 剩余票数:47 窗口:<NSThread: 0x600002fd6440>{number = 3, name = (null)}
2019-08-09 18:41:30.453306+0800 GCDtest[2660:303248] 剩余票数:46 窗口:<NSThread: 0x600002fd7400>{number = 4, name = (null)}
2019-08-09 18:41:30.657614+0800 GCDtest[2660:303249] 剩余票数:45 窗口:<NSThread: 0x600002fd6440>{number = 3, name = (null)}
... 省略 ...
2019-08-09 18:41:39.179921+0800 GCDtest[2660:303249] 剩余票数:3 窗口:<NSThread: 0x600002fd6440>{number = 3, name = (null)}
2019-08-09 18:41:39.381520+0800 GCDtest[2660:303248] 剩余票数:2 窗口:<NSThread: 0x600002fd7400>{number = 4, name = (null)}
2019-08-09 18:41:39.583975+0800 GCDtest[2660:303249] 剩余票数:1 窗口:<NSThread: 0x600002fd6440>{number = 3, name = (null)}
2019-08-09 18:41:39.786290+0800 GCDtest[2660:303248] 剩余票数:0 窗口:<NSThread: 0x600002fd7400>{number = 4, name = (null)}
2019-08-09 18:41:39.988175+0800 GCDtest[2660:303249] 所有火车票均已售完
2019-08-09 18:41:39.988514+0800 GCDtest[2660:303248] 所有火车票均已售完

此时就可以看到与票数是依次正确减少的,这种访问数据才是线程安全的。

初始semaphoreLock为1,第一个线程进入saleTicketSafe时dispatch_semaphore_wait使semaphoreLock-1为0,第二个线程再进入saleTicketSafe时dispatch_semaphore_wait使semaphoreLock-1为-1,第二个线程就会一直等待,等第一个线程扣除余票后dispatch_semaphore_signal使semaphoreLock+1为0,此时正在等待中的第二个线程开始执行;

以此类推,总是同时只有一个线程在访问它们的共享资源self.ticketSurplusCount,保证了self.ticketSurplusCount的安全性。

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

推荐阅读更多精彩内容

  • 锁是一种同步机制,用于多线程环境中对资源访问的限制iOS中常见锁的性能对比图(摘自:ibireme): iOS锁的...
    LiLS阅读 1,507评论 0 6
  • demo下载 建议一边看文章,一边看代码。 声明:关于性能的分析是基于我的测试代码来的,我也看到和网上很多测试结果...
    炸街程序猿阅读 784评论 0 2
  • 线程安全是怎么产生的 常见比如线程内操作了一个线程外的非线程安全变量,这个时候一定要考虑线程安全和同步。 - (v...
    幽城88阅读 647评论 0 0
  • 一、前言 前段时间看了几个开源项目,发现他们保持线程同步的方式各不相同,有@synchronized、NSLock...
    稻春阅读 464评论 0 0
  • 今天继续听书,热播有声书《大江大河》,听到一个金句: 人不可能永远处于从属地位,人得在工作之外有所布局,主动是最好...
    吟花飞阅读 882评论 0 0