ios:线程锁

简介: NSRecursiveLock :递归锁,有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决。

一、NSLock(普通锁)


NSLock 是对 pthread_mutex 普通锁的封装。

遵守 NSLocking 协议

它的两个方法


- (BOOL)tryLock;
- (BOOL)lockBeforeDate:(NSDate *)limit;

(BOOL)lockBeforeDate:(NSDate *)limit;

传入一个时间,在这个时间之前如果能等到这把锁放开的话,就给这把锁加锁。如果时间没到,就一直在这里等。由于 NSLock 是互斥锁,所以等待的时间就是睡觉。如果时间到了,还没有等到锁。就加锁失败。

(BOOL)tryLock;

尝试加锁

NSLocking 协议里面有两个方法


/// 加锁
- (void)lock;
- /// 解锁
- (void)unlock;


二、NSRecursiveLock(递归锁)


原理:

NSRecursiveLock :递归锁,有时候“加锁代码”中存在递归调用,递归开始前加锁,递归调用开始后会重复执行此方法以至于反复执行加锁代码最终造成死锁,这个时候可以使用递归锁来解决。

使用递归锁可以在一个线程中反复获取锁而不造成死锁,这个过程中会记录获取锁和释放锁的次数,只有最后两者平衡锁才被最终释放。


应用:

NSRecursiveLock实际上定义的是一个递归锁,这个锁可以被同一线程多次请求,而不会引起死锁。

这主要是用在循环或递归操作中。我们先来看一个示例:



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


这段代码是一个典型的死锁情况。在我们的线程中,RecursiveMethod是递归调用的。所以每次进入这个block时,都会去加一次锁,而从第二次开始,由于锁已经被使用了且没有解锁,所以它需要等待锁被解除,这样就导致了死锁,线程被阻塞住了。调试器中会输出如下信息:


2016-11-06 15:18:32.865 SSS[19932:855235] value = 5
2016-11-06 15:18:34.938 SSS[19932:855235]  -[NSLock lock]: deadlock (<NSLock: 0x6100000cc9b0> '(null)')
2016-11-06 15:18:34.938 SSS[19932:855235]  Break on _NSLockError() to debug.


在这种情况下,我们就可以使用NSRecursiveLock。它可以允许同一线程多次加锁,而不会造成死锁。递归锁会跟踪它被lock的次数。每次成功的lock都必须平衡调用unlock操作。只有所有达到这种平衡,锁最后才能被释放,以供其它线程使用。

所以,对上面的代码进行一下改造


NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

输出的结果如下:

2016-11-06 15:21:19.315 SSS[19972:857154] value = 5
2016-11-06 15:21:21.389 SSS[19972:857154] value = 4
2016-11-06 15:21:23.464 SSS[19972:857154] value = 3
2016-11-06 15:21:25.537 SSS[19972:857154] value = 2
2016-11-06 15:21:27.612 SSS[19972:857154] value = 1


三、OSSpinLock(自旋锁,弃用)


1、含义

OSSpinLock叫做”自旋锁”,等待锁的线程会处于忙等(busy-wait)状态,一直占用着CPU资源

目前已经不再安全,可能会出现优先级反转问题

如果等待锁的线程优先级较高,它会一直占用着CPU资源,优先级低的线程就无法释放锁

需要导入头文件 #import

2、主要代码

@property(nonatomic,assign) OSSpinLock sLock;
self.sLock = OS_SPINLOCK_INIT;
// 加锁, 因为要传的是指针,所以传入地址
OSSpinLockLock(&_sLock);
// 尝试加锁(如果需要等待就不加锁,直接返回false;如果不需要等待就加锁,返回true)
BOOL result = OSSpinLockTry(&(_sLock));
// 解锁
OSSpinLockUnlock(&_sLock);


3、卖票示例

示例前准备:


/// 操作按钮
@property (nonatomic, strong) UIButton *btn;
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.btn];
}
#pragma mark- Methods
- (void)btnAction {
}
#pragma mark- Setting
- (UIButton *)btn {
    if (!_btn) {
        _btn = [UIButton buttonWithType:UIButtonTypeCustom];
        _btn.frame = CGRectMake(100, 100, 100, 30);
        _btn.backgroundColor = [UIColor lightGrayColor];
        [_btn setTitle:@"LockTest" forState:UIControlStateNormal];
        [_btn addTarget:self action:@selector(btnAction) forControlEvents:UIControlEventTouchUpInside];
    }
    return _btn;
}


场景演示代码:


/// 添加操作演示
- (void)btnAction {
    [self ticketTest];
}
/// 卖票演示
- (void)ticketTest {
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    // 窗口1
    dispatch_async(queue, ^{
      // 卖票
        for (int i = 0 ; i < 3; i++) {
            [self saleTicket];
        }
    });
    // 窗口2
    dispatch_async(queue, ^{
      // 卖票
        for (int i = 0 ; i < 3; i++) {
            [self saleTicket];
        }
    });
    // 窗口3
    dispatch_async(queue, ^{
      // 卖票
        for (int i = 0 ; i < 3; i++) {
            [self saleTicket];
        }
    });
}


没加锁代码:


/// 总票数
/// 操作按钮
@property (nonatomic, strong) UIButton *btn;
/// 总票数
@property (nonatomic, assign) NSInteger ticketsCount;
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.btn];
    self.ticketsCount = 15;
}
/// 添加操作演示
- (void)btnAction {
    [self ticketTest];
}
/// 卖1张票
- (void)saleTicket {
    // 原来代码
    if (self.ticketsCount > 0) {
        NSInteger oldTicketsCount = self.ticketsCount;
        sleep(.2);
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        NSLog(@"还剩 %ld 张票 -%@", (long)oldTicketsCount, [NSThread currentThread]);
    } else {
        NSLog(@"票已卖完,建议购买下班车 -%@", [NSThread currentThread]);
    }
}


控制台输出:


2021-03-26 11:03:38.678146+0800 LockDemo[37717:19153535] 还剩 14 张票 -<NSThread: 0x282435280>{number = 25, name = (null)}
2021-03-26 11:03:38.678146+0800 LockDemo[37717:19153177] 还剩 14 张票 -<NSThread: 0x2824c13c0>{number = 20, name = (null)}
2021-03-26 11:03:38.678146+0800 LockDemo[37717:19153708] 还剩 14 张票 -<NSThread: 0x282b6dec0>{number = 29, name = (null)}
2021-03-26 11:03:38.882707+0800 LockDemo[37717:19153708] 还剩 13 张票 -<NSThread: 0x282b6dec0>{number = 29, name = (null)}
2021-03-26 11:03:38.883040+0800 LockDemo[37717:19153535] 还剩 13 张票 -<NSThread: 0x282435280>{number = 25, name = (null)}
2021-03-26 11:03:38.883166+0800 LockDemo[37717:19153177] 还剩 13 张票 -<NSThread: 0x2824c13c0>{number = 20, name = (null)}
2021-03-26 11:03:39.087065+0800 LockDemo[37717:19153535] 还剩 12 张票 -<NSThread: 0x282435280>{number = 25, name = (null)}
2021-03-26 11:03:39.087065+0800 LockDemo[37717:19153177] 还剩 12 张票 -<NSThread: 0x2824c13c0>{number = 20, name = (null)}
2021-03-26 11:03:39.087065+0800 LockDemo[37717:19153708] 还剩 12 张票 -<NSThread: 0x282b6dec0>{number = 29, name = (null)}


加锁代码:

加锁: 别的线程无法再进行访问

解锁: 用完后,解除锁定,让其他线程可以访问。

如果使用须导入 #import

第一次加锁:在 saleTicket 方法中 ,一开始就加锁


演示前准备:


/// 操作按钮
@property (nonatomic, strong) UIButton *btn;
/// 自旋锁
@property(nonatomic,assign) OSSpinLock sLock;
/// 总票数
@property (nonatomic, assign) NSInteger ticketsCount;
- (void)viewDidLoad {
    [super viewDidLoad];
    [self.view addSubview:self.btn];
    self.sLock = OS_SPINLOCK_INIT;
    self.ticketsCount = 15;
}


代码加锁:


/// 卖1张票
- (void)saleTicket {
    // 加锁, 因为要传的是指针,所以传入地址
    OSSpinLockLock(&_sLock);
    // 原来代码
    if (self.ticketsCount > 0) {
        NSInteger oldTicketsCount = self.ticketsCount;
        sleep(.2);
        oldTicketsCount--;
        self.ticketsCount = oldTicketsCount;
        NSLog(@"还剩 %ld 张票 -%@", (long)oldTicketsCount, [NSThread currentThread]);
    } else {
        NSLog(@"票已卖完,建议购买下班车 -%@", [NSThread currentThread]);
    }
    // 解锁
    OSSpinLockUnlock(&_sLock);
}


控制台输出:


2021-03-26 11:13:32.843142+0800 LockDemo[37750:19157258] 还剩 14 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)}
2021-03-26 11:13:33.045948+0800 LockDemo[37750:19157258] 还剩 13 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)}
2021-03-26 11:13:33.248671+0800 LockDemo[37750:19157258] 还剩 12 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)}
2021-03-26 11:13:33.471973+0800 LockDemo[37750:19157646] 还剩 11 张票 -<NSThread: 0x283343c80>{number = 29, name = (null)}
2021-03-26 11:13:33.677452+0800 LockDemo[37750:19157646] 还剩 10 张票 -<NSThread: 0x283343c80>{number = 29, name = (null)}
2021-03-26 11:13:33.882731+0800 LockDemo[37750:19157646] 还剩 9 张票 -<NSThread: 0x283343c80>{number = 29, name = (null)}
2021-03-26 11:13:34.115218+0800 LockDemo[37750:19157645] 还剩 8 张票 -<NSThread: 0x283322180>{number = 26, name = (null)}
2021-03-26 11:13:34.318081+0800 LockDemo[37750:19157645] 还剩 7 张票 -<NSThread: 0x283322180>{number = 26, name = (null)}
2021-03-26 11:13:34.519596+0800 LockDemo[37750:19157645] 还剩 6 张票 -<NSThread: 0x283322180>{number = 26, name = (null)}
2021-03-26 11:13:36.509108+0800 LockDemo[37750:19157258] 还剩 5 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)}
2021-03-26 11:13:36.714835+0800 LockDemo[37750:19157258] 还剩 4 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)}
2021-03-26 11:13:36.920536+0800 LockDemo[37750:19157258] 还剩 3 张票 -<NSThread: 0x283cd1500>{number = 9, name = (null)}
2021-03-26 11:13:37.135940+0800 LockDemo[37750:19157645] 还剩 2 张票 -<NSThread: 0x283322180>{number = 26, name = (null)}
2021-03-26 11:13:37.337543+0800 LockDemo[37750:19157645] 还剩 1 张票 -<NSThread: 0x283322180>{number = 26, name = (null)}
2021-03-26 11:13:37.539328+0800 LockDemo[37750:19157645] 还剩 0 张票 -<NSThread: 0x283322180>{number = 26, name = (null)}
2021-03-26 11:13:37.576318+0800 LockDemo[37750:19157646] 票已卖完,建议购买下班车 -<NSThread: 0x283343c80>{number = 29, name = (null)}
2021-03-26 11:13:37.576725+0800 LockDemo[37750:19157646] 票已卖完,建议购买下班车 -<NSThread: 0x283343c80>{number = 29, name = (null)}
2021-03-26 11:13:37.576868+0800 LockDemo[37750:19157646] 票已卖完,建议购买下班车 -<NSThread: 0x283343c80>{number = 29, name = (null)}


4、 锁原理分析


加锁本质就是线程阻塞,常见的线程阻塞有两种:


1、 相当于写了一个 while 循环,一直在循环代码

2、让线程直接睡眠

OSSpinLock 这个锁的线程阻塞就是忙等。


OSSpinLock 叫做”自旋锁”。等待别人把锁解开的时间中,一直占用着CPU 资源。这种状态我们叫做忙等,即一边忙着做事情,一边等待着解开锁。

其实就相当于写了一个while 循环。

相当于写了这样一行代码 while(锁还没被放开);

每次执行的时候,都会看下锁是否被放开,如果没有被放开,就执行一次。如果被放开了,就不在执行。

注意: 目前 OSSpinLock 已经不在安全,可能会出现优先级翻转问题。


例如:

我们有三个线程 , thread1,thread2,thread3。当我们支持多线程,并开启三条线程时,有可能3条线程同时做事情。那么系统是如何调度三条线程的?

流程分析:

1、安排时间给 三条线程,每条线程执行都执行一小会(也许三秒钟?)。例如,先给 thread1 3秒钟,在给 thread2 3秒钟,在给 thread3 3秒钟。然后在给 thread1 3秒钟 … … 一直这样,直到3个线程执行完毕。其实这是多线程的原理。

2、这也叫做 时间片轮转调度算法(操作系统在调度进程和线程时基本上都是按照上面的套路),它会牵扯到线程优先级问题,* 如果线程优先级高,给分配的时间就多


问题:


优先级翻转的问题。如thread1: 优先级高,thread2: 优先级低;

当 thread2 先进入 saleTicket 的锁中,就会先把OSSpinLockLock(&_lock) 加锁。这样当线程1 进入时,发现锁已经被加了。线程1 只能忙等。也就是在做 while(未解锁)这一行代码。

由于 thread1 的优先级高,很有可能 CPU 一直分配时间给 它,也就是说,CPU一直在做 while(未解锁)这一行代码。

也就是说,CPU 可能没有时间在 分配给 thread2 ,会照成 thread2 的线程不能再往下执行,也就永远无法放开这把锁。

这就是 自旋锁会发生的事情。

如果是 睡眠,就不会发生这种事情。

线程就不分配时间给thread1, thread2 就可以继续往下走,就可以完成解锁操作。然后 thread1 从休眠中唤醒,发现锁已经解开,thread1 就进行加锁,往下执行。


这就是为啥在 使用 OSSpinLockLock 的时候,苹果会提示 这个锁已经 不在使用。


还有更多,后面有时间加—


参考:


1、线程锁-NSRecursiveLock

2、多线程六——加锁方案一 : OSSpinLock


3、线程锁- @synchronized

4、线程锁-NSConditionLock

5、zhuge1127的博客


相关文章
|
2月前
|
安全 编译器 C#
C#学习相关系列之多线程---lock线程锁的用法
C#学习相关系列之多线程---lock线程锁的用法
|
2月前
|
存储 Java
高并发编程之多线程锁和Callable&Future 接口
高并发编程之多线程锁和Callable&Future 接口
28 1
|
2月前
|
存储 安全 Java
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
并发编程知识点(volatile、JMM、锁、CAS、阻塞队列、线程池、死锁)
71 3
|
11天前
|
算法 Java 编译器
【JavaEE多线程】掌握锁策略与预防死锁
【JavaEE多线程】掌握锁策略与预防死锁
20 2
|
11天前
|
安全 Java 编译器
【JavaEE多线程】线程安全、锁机制及线程间通信
【JavaEE多线程】线程安全、锁机制及线程间通信
31 1
|
12天前
|
安全 Java 调度
Java并发编程:深入理解线程与锁
【4月更文挑战第18天】本文探讨了Java中的线程和锁机制,包括线程的创建(通过Thread类、Runnable接口或Callable/Future)及其生命周期。Java提供多种锁机制,如`synchronized`关键字、ReentrantLock和ReadWriteLock,以确保并发访问共享资源的安全。此外,文章还介绍了高级并发工具,如Semaphore(控制并发线程数)、CountDownLatch(线程间等待)和CyclicBarrier(同步多个线程)。掌握这些知识对于编写高效、正确的并发程序至关重要。
|
15天前
|
存储 缓存 Java
线程同步的艺术:探索 JAVA 主流锁的奥秘
本文介绍了 Java 中的锁机制,包括悲观锁与乐观锁的并发策略。悲观锁假设多线程环境下数据冲突频繁,访问前先加锁,如 `synchronized` 和 `ReentrantLock`。乐观锁则在访问资源前不加锁,通过版本号或 CAS 机制保证数据一致性,适用于冲突少的场景。锁的获取失败时,线程可以选择阻塞(如自旋锁、适应性自旋锁)或不阻塞(如无锁、偏向锁、轻量级锁、重量级锁)。此外,还讨论了公平锁与非公平锁,以及可重入锁与非可重入锁的特性。最后,提到了共享锁(读锁)和排他锁(写锁)的概念,适用于不同类型的并发访问需求。
246 2
|
16天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
18天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(下)
42 0
|
18天前
|
存储 安全 Java
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)(上)
多线程编程常见面试题讲解(锁策略,CAS策略,synchronized原理,JUC组件,集合类)
35 0

热门文章

最新文章