马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?立即注册
x
目录
前言
一、常见的锁计谋
1.1 乐观锁 vs 悲观锁
1.2 普通的互斥锁 vs 读写锁
1.3 重量级锁 vs 轻量级锁
1.4 自旋锁 vs 挂起等待锁
1.5 公平锁 vs 非公平锁
1.6 可重入锁 vs 不可重入锁
二、CAS
2.1 CAS典范应用场景
2.1.1 使用CAS实现原子类
2.1.2 使用CAS实现自旋锁
2.2 CAS中的ABA标题(小概率bug)
2.2.1 什么是ABA标题
2.2.2 ABA标题引发的bug
2.2.3 解决ABA标题的办法
前言
前面所介绍的 多线程基础篇的内容,主要介绍的还是一些 和多线程相干性非常高的内容,也都是工作中经常会涉及到的标题和代码~
而接下来的多线程进阶篇的内容,则是需要对多线程内容进行进一步的补充~
进阶篇中的很多知识,不再是工作中常用的,但是却是在面试中常考的(俗称:八股文,面试造核弹,工作拧螺丝)~
下面,就正式开始来学习 进阶篇的内容 ......
一、常见的锁计谋
简朴通俗的来说,锁计谋就是 加锁的时候是咋加的~(锁计谋也是一个锁的形容词)
1.1 乐观锁 vs 悲观锁
乐观锁:预测接下来锁辩说的概率不大,就会少做一点工作,成本更小~
悲观锁:预测接下来锁辩说的概率很大,就会多做一点工作,成本更大~
好比说,就前段时间,西安那边又有确诊的了~
有居民就比较紧张,就在想是不是要在家里屯点菜啥的(疫情会引起封城,封城会影响买菜),提前屯点菜以备不时之需~
这个就可以看作是 悲观锁,耗费所需成本较大(买菜、运菜、放在地上......)~
其时有居民却以为,由于之前已经有过反复确诊的履历,所以说 已经有了不少履历了,所以封城的概率比较小,不需要提前屯菜(屯了吃不完大概率会坏)~
这个就可以看作是 乐观锁,话费所需成本更小~
synchronized 就既是一个悲观锁,也是一个乐观锁,准确的来说 它是一个自顺应锁~
如果当前锁辩说概率不大,就以乐观锁的方式运行,每每是纯用户态实行的~
一旦发现锁辩说概率大了,就以悲观锁的方式运行,每每要进入内核,对当前线程进行挂起等待~
1.2 普通的互斥锁 vs 读写锁
synchronized 就属于普通的互斥锁,两个加锁操作之间会发生竞争~
读写锁,把加锁操作细化了 "加读锁" "加写锁" ~
情况一:
线程A 尝试加写锁,线程B 尝试加写锁,此时 A和B 产生竞争,和普通的锁没有区别~
情况二:
线程A 尝试加读锁,线程B 尝试加读锁,此时 A和B 不产生竞争,和没有加锁一样(多线程读,不涉及修改,是线程安全的)~
这种情况是相当普遍的~
情况三:
线程A 尝试加读锁,线程B 尝试加写锁,此时 A和B 产生竞争,和普通的锁没有区别~
1.3 重量级锁 vs 轻量级锁
重量级锁:锁开销比较大,做的工作比较多~
轻量级锁:锁开销比较小,做的工作比较小~
重量级锁、轻量级锁 与之前所介绍的 乐观锁、悲观锁 差不多(内容上不是完全的区分开),但是最终的着力点还是不一样的~
此中,在大部门情况下(不绝对),悲观锁 经常会是重量级锁,乐观锁 经常会是轻量级锁~
重量级锁 主要依赖了操作体系提供的锁,使用这种锁,容易产生壅闭等待~
轻量级锁 主要尽量的制止使用操作体系提供的锁,尽量在用户态完成功能,尽量制止用户态和内核态的切换,尽量制止挂起等待~
同时,synchronized 是一个自顺应锁,既是轻量级锁,也是重量级锁~
锁辩说不高:轻量级
锁辩说很高:重量级
1.4 自旋锁 vs 挂起等待锁
自旋锁 是轻量级锁的详细实现,挂起等待锁 是重量级锁的详细实现~
自旋锁:当发生锁辩说的时候,不会挂起等待,会迅速来尝试看这个锁能不能获取到(更轻量,乐观锁)~
特点:
- 一旦锁被开释,就可以第一时间获取到
- 如果锁一直不开释,就会斲丧大量的资源
可以看作是一个 不断的循环,可以用一个伪代码来表现:
- //自旋锁伪代码,不停的循环
- while(抢锁(lock) == 失败) {
-
- }
复制代码 挂起等待锁:发现锁辩说,就挂起等待(更重量,悲观锁)~
特点:
- 一旦锁被开释,不能第一时间获取到
- 在锁被其他线程占用的时候,会放弃CPU资源
synchronized 作为轻量级锁的时候,内部是 自旋锁;作为重量级锁的时候,内部是 挂起等待锁~
1.5 公平锁 vs 非公平锁
啥样的情况才算是公平?
一般以为,符合 "先到先得" 如许的规则,就是公平!!!
公平锁:多个线程等待一把锁的时候,谁先来尝试拿着一把锁,这把锁就是谁的~
非公平锁:多个线程等待一把锁的时候,就和哪个线程先来后到没有关系,每个线程拿到锁的概率是均等的~
synchronized 黑白公平锁~
1.6 可重入锁 vs 不可重入锁
一个线程连续加锁两次,不会造成死锁,那么这个锁就叫做 可重入锁~
一个线程连续加锁两次,会造成死锁,那么这个锁就叫做 不可重入锁
- private static void func() {
- //......进行一些多线程操作
- //第一次加锁
- synchronized (Demo27.class) {
- //第二次加锁
- synchronized (Demo27.class) {
-
- }
- }
- }
复制代码 如上述代码,第一次加锁能够成功,Demo27.class 处于被加锁的状态;但是 第二次加锁,由于 Demo27.class 已经是被加锁的状态了,所以就会出现出 壅闭状态~
要等待第一次加锁开释掉,第二次加锁才能够成功;但是 要想第一次加锁开释,那么 又必须要到第二次加锁成功之后,代码往下实行 ......
如许就构成了一个死循环,就叫做 死锁!!!
synchronized 属于可重入锁~
二、CAS
CAS 是操作体系硬件 给JVM提供的别的一种更轻量的原子操作的机制~
准确来说,CAS是CPU提供的一条特别的指令 —— compare and swap(比较和交换)~
CAS 是一个原子指令~
比较:是比较内存和寄存器的值~
如果相等,则把寄存器和另一个值进行交换;如果不相等,就不进行操作~
- //CAS 的伪代码来理解它的工作流程
- //其中,address表示内存地址,expextValue表示一个寄存器中 用来比较的值,
- //expextValue表示另一个寄存器中 用来交换的值
- boolean CAS(address,expextValue,swapValue) {
- if(&address == expextValue) {
- &address = swapValue;
- return true;
- }
- return false;
- }
- //上面一系列操作都是由一个CPU指令来完成的
复制代码 2.1 CAS典范应用场景
2.1.1 使用CAS实现原子类
原子类:这是标准库中提供的一组类,可以让原子的进行 ++、-- 等运算~
- package thread;
-
- public class Demo28 {
- public static int count = 0;
-
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() -> {
- for (int i = 0; i < 50000; i++) {
- count++;
- }
- });
- Thread t2 = new Thread(() -> {
- for (int i = 0; i < 50000; i++) {
- count++;
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println("count = " + count);
- }
- }
复制代码 在之前,我们已经介绍过,最终的效果不是 10_0000 ~
运行效果:

我们可以使用加锁来解决这个标题,也可以使用原子类来解决这个标题:
- package thread;
-
- import java.util.concurrent.atomic.AtomicInteger;
-
- public class Demo28 {
- //public static int count = 0;
- public static AtomicInteger count = new AtomicInteger(0);
-
- public static void main(String[] args) throws InterruptedException {
- Thread t1 = new Thread(() -> {
- for (int i = 0; i < 50000; i++) {
- //count++;
- //这个方法相当于count++
- count.getAndIncrement();
- }
- });
- Thread t2 = new Thread(() -> {
- for (int i = 0; i < 50000; i++) {
- //count++;
- count.getAndIncrement();
- }
- });
- t1.start();
- t2.start();
- t1.join();
- t2.join();
- System.out.println("count = " + count);
- }
- }
- //和之前的不同的代码已注释,这是使用 原子类来解决问题的,没有使用加锁操作,也实现了线程安全
复制代码 运行效果:
在Java标准库 里面提供了基于CAS所实现的 "原子类",是线程安全的~
这些 "原子类" 通常以 Atomic 开头,对常用的 int、long等等 进行了封装,如:
2.1.2 使用CAS实现自旋锁
- //自旋锁伪代码
- public class SpinLock {
- private Thread owner = null;
- public void lock() {
- //当前的owner是否为空,为空即为当前没有加锁,于是就进行交换,
- //把当前要给加锁的线程的值赋予owner
- //非空就不去进行交换,就循环继续进行,呈现自旋的状态
- while(!CAS(this.owner,null,Thread.currentThread())) {
- }
- }
- public void unlock(){
- this.owner = null;
- }
- }
复制代码 当 owner 为 null 的时候 CAS 才能成功,循环才能结束~
当 owner 为非null,这阐明当前的锁已经被其他线程给占用了,因此 就需要继续循环(自旋)~
2.2 CAS中的ABA标题(小概率bug)
2.2.1 什么是ABA标题
ABA标题可以单纯的如许理解:如果你去买一个手机,那么你无法区分 它是一个新机,还是一个翻新机(二手的、外貌包装和新机一样)~
雷同的,在CAS里面,也无法区分,数据始终就是A;还是数据从 A 变成 B,之后又变回了 A ~
如果是前者,那么一点标题都没有;但是如果是后者,那么 CAS 就会有肯定的概率引发 bug(极端情况下的小概率变乱) ~
2.2.2 ABA标题引发的bug
这里结合一个详细的例子,来介绍ABA标题引发的bug~
假设风趣老铁有 1000 存款,此时想要从 ATM机 上取走 500(ATM机 按照CAS的方式来进行操作)~
取钱的时候,按下取款按钮,就会触发一个 "取钱的线程",但是 风趣老铁手一滑,连续按了两下(即 产生了两个线程)~
符合预期的方式(即使手滑了多点了两次,仍旧只取走了500):
但是,怕就怕在这期间 突然又来了一个线程(好比说 风趣老铁的一个朋友,此时恰好向风趣老铁转了500)~
这时候,就扣除了两次500了,这个就是典范的ABA标题(极端情况下的小概率标题)~
此时,线程2不知道 当前的1000,始终是1000;还是 1000 -> 500 -> 1000 ~
2.2.3 解决ABA标题的办法
端庄的解决ABA标题的办法,是想办法获取到中心过程 —— 引入一个 "版本号" 来解决~
在上述的例子当中,CAS是比较的是 余额,余额雷同,就可以进行修改(余额是可以变大和变小,所以就会出现ABA标题)~
但是,如果换成 "版本号",并且规定 "版本号" 只能增不能减,那么就不会出现ABA标题 ~
当然,解决ABA标题的办法肯定不止这一种,这里只是列举了一种非常典范的办法 ~
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |