iOS基础 - 模拟两窗口售票:NSLock | @synchronized
▶ 线程安全
多线程同时访问一块公共资源是一件很危险的事!下面我们来模拟多窗口售票的情景
A. 使用 NSLock 保证线程安全!NSLock 是 OC 层封装底层线程操作来实现的一种锁,继承 NSLocking 协议,在此我们不讨论各种锁的实现细节,因为基本用不到。NSLock 的使用非常简单
1 #import "ViewController.h" 2 @interface ViewController (){ 3 4 NSInteger _TCNumber; // 总票数 5 NSInteger _ticketsCount; // 剩余票数 6 NSLock *_threadLock; // 线程锁 7 } 8 9 @end 10 11 @implementation ViewController 12 13 - (void)viewDidLoad { 14 [super viewDidLoad]; 15 16 // 初始化 17 _TCNumber = 500; 18 _ticketsCount = _TCNumber; 19 _threadLock = [[NSLock alloc] init]; 20 } 21 22 // 开启售票:两个售票窗口 23 - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ 24 [self saleTicketsWindow]; 25 } 26 27 - (void)saleTicketsWindow{ 28 29 // 线程A 30 NSThread *thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(makeSaled) object:nil]; 31 thread1.name = @"窗口A"; 32 // 线程B 33 NSThread *thread2 = [[NSThread alloc] initWithTarget:self selector:@selector(makeSaled) object:nil]; 34 thread2.name = @"窗口B"; 35 // 开启线程 36 [thread1 start]; 37 [thread2 start]; 38 } 39 40 - (void)makeSaled{ 41 42 while (1) { 43 44 // 上锁 45 [_threadLock lock]; 46 47 if (_ticketsCount > 0) { 48 _ticketsCount --; // 出票 49 NSInteger saleCount = _TCNumber - _ticketsCount;// 卖出张数 50 NSLog(@"售出:%ld 剩余:%ld %@卖出的",(long)saleCount,(long)_ticketsCount,[NSThread currentThread].name); 51 }else{ 52 NSLog(@"票已售完!"); 53 [_threadLock unlock];// 解锁 54 break; 55 } 56 [_threadLock unlock]; // 解锁 57 } 58 } 59 60 @end
日志信息:局部截图
B. 使用 @synchronized:它所做的事情跟锁类似,是用来防止不同的线程同时执行同一段代码。相比于使用 NSLock 创建锁对象、加锁和解锁来说,它用着更为方便、可读性更高,但并不是在任意场景下都能使用 @synchronized,它的性能较低
注:互斥锁其实就是使用了线程同步技术。它的优点就是能有效防止因多线程抢夺资源造成的数据安全问题,缺点就是需要消耗大量的 CPU 资源
1 #import "ViewController.h" 2 @interface ViewController () 3 4 @property (nonatomic,assign) NSInteger ticketsCount;// 剩余票数 5 @property (nonatomic,strong) NSThread *thread1; // 线程1 6 @property (nonatomic,strong) NSThread *thread2; // 线程2 7 8 @end 9 10 @implementation ViewController 11 12 - (void)viewDidLoad{ 13 [super viewDidLoad]; 14 15 self.ticketsCount = 5; 16 // 开启两个线程,模拟售票员 17 self.thread1=[[NSThread alloc]initWithTarget:self selector:@selector(sellTickets) object:nil]; 18 self.thread1.name=@"售票员A"; 19 self.thread2=[[NSThread alloc]initWithTarget:self selector:@selector(sellTickets) object:nil]; 20 self.thread2.name=@"售票员B"; 21 } 22 23 // 开启线程 24 -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{ 25 [self.thread1 start]; 26 [self.thread2 start]; 27 } 28 29 -(void)sellTickets{ 30 31 while (1) { 32 33 @synchronized(self){ 34 35 long count = self.ticketsCount; 36 if (count > 0) { 37 38 [NSThread sleepForTimeInterval:1.0];// 间隔 1 秒出票 39 self.ticketsCount = count - 1; 40 NSThread *currentTH = [NSThread currentThread]; 41 NSLog(@"%@--卖了一张票,还剩余%ld张票",currentTH,self.ticketsCount); 42 }else{ 43 NSLog(@"票已售完!"); 44 break; 45 } 46 } 47 } 48 } 49 50 @end
使用 @synchronized 多次对同一个对象进行锁定不会导致任何后果或问题。因为每次执行 @synchronized 块时,就会检查对象是否已被锁定;如果是,则当前线程会等待直到锁被释放,一旦锁被释放,线程可以再次获得该锁并继续执行!
多次锁定同一个对象可能会导致线程等待的时问增加,因为每次获取锁时都需要等待前一个锁释放。这可能会影响程序的性能。建议在确保必要性的情況下,尽量避免多次对同一个对象使用 @synchronized 锁定!如果可能,可以使用其他同步机制来替代,例如使用 GCD 的串行队列或使用 NSLock
注:@synchronized 加锁对象不能为 nil,否则加锁无效,并不能保证线程安全