在多线程开发中,不可忽视的一个问题就是多个线程同时访问同一个资源时,会造成脏数据等预想不到的结果,为了避免这种现象,我们需要在访问资源的时候添加线程锁,来控制访问。
添加线程锁的方式在这主要讲三种方式:
- @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的安全性。