Java之线程篇六

目录

CAS 

CAS伪代码

CAS的应用

实现原子类 

实现自旋锁

CAS的ABA问题

ABA问题导致BUG的例子 

相关面试题

synchronized原理

synchronized特性 

加锁过程

相关面试题

Callable

相关面试题

JUC的常见类

ReentrantLock

ReentrantLock 和 synchronized 的区别:

原子类

信号量

相关面试题


CAS 

CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。
1. 比较 A 与 V 是否相等。(比较)
2. 如果比较相等,将 B 写入 V。(交换)
3. 返回操作是否成功。

CAS伪代码
下面写的代码不是原子的 , 真实的 CAS 是一个原子的硬件指令完成的 . 这个伪代码只是辅助理解
CAS 的工作流程。
boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}

CAS其实是一个cpu指令,单个的cpu指令,是原子的!!!

就可以使用CAS完成一些操作,进一步代替“加锁”;

基于CAS实现线程安全的方式也称为“无锁化编程”。

优点:保证线程安全,同时避免阻塞,影响效率;

缺点:代码会变复杂,不好理解;只能够适用特定场景,不如加锁方式更普遍。

CAS的应用
实现原子类 

标准库中提供了 java.util.concurrent.atomic 包, 里面的类都是基于这种方式来实现的. 
典型的就是 AtomicInteger 类. 其中的 getAndIncrement 相当于 i++ 操作.

伪代码实现

class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}
}
实现自旋锁

伪代码实现

public class SpinLock {private Thread owner = null;public void lock(){// 通过 CAS 看当前锁是否被某个线程持有. // 如果这个锁已经被别的线程持有, 那么就自旋等待. // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程. while(!CAS(this.owner, null, Thread.currentThread())){}}public void unlock (){this.owner = null;}
}
CAS的ABA问题

ABA问题即:

假设有两个线程t1和t2,有一个共享变量num,初值为A,接下来t1想使用CAS把num改为Z,那么就需要先读取num的值,记录到oldNum变量中,然后使用CAS判断当前num的值是否为A,如果为A,则改为Z。

但是在t1执行上述操作之前,t2线程可能把num的值从A改为B,又从B改为A。

那么此时,线程t1无法区分当前这个变量始终是A,还是经历了一个变化过程。

ABA问题导致BUG的例子 

假设你要去ATM取款机取钱,余额有1000,要取款500,但是取款的时候ATM机卡了一下,所以你按了两下,假设ATM取款机按CAS方式工作,虽然你按了两次,但是你取出的是500,不过,如果恰巧这个时候有人给你转了500,这个时候,你按的第2下取款,使用CAS方式会发现余额还是1000,那么此时就会余额没有改变,你最后会取出1000.

解决方法:引入版本号等方式

给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期. 
CAS 操作在读取旧值的同时, 也要读取版本号. 
真正修改的时候, 
如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

相关面试题
1) 讲解下你自己理解的 CAS 机制
全称 Compare and swap, 即 "比较并交换". 相当于通过一个原子的操作, 同时完成 "读取内存, 比较是否相等, 修改内存" 这三个步骤. 本质上需要 CPU 指令的支撑

2) ABA问题怎么解决?

给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期. 
如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版本号比之前读到的版本号大, 就认为操作失败.

synchronized原理
synchronized特性 

1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁. 
3. 实现轻量级锁的时候大概率用到的自旋锁策略
4. 是一种不公平锁
5. 是一种可重入锁
6. 不是读写锁

加锁过程

JVM 将 synchronized 锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。会根据情况,进行依次升级。 

1)偏向锁

第一个尝试加锁的线程, 优先进入偏向锁状态. 
偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程. 
如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)
如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态. 
偏向锁本质上相当于 "延迟加锁" . 能不加锁就不加锁, 尽量来避免不必要的加锁开销. 
但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.

2)轻量级锁

随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁). 
此处的轻量级锁就是通过 CAS 来实现.

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

3)重量级锁

如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁
此处的重量级锁就是指用到内核提供的 mutex . 
执行加锁操作, 先进入内核态. 
在内核态判定当前锁是否已经被占用
如果该锁没有占用, 则加锁成功, 并切换回用户态. 
如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒. 
经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒这个线程, 尝试重新获取锁. 

锁消除

编译器+JVM 判断锁是否可消除. 如果可以, 就直接消除。

例如:单线程环境下。 

锁粗化

一段逻辑中如果出现多次加锁解锁, 编译器 + JVM 会自动进行锁的粗化.  

相关面试题

1)什么是偏向锁?

偏向锁不是真的加锁 , 而只是在锁的对象头中记录一个标记 ( 记录该锁所属的线程 ). 如果没有其他线程参与竞争锁, 那么就不会真正执行加锁操作 , 从而降低程序开销 . 一旦真的涉及到其他的线程竞争, 再取消偏向锁状态 , 进入轻量级锁状态 .
Callable

Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务, 
Runnable 描述的是不带返回值的任务. 
Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为
Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定. 
FutureTask 就可以负责这个等待结果出来的工作 

理解FutureTask

想象去吃麻辣烫 . 当餐点好后 , 后厨就开始做了 . 同时前台会给你一张 " 小票 " . 这个小票就是
FutureTask. 后面我们可以随时凭这张小票去查看自己的这份麻辣烫做出来了没 .
代码示例
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;public class Demo30 {public static void main(String[] args) throws ExecutionException, InterruptedException {// 定义了任务.Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 1000; i++) {sum += i;}return sum;}};// 把任务放到线程中进行执行.FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread t = new Thread(futureTask);t.start();// 此处的 get 就能获取到 callable 里面的返回结果.// 由于线程是并发执行的. 执行到主线程的 get 的时候, t 线程可能还没执行完.// 没执行完的话, get 就会阻塞.System.out.println(futureTask.get());}
}
相关面试题

介绍一下Callable是什么

Callable 是一个 interface . 相当于把线程封装了一个 "返回值". 方便程序猿借助多线程的方式计算结果. 
Callable 和 Runnable 相对, 都是描述一个 "任务". Callable 描述的是带有返回值的任务, 
Runnable 描述的是不带返回值的任务.
Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为
Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定. 
FutureTask 就可以负责这个等待结果出来的工作.

JUC的常见类

JUC,是java.util.concurrent的缩写。

ReentrantLock

可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全. 
ReentrantLock 也是可重入锁. "Reentrant" 这个单词的原意就是 "可重入".

ReentrantLock的用法

lock(): 加锁, 如果获取不到锁就死等. 
trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁. 
unlock(): 解锁

ReentrantLock 和 synchronized 的区别:

synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准库的一个类, 在 JVM 外实现的(基于 Java 实现). 
synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock. 
synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃. 
synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式. 

更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.

// ReentrantLock 的构造方法
public ReentrantLock(boolean fair) {sync = fair ? new FairSync() : new NonfairSync();
}

 如何选择使用哪个锁?

锁竞争不激烈的时候, 使用 synchronized, 效率更高, 自动释放更方便. 
锁竞争激烈的时候, 使用 ReentrantLock, 搭配 trylock 更灵活控制加锁的行为, 而不是死等. 
如果需要使用公平锁, 使用 ReentrantLock.

原子类

原子类内部用的是 CAS 实现,所以性能要比加锁实现 i++ 高很多。原子类有以下几个
AtomicBoolean
AtomicInteger
AtomicIntegerArray
AtomicLong
AtomicReference
AtomicStampedReference 

ExecutorService和Executors

ExecutorService 表示一个线程池实例. 
Executors 是一个工厂类, 能够创建出几种不同风格的线程池. 
ExecutorService 的 submit 方法能够向线程池中提交若干个任务. 

代码示例

ExecutorService pool = Executors.newFixedThreadPool(10);
pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("hello");}
});

Executors 创建线程池的几种方式:

newFixedThreadPool: 创建固定线程数的线程池
newCachedThreadPool: 创建线程数目动态增长的线程池.
newSingleThreadExecutor: 创建只包含单个线程的线程池. 
newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer. 

Executors 本质上是 ThreadPoolExecutor 类的封装. 

信号量

信号量(Semaphore), 用来表示 "可用资源的个数". 本质上就是一个计数器.
Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用. 

acquire 方法表示申请资源 (P 操作 ), release 方法表示释放资源 (V 操作 )

 代码示例

import java.util.concurrent.Semaphore;public class Demo24 {public static void main(String[] args) throws InterruptedException {Semaphore semaphore = new Semaphore(4);semaphore.acquire();System.out.println("P 操作");semaphore.acquire();System.out.println("P 操作");semaphore.acquire();System.out.println("P 操作");semaphore.acquire();System.out.println("P 操作");semaphore.acquire();System.out.println("P 操作");}
}

运行结果

相关面试题

1) 线程同步的方式有哪些?

synchronized, ReentrantLock, Semaphore 等都可以用于线程同步.

2) 为什么有了 synchronized 还需要 juc 下的 lock?

以 juc 的 ReentrantLock 为例, 
synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更
灵活,
synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时
间就放弃. 
synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个
true 开启公平锁模式. 
synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的
线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线
程. 

3) AtomicInteger 的实现原理是什么?

基于 CAS 机制. 伪代码如下:  

class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while ( CAS(value, oldValue, oldValue+1) != true) {oldValue = value;}return oldValue;}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/54532.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

缓存穿透 问题(缓存空对象)

文章目录 1、缓存穿透2、缓存空对象3、AlbumInfoApiController --》getAlbumInfo()4、AlbumInfoServiceImpl --》getAlbumInfo()5、RedisConstant6、请求缓存不存在的数据 1、缓存穿透 缓存穿透带有恶意性&#xff0c;强调不存在的数据。 2、缓存空对象 3、AlbumInfoApiCont…

华为OD机试 - 需要打开多少监控器(Java 2024 E卷 100分)

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试&#xff08;JAVA&#xff09;真题&#xff08;E卷D卷A卷B卷C卷&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;私信哪吒&#xff0c;备注华为OD&#xff0c;加…

软件测试 BUG 篇

目录 一、软件测试的生命周期 二、BUG 1. bug的概念 2. 描述bug的要素 3. bug的级别 4. bug的生命周期 5. 与开发产生争执怎么办&#xff1f;&#xff08;面试高频考题&#xff09; 5.1 先检查自身&#xff0c;是否bug描述不清楚 5.2 站在用户角度考虑并抛出问题 5.3 …

分享两个虚拟试衣工具,一个在线,一个离线,还有ComfyUI插件

SAM &#xff0c;对不住了&#xff01; 我没记错的话&#xff0c;OpenAI CEO&#xff0c;性别男&#xff0c;取向男&#xff0c;配偶男。 这又让我联想到了苹果CEO库克... 所以OpenAI和Apple可以一啪即合。 钢铁直男老马就和他们都不对付~~ 开个玩笑&#xff0c;聊…

C++:多态(协变,override,final,纯虚函数抽象类,原理)

目录 编译时多态 函数重载 模板 运行时多态 多态的实现 实现多态的条件 协变 析构函数的重写 override 关键字 final 关键字 重载、重写、隐藏对比 纯虚函数和抽象类 多态的原理 多态是什么&#xff1f; 多态就是有多种形态 多态有两种&#xff0c;分别是编译时…

Linux驱动开发 ——架构体系

只读存储器&#xff08;ROM&#xff09; 1.作用 这是一种非易失性存储器&#xff0c;用于永久存储数据和程序。与随机存取存储器&#xff08;RAM&#xff09;不同&#xff0c;ROM中的数据在断电后不会丢失&#xff0c;通常用于存储固件和系统启动程序。它的内容在制造时或通过…

Java基础面试题——异常

目录 关系图 1. Throwable和Exception之间的关系 2.异常分为哪两大类 3.常见的 RuntimeException 4. 常见的 Error 5.什么是已检查异常和未检查异常&#xff1f;它们的区别是什么&#xff1f; 6.Java 中如何自定义异常&#xff1f; 7.throw 和 throws 的区别是什么&…

GlusterFS 分布式文件系统

一、GlusterFS 概述 1.1 什么是GlusterFS GlusterFS 是一个开源的分布式文件系统&#xff0c;它可以将多个存储服务器结合在一起&#xff0c;创建一个大的存储池&#xff0c;供客户端使用。它不需要单独的元数据服务器&#xff0c;这样可以提高系统的性能和可靠性。由于没有…

视频转文字工具:开启视频内容深度挖掘的钥匙

图片里到文字要提取出来&#xff0c;现在有很多的工具&#xff0c;但是视频里的文字要提取出来&#xff0c;是不是就不那么好操作呢&#xff1f;并不是的&#xff0c;现在也有不少支持视频转文字的工具&#xff0c;这次我们就来介绍一些可以提高我们视频文字提取效率的工具吧。…

PostgreSQL(PG)(二十二)

&#x1f33b;&#x1f33b; 目录 &#x1f33b;&#x1f33b; 一、PostgreSQL 简介1.1、PG 的历史1.2、PG的社区1.2.1 纯社区1.2.2 完善的组织结构1.2.3 开源许可独特性 1.3 、PostgreSQL与MySQL的比较 二、PostgresQL的下载安装2.1、Windows上安装 PostgreSQL2.2、远程 连接 …

RK3568部署DOCKER启动服务器失败解决办法

按照上文的方法部署完DOCKER之后&#xff0c;启动服务异常&#xff0c;查阅网络相关资源&#xff0c;解决方案如下&#xff1a; 修改/源码/kernel/arch/arm64/configs/OK3568-C-linux_defconfig&#xff0c;在最后添加 CONFIG_MEMCGy CONFIG_VETHy CONFIG_BRIDGEy CONFIG_BRID…

GS-SLAM论文阅读笔记--TAMBRIDGE

前言 本文提出了一个自己的分类方法&#xff0c;传统的视觉SLAM通常使用以帧为中心的跟踪方法&#xff0c;但是3DGS作为一种高效的地图表达方法好像更侧重于地图的创建。这两种方法都有各自的优缺点&#xff0c;但是如果能取长补短&#xff0c;互相结合&#xff0c;那么就会是…

6.7泊松噪声

基础概念 在OpenCV联合C中给一张图片添加泊松噪声&#xff08;Poisson Noise&#xff09;可以通过生成随机数并在图像的每个像素上加上这些随机数来实现。泊松噪声是一种统计分布服从泊松分布的噪声&#xff0c;通常用于模拟光子计数等场景。 使用泊松噪声的场景 泊松噪声通…

【解决】chrome 谷歌浏览器,鼠标点击任何区域都是 Input 输入框的状态,能看到输入的光标

chrome 谷歌浏览器&#xff0c;鼠标点击任何区域都是 Input 输入框的状态&#xff0c;能看到输入的光标 今天打开电脑的时候&#xff0c;网页中任何文本的地方&#xff0c;只要鼠标点击&#xff0c;就会出现一个输入的光标&#xff0c;无论在哪个站点哪个页面都是如此。 我知道…

CQRS模型解析

简介 CQRS中文意思为命令于查询职责分离&#xff0c;我们可以将其了解成读写分离的思想。分为两个部分 业务侧和数据侧&#xff0c;业务侧主要执行的就是数据的写操作&#xff0c;而数据侧主要执行的就是数据的读操作。当然两侧的数据库可以是不同的。目前最为常用的CQRS思想方…

C++调用C# DLL之踩坑记录

C是非托管代码&#xff0c;C#则是托管代码&#xff0c;无法直接调用 CLR的介绍见CLR简介 MSDN提到了两种非托管-托管的交互技术&#xff1a;CLR Interop和COM Interop 后者要将C# 类库注册为COM组件&#xff0c;本文只探讨CLR&#xff0c;要通过C CLR写中间层代码 方式一&…

获取参数

获取querystring参数 querystring 指的是URL中 ? 后面携带的参数&#xff0c;例如&#xff1a;http://127.0.0.1:9090/web?query杨超越。 获取请求的querystring参数的方法如下&#xff1a; 方法1&#xff1a; Query package main// querystringimport ("github.com/…

引领长期投资新篇章:价值增长与财务安全的双重保障

随着全球金融市场的不断演变&#xff0c;长期投资策略因其稳健性和对价值增长的显著推动作用而日益受到投资者的重视。在这一背景下&#xff0c;Zeal Digital Shares&#xff08;ZDS&#xff09;项目以其创新的数字股票产品&#xff0c;为全球投资者提供了一个全新的长期投资平…

最优化理论与自动驾驶(十一):基于iLQR的自动驾驶轨迹跟踪算法(c++和python版本)

最优化理论与自动驾驶&#xff08;四&#xff09;&#xff1a;iLQR原理、公式及代码演示 之前的章节我们介绍过&#xff0c;iLQR&#xff08;迭代线性二次调节器&#xff09;是一种用于求解非线性系统最优控制最优控制最优控制和规划问题的算法。本章节介绍采用iLQR算法对设定…

分析redis实现分布式锁的思路

文章目录 1、基于redis实现分布式锁&#xff1a;利用key的唯一性1.1、独占排他1.2、死锁问题1.2.1、redis客户端程序获取了锁之后&#xff0c;服务器立马宕机&#xff0c;就会导致死锁。1.2.2、不可重入&#xff1a;可重入 1.3、原子性&#xff1a;加锁和过期之间&#xff1a;s…