个人博客网站html模板,wordpress 问答 api,网站建设费用一年,wordpress域名根管提示#xff1a;文章写完后#xff0c;目录可以自动生成#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、常见的锁策略 1.1 乐观锁 vs 悲观锁 1.2 普通的互斥锁 vs 读写锁 1.3 重量级锁 vs 轻量级锁 1.4 自旋锁 vs 挂起等待锁 1.5 公平… 提示文章写完后目录可以自动生成如何生成可参考右边的帮助文档 文章目录 前言一、常见的锁策略 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;//这个方法相当于countcount.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())) {}}
} 当 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机 上取走 500ATM机 按照CAS的方式来进行操作 取钱的时候按下取款按钮就会触发一个 取钱的线程但是 滑稽老铁手一滑连续按了两下即 产生了两个线程 但是怕就怕在这期间 突然又来了一个线程比如说 滑稽老铁的一个朋友此时正好向滑稽老铁转了500 这时候就扣除了两次钱了这个就是典型的ABA问题极端情况下的小概率问题; 此时线程2不知道 当前的1000始终是1000还是 1000 - 500 - 1000 ; 2.2.3 解决ABA问题的办法
正经的解决ABA问题的办法是想办法获取到中间过程 —— 引入一个 版本号 来解决 ;
在上述的例子当中CAS是比较的是 余额余额相同就可以进行修改余额是可以变大和变小所以就会出现ABA问题
但是如果换成 版本号并且规定 版本号 只能增不能减那么就不会出现ABA问题; 当然解决ABA问题的办法肯定不止这一种这里只是列举了一种非常典型的办法 ; 总结
好了这篇博客到这里就已经结束了
本篇博客主要介绍的是 各种常见的锁策略以及CAS、CAS中的小概率bug —— ABA问题并且介绍了ABA问题的解决方案