多线程——常见的锁策略

一、什么叫锁策略?

锁策略是一把锁的具体实现方式,常见的锁策略有:

①悲观锁&乐观锁    ②轻量级锁&重量级锁   ③自旋锁&挂起等待锁    ④互斥锁&读写锁   

 ⑤不可重入锁(死锁)&可重入锁(不死锁) ⑥公平锁&非公平锁

二、悲观锁&乐观锁 

锁的实现者,预测接下来锁竞争的概率,根据概率来决定接下来怎么做。

(1)悲观锁

1.概念

预测锁竞争的概率大,导致做的工作多、效率低;(每次拿数据的时候都认为别人会修改,所以每次拿取数据的时候都会上锁,获取到锁再操作数据,获取不到就等待;别人拿数据时都要阻塞等待直到他下一次拿数据解锁)

2.应用场景

若真实的锁冲突比较大,使用悲观锁会比较合适;使用乐观锁会导致”白跑很多趟“,效率低下。

例如——同学 A 和 同学 B 想请教老师一个问题. 
同学 A 认为 "老师是比较忙(锁冲突大)的, 我来问问题, 老师不一定有空解答". 因此同学 A 会先给老师发消息: "老师你忙嘛? 我下午两点能来找你问个问题嘛?" (相当于加锁操作) 得到肯定的答复之后, 才会真的来问问题. 
如果得到了否定的答复, 那就等一段时间, 下次再来和老师确定时间. 这个是悲观锁.

(2)乐观锁

1.概念:预测锁竞争的概率小,导致做的工作少、效率高;(认为别人不会来修改数据所以并不会真正加锁,直到真正提交更新的数据之后,才会加锁进行数据冲突检测<主要功能>——通过引入“版本号”检测,若并未发现冲突直接返回用户态)

2.应用场景:若真实的锁冲突比较小,使用乐观锁会比较合适。

例如——同学 B 认为 "老师是比较闲的(认为锁冲突小), 我来问问题, 老师大概率是有空解答的". 因此同学 B 直接就来找老师.(没加锁, 直接访问资源) 如果老师确实比较闲, 那么直接问题就解决了. 如果老师这会确实很忙, 那么同学 B 也不会打扰老师(已经访问到冲突), 就下次再来(虽然没加锁, 但是能识别出数据访问冲突). 这个是乐观锁.

注意:synchronized初始使用乐观锁,当发现锁竞争比较频繁时,会自动切换成悲观锁策略。

例如——同学 C 开始认为 "老师比较闲的", 问问题都会直接去找老师。但是直接来找两次老师之后, 发现老师都挺忙的, 于是下次再来问问题, 就先发个消息问问老师忙不忙, 再决定是否来问问题.

三、读写锁&互斥锁

(1)读写锁

1.概念

一个线程对数据的访问主要存在两种操作——读数据和写数据。读写锁就是把读操作和写操作区分对待,Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁. 
ReentrantReadWriteLock.ReadLock 类表示一个读锁. 这个对象提供了 lock / unlock 方法进行
加锁解锁。
ReentrantReadWriteLock.WriteLock 类表示一个写锁. 这个对象也提供了 lock / unlock 方法进
行加锁解锁.。

2.实现方法

进行读写锁,通常分为三个步骤:①给读加锁 ②给写加锁 ③解锁;

3.产生的结果

读写锁的引入即会线程安全也会产生线程不安全问题

①线程安全——读锁和读锁之间,无锁竞争(不互斥,多个线程同时读同一个变量,不会阻塞等待)

②线程不安全——读锁和写锁、写锁和写锁,有锁竞争(互斥,要挂起阻塞等待,虽然减慢了速度,但是保证了准确性)

4.应用场景

读写锁适用于”一写多读“(也就是”频繁读,不频繁写“)的场景中。

(2)互斥锁

1.概念

引入互斥锁为了确保在多个线程访问共享资源时的互斥性——当多个线程同时访问(读锁和写锁、写锁和写锁)共享资源时,可能会导致数据的竞争问题。为了避免这种问题,需要确保在任何时候只有一个线程能够访问共享资源(加锁),而其他线程需要等待直到资源可用

2.实现方法

初始化一个互斥锁

②加锁和解锁

加锁——线程在进行上锁时,其锁资源被其他线程持有,那么该线程则会执行阻塞等待,等待锁资源被解除之后,才可以进行加锁;若线程加锁成功,则可以访问共享资源,期间不会被打断,在访问结束之后解锁。

解锁——当一个线程完成对共享资源的访问时,它会释放互斥锁,以便其他线程可以获取互斥锁并访问共享资源。

④互斥锁并不能保证线程的执行先后,但可以保证对共享资源操作的完整性

注意:synchronized不是读写锁,而是互斥锁

四、轻量级锁&重量级锁

(1)轻量级锁

1.概念

随着其他线程进入,开始有锁竞争(竞争轻量),轻量级锁也是自适应的自旋锁

2.实现方法

轻量级锁就是通过 CAS 来实现. 
①通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
②如果更新成功, 则认为加锁成功
③如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU). 

自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源. 
因此此处(自适应的自旋锁)的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了——解释了”自适应“。

3.特点

①少量的内核态(时间成本不可控)、用户态(时间成本可控)切换

②不太容易引发线程调度

③过程更快更高效
 

(2)重量级锁

1.概念

多线程调度时锁竞争进一步激烈,轻量级锁就会变成重量级锁——线程自行放弃cpu资源,由内核态(时间成本不可控)进行后续调度;重量级锁是自适应的挂起等待锁。

2.实现方法

重量级锁就是通过内核态提供的mutex互斥锁实现 . 

①执行加锁操作前, 先进入内核态. 
②在内核态判定当前锁是否已经被占用
③如果该锁没有占用, 则加锁成功, 并切换回用户态. 
④如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起等待. 等待被操作系统唤醒. 
⑤经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒,这个线程, 尝试重新获取锁. 

 

3.特点

①大量的内核态(时间成本不可控)、用户态(时间成本可控)切换——导致成本很高

②容易引发线程调度

③过程更慢更低效

注意:synchronized开始是一个轻量级锁,如果锁冲突比较严重,就会变成重量级锁~

五、自旋锁&挂起等待锁

(1)自旋锁

1.概念

自旋锁是线程在抢锁失败后进入阻塞状态,没有放弃CPU,而是等待锁被释放。

2.实现方法

与轻量级锁相同,通过 CAS 来实现.(见上述) 

3.特点

①自旋锁是一种典型的 轻量级锁 的实现方式

②如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止,一旦锁被其他线程释放, 就能第一时间获取到锁。

③优点: 没有放弃 CPU, 不涉及线程阻塞和调度, 一旦锁被释放, 就能第一时间获取到锁.——纯用户态(速度快)。
④缺点: 如果锁被其他线程持有的时间比较久,自旋操作是一直让 CPU 空转, 那么就会持续的消耗 CPU 资源——“忙等”现象。

4.自旋锁的使用场景

①等待时间比较短的任务中;

②线程数量不太多的应用中;

(2)挂起等待锁

1.概念

当某个线程没有申请到锁的时候,此时该线程会被挂起,即加入到等待队列等待。当锁被释放的时候,就会被唤醒,重新竞争锁(放弃CPU,进入等待队列)

2.实现方法

和重量级锁相同,就是通过内核态提供的mutex互斥锁实现(见上述)

3.特点

①自旋锁是一种典型的重量级锁 的实现方式

②如果获取锁失败, 不会再尝试获取锁, 而是一直阻塞等待。
③优点: 放弃 CPU, 涉及线程阻塞和调度, 一旦锁被释放,此时CPU就会空出来可以做其他的工作 。
④缺点: 如果锁被其他线程持有的时间比较久,被唤醒的不及时的话,线程获取到锁的时机可能并不会很及时——内核态(速度慢)。

4.挂起等待锁的使用场景

①等待时间比较长的任务中;

②线程数量较多的应用中;

注意:synchronized开始是一个轻量级锁,如果锁冲突比较严重,就会变成重量级锁.而synchronized中的轻量级锁策略往往是通过自旋锁的方式实现的。

六、公平锁&非公平锁

(1)公平锁

1.概念

线程在阻塞队列中竞争锁时,一旦前面的持有锁线程将锁释放,阻塞队列中的线程竞争锁时遵循”先来后到“

举例:商店搞活动,活动开始所有人自觉排成队列参与活动~

(2)非公平锁

1.概念

线程在竞争锁时(无队列),一旦前面的持有锁线程将锁释放,线程竞争锁时不遵循”先来后到“(一起竞争)

举例:商店搞活动,活动一开始所有人蜂拥而至~

注意:自带的synchronized锁是非公平锁操作系统内部也默认实现的是非公平锁,要想实现公平锁,需要在synchronized的基础上加上队列,记录加锁线程的顺序。

七、可重入锁(不死锁)&不可重入锁(死锁)

(1)可重入锁——不死锁

1.概念

可重入锁也叫递归锁,是”可以重复进入的锁“,它允许同一个线程连续多次获取同一把锁。java中以Reentrant开头命名的锁、JDK提供的所有Lock实现类——都是可重入锁,而Linux系统提供的mutex是不可重入锁。

2.实现方式

在锁中记录该线程持有者的线程身份以及一个计数器(记录加锁次数),若果发现当前加锁的线程就是持有锁的线程,则直接计数自增。

(2)不可重入锁——死锁

1.概念

不可重入锁是”不可以重复进入的锁“,它不允许同一个线程连续多次获取同一把锁。当第一次线程A锁未释放,第二次线程A又要重新加锁的时候就要阻塞等待,显然此时线程A既不能获取到锁还必须一直阻塞等待,这就是所谓的”死锁“现象

2.实现方式

在锁中记录该线程持有者的线程身份以及一个计数器(记录加锁次数),若果发现当前加锁的线程就是持有锁的线程,且计数器的次数为2时,立刻停止,这就出现了死锁

问题:如何解决"死锁"现象

答:加锁的时候判定一下,看当前尝试申请锁的线程是不是已经就是锁的持有者了,如果是,直接解锁放行~

注意:synchronized是可重入锁。

八、死锁(详细理解)

1.死锁的四个必要条件<同时满足>

①互斥作用——一个线程拿到一把锁之后,另一个线程不能使用。(锁的基本特点,原子性,为锁特征)

②不可抢占——一个线程拿到锁,只能自己主动释放,不能被其他线程强行占有。(不可挖墙脚,为锁特征)

③请求和保持——当一个线程占据多把锁后,除非显式释放锁,否则锁一直被该线程锁占用(碗里的锅里的都得到,为代码特征)

④循环等待——多个线程等待关系闭环了(房间钥匙锁车里了,车钥匙锁房间里,为代码特征)

2.死锁的三种典型情况

①一个线程,一把锁,这把锁是可重入锁不影响,是不可重入锁会死锁。

②两个线程两把锁,即使是可重入锁,也会出现死锁。

③N个线程,M把锁,直接是死锁——”类比于哲学家就餐“。

哲学家就餐问题:假设有五个哲学家和五根筷子,随即进行吃面条(拿起两根筷子——加锁)和思考人生(放下筷子——解锁)两个行为;若想拿筷子但是发现筷子被别人占用了,就会阻塞等待(等待的同时不会放下手里已经占有到的筷子),若是五个哲学家同时拿起左手边的筷子,结果一个人都吃不到面条,这就是典型的”死锁“!

3.如何避免死锁

破坏循环等待——针对锁进行编号,如果需要同时获取到多把锁,约定加锁顺序(务必是先对小的编号加锁,后对大的编号加锁)

synchronized(locker1){
synchronized(locker2){
//........代码
}
}
//先对1号加锁,再对2号加锁,先释放1号的锁,2号的锁才能释放。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值