为什么并发执行线程要加锁
保证共享资源访问的安全性,避免出现竞态条件
竞态条件:多个线程同时访问和修改共享资源时,由于线程调度的不确定性,会出现多个线程交叉执行的情况
通过加锁,确保任何时刻只有一个线程访问共享数据,从而避免竞态条件,确保数据的一致性和完整性。
自旋锁是什么?
特点:当线程尝试获取锁但是锁已经被占用时,不会立即阻塞,而是通过循环重试(忙等)的方式来持续检查锁的状态,直到获取锁为止。
自旋锁通过CPU提供的CAS函数,在用户态完成加锁和解锁操作,不会主动产生线程上下文切换,因此开销更小,自旋锁会占用CPU资源,因此适合锁被占用时间极短的场景。
一般加锁的过程:
- 查看锁的状态,如果锁是空闲的,执行第二步
- 将锁设置为当前线程所有
CAS合并这两步形成原子指令
注意:单核CPU需要抢占式调度器,否则自旋锁不能使用,因为自旋锁不会放弃CPU的控制权
死锁发生的条件
死锁:多个线程互相等待对方所持有的资源,且每个进程都无法主动释放自己已占有的资源时,导致线程的永久阻塞。
- 互斥条件:多个线程同时使用同一个资源
- 持有并等待: 线程已持有部分资源,又去请求其他资源,请求失败也不去释放已有资源而是继续持有并等待
- 不可剥夺条件:线程的资源不能被强制夺取,只能由持有者自己释放
- 环路等待条件:存在线程-资源的循环等待链,每个进程都在等待链中下一个进程锁持有的资源,形成闭环。
如何避免死锁
破坏死锁的任意一个条件即可,最常用且可行的是使用资源有序分配法,破坏环路等待条件
资源有序分配法:线程A和线程B所需的资源的顺序一样,当线程A尝试先获取资源A,然后获取资源B,线程B也是先获取资源A再获取资源B。也就是说,线程A和线程B总是以相同的顺序获取自己需要的资源。
银行家算法
银行家算法是一种用来避免死锁的资源分配策略。
在给进程分配资源前,先判断进程的安全性,也就是预执行,判断分配后是否存在死锁等现象。如果当前资源能满足其执行,则尝试分配,如果不满足,则让进程等待
通过不断检查剩余可用资源,看是否满足某个进程的最大需求,如果可以则加入安全序列,并把该进程当前持有的资源回收;不断重复这个过程,看最后能否把所有进程都添加进安全序列
安全序列一定不会发生死锁,但没有死锁不一定是安全序列
银行家算法缺点:
- 进程提前声明最大资源需求”,实际难满足
- 不适用于资源 / 进程动态变化的系统
- 安全性检查时间复杂度高,开销大
乐观锁和悲观锁的区别
乐观锁:
- 基本思想:乐观锁假设多个事务之间很少发生冲突,因此读取数据时不会加锁,而是在更新数据时检查数据的版本(时间辍或版本号),如果版本匹配则执行更新操作,否则认为发生了冲突
- 使用场景:乐观锁适合读多写少的场景,可以减少锁的竞争,提高并发性能。如数据库中的乐观锁可以用于处理并发更新同一行的场景。
悲观锁:
- 基本思想:假设多个事务之间频繁发生冲突,读取数据时会加锁,防止其他事物对数据进行修改,直到事务完成后释放锁
- 使用场景:适合写多的场景,通过加锁保证数据的一致性。如数据库的行级锁机制用来处理并发更新同一行的情况。
乐观锁适用于读多写少的场景,使用版本控制避免冲突;悲观锁适用于写多的场景,通过加锁避免冲突。