多线程NSThread安全隐患与解除方法—— 利用加锁@synchronized(){}来解决

来源:互联网 发布:阿里云 系统盘 数据盘 编辑:程序博客网 时间:2024/06/07 08:08
 多线程的安全隐患。
资源共享
1块资源可能会被多个线程共享,也就是多个线程可能会访问同一块资源。比如多个线程访问同一个对象、同一个变量、同一个文件。
当多个线程访问同一块资源时,很容易引起数据错乱和数据安全问题。

例如:存钱取钱的问题:如下图所示


数据库中一个账户的余额是唯一的,假设有两个线程同时访问修改余额就会发生错误,例如:两个线程同时拿到余额值都为1000,第一个线程在此基础上添加1000, 但是还未添加完毕即在将修改后的余额2000存放到数据库之前,另一个线程同时也在修改余额假设是取500本来获取的余额应该是第一个线程添加后的余额2000,但是由于没有添加限制所以第二个线程获取的余额值是不是更新后的值,而是原先的1000。所以当第一个线程把2000存放到数据库之前,第二个线程就开始获取余额进行修改了。造成错误。卖票原理与此类似。如下图所示:



安全隐患的解决-互斥锁  

添加互斥锁解决卖票问题,原理如下:为共享的资源即数据库中剩余的票数加了一把锁,当第一个线程修改共享资源时,立刻把锁锁住即在第一个线程没有修改完成(或把修改后的剩余票数存入数据库)之前,是不允许别的线程对剩余票数进行修改的。只有当第一个获取资源的线程修改完成后,才会把锁打开。然后才会允许别的线程对唯一的剩余票数进行修改。这就保证在同一时间只允许同一线程对共享资源进行修改,从而避免了多个线程同时修改同一资源造成数据错乱的错误。加锁前与加锁后的分析对比如下:

加锁前:


加锁后:



互斥锁的使用格式:
@synchronized(锁对象){  // 需要锁定的代码}
注意:锁定一份代码只用一把锁,用多把锁是无效的
互斥锁的优缺点:
优点:能有效防止因多线程抢夺资源造成的数据安全问题。
缺点:需要消耗大量的CPU资源。
互斥锁的使用前提:多条线程抢夺同一资源
相关专业术语:线程同步
线程同步的意思是:多条线程按顺序的执行任务。
互斥锁,就是使用了线程同步技术。

原子属性与非原子属性操作:
OC在定义属性时有nonatomic和atomic两种选择。
atomic:原子属性,为setter方法加锁(默认是atomic)
nonatomic:非原子属性,不会为setter方法加锁。
二者对比:
atomic:线程安全,需要消耗大量的资源。
nonatomic:非线程安全,适合内存小的移动设备。
IOS开发的建议:
所有的属性都声明为nonatomic
尽量避免多线程抢夺同一块资源
尽量将加锁、资源抢夺的业务逻辑交给服务器端处理,减小移动客户端的压力。
atomic属性为setter方法加的锁被称为自旋锁。我们在setter方法中使用@synchronized为变量加的锁被称为互斥锁。二者的共同点如下:当多个线程抢夺资源变量时,都在setter方法中为其变量加锁,即同一时间只允许一个线程调用set方法修改变量的值。就是说不允许多个线程同时调用set方法修改同一 变量的值。
不同之处如下:
当多个执行修改同一变量操作的线程被创建并start后被移入到线程池中,当第一个线程获得CPU后处于运行状态,由于线程池中的线程是轮流获取时间片的,在第一个线程获得了资源但失去时间片后由于任务没有修改完成资源锁仍然未打开,只要获得资源的线程未完成任务之前,锁是不会打开的。其他需要修改该共享资源(变量)的所有线程只要获得时间片在互斥锁的情况下都会被置于休眠状态即阻塞状态,待获得资源的线程完成任务即解锁之后才会被唤醒来进行互斥资源的抢夺。所不同的是而自旋锁不会把等待锁的就绪进程移出线程池,而是让它们一直处于就绪状态在内存中进行等待,与互斥锁相比就省去了让等待锁的线程休眠和在锁打开时唤醒休眠的线程的步骤。因而,自旋锁的效率相对比自己重写setter为变量加互斥锁的方法要高一些。也就是说系统为atomic属性设置的自旋锁比互斥锁少了一个休眠和唤醒的步骤。
需要注意的是:atomic情况下,只要重写了set方法,getter方法也得重写。

新建第一个工程,分析安全隐患:

////  ViewController.m//  买火车票的安全隐患////  Created by apple on 15/10/5.//  Copyright (c) 2015年 LiuXun. All rights reserved.//#import "ViewController.h"@interface ViewController ()/**   总票数 */@property (nonatomic, assign) int tickets;@end@implementation ViewController-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{  // 剩余二十张票    self.tickets = 20;        // 主线程工作//    [self saleTickets];        // 增加子线程 卖票    NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];    threadA.name = @"售票员A";    [threadA start];        NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];    threadB.name = @"售票员B";    [threadB start];        }#pragma mark -卖票/** 1. 开发比较复杂的多线程程序时,可以让现在的主线程把功能实现 2. 实现功能以后,可以把耗时的功能放到子线程 3. 再增加一个线程,建议开发的时候线程一个一个增加 */-(void)saleTickets{    while (YES) {        // 模拟一下延时,卖一张票休息1秒        [NSThread sleepForTimeInterval:1];                // 1 判断是否还有票        if (self.tickets > 0) {            // 2、如果有票就卖一张            self.tickets--;            NSLog(@"剩余的票数--%d--%@",self.tickets, [NSThread currentThread]);        }else        {         // 3、 如果没有就返回            NSLog(@"没有票了");            break;        }    }}- (void)didReceiveMemoryWarning {    [super didReceiveMemoryWarning];    // Dispose of any resources that can be recreated.}@end
运行结果如下:

当只运行主线程时如下所示:


当只运行两个同时访问同一变量即剩余票数的单线程,如下:



需要加锁解决多线程抢夺资源时造成的数据错乱问题,新建第二个工程如下:

////  ViewController.m//  解决买火车票的安全隐患////  Created by apple on 15/10/5.//  Copyright (c) 2015年 LiuXun. All rights reserved.//#import "ViewController.h"@interface ViewController ()/**  线程安全的概念:就是在多个线程同时执行的时候,能够保证资源信息的准确性 “UI线程-- 主线程” ** UIKit 中绝对部分的类,都不是”线程安全“的 怎么解决这个线程不安全的问题?  苹果约定,所有程序的更新UI都在主线程进行,也就不会出现多个资源同时改变一个资源  // 在主线程更新UI有什么好处? 1. 只在主线程更新UI,就不会出现多个线程同时改变 同一个UI控件 2. 主线程优先级最高。也就意味着UI的更新优先级高。会让用户感觉用着很流畅. *//** 总票数 */@property (nonatomic, assign) int tickets;@property (atomic, assign) NSObject *obj;// nonatomic 非原子性操作// atomic 原子属性— 默认属性//  原子属性就是针对多线程设计的。原子属性实现单(线程)写 多(线程)读//  因为写的安全级别最高。读的要求低一些,可以多读几次来保证数据的正确性@end@implementation ViewController// 如果同时重写了setter和getter方法,"_成员变量"就不会再提供// 可以使用@synthesize 合成指令,告诉编译器属性的成员变量的名字@synthesize obj = _obj;-(NSObject *)obj{    return _obj;}// atomic情况下,只要重写了set方法,getter方法也得重写-(void) setObj:(NSObject *)obj{    // 原子属性内部使用的是 自旋锁    // 自旋锁和互斥锁    // 共同点:都可以锁定一段代码。同一时间,只有一个线程能够执行这段锁定的代码。    // 区别:互斥锁,在锁定的时候。其他线程会睡眠,等待条件满足再唤醒    // 自旋锁:在锁定的时候,其他的线程会做死循环,一直等待这条件满足,一旦条件满足马上去执行,少了一个唤醒的过程    @synchronized(self){ // 模拟锁,真实情况下使用的不是互斥锁        _obj = obj;    }}-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{    // 剩余二十张票    self.tickets = 20;        // 主线程工作    //    [self saleTickets];        // 增加子线程 卖票    NSThread *threadA = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];    threadA.name = @"售票员A";    [threadA start];        NSThread *threadB = [[NSThread alloc] initWithTarget:self selector:@selector(saleTickets) object:nil];    threadB.name = @"售票员B";    [threadB start];        }#pragma mark -卖票/** 1. 开发比较复杂的多线程程序时,可以让现在的主线程把功能实现 2. 实现功能以后,可以把耗时的功能放到子线程 3. 再增加一个线程,建议开发的时候线程一个一个增加 *//** 加锁,互斥锁 加锁时,锁定的代码尽量少 加锁范围内的代码,同一时间只允许一个线程执行 互斥锁的参数:任何继承NSObject的对象都可以 要保证这个锁,所有的线程都能访问的到,而且所有线程访问的是一个锁对象 */-(void)saleTickets{    while (YES) {        // 模拟一下延时,卖一张票休息1秒        [NSThread sleepForTimeInterval:1];                @synchronized(self){  // 因为苹果公司不推荐我们使用锁,使用锁性能会很差,所以没有提示        // 1 判断是否还有票        if (self.tickets > 0) {            // 2、如果有票就卖一张            self.tickets--;            NSLog(@"剩余的票数--%d--%@",self.tickets, [NSThread currentThread]);        }else        {            // 3、 如果没有就返回            NSLog(@"没有票了");            break;        }        }    }    }- (void)didReceiveMemoryWarning {    [super didReceiveMemoryWarning];    // Dispose of any resources that can be recreated.}@end
运行结果如下:




0 0
原创粉丝点击