【Java多线程进阶】JUC常见类以及CAS机制
1. Callable的用法
之前已经接触过了Runnable接口,即我们可以使用实现Runnable接口的方式创建一个线程,而Callable也是一个interface,我们也可以用Callable来创建一个线程。
- Callable是一个带有泛型的interface
- 实现Callable接口必须重写call方法
- call方法带有返回值,且返回值类型就是泛型类型
- 可以借助FutureTask类接收返回值,更方便的帮助程序员完成计算任务并获取结果
下面我们来举一个案例,要求分别使用实现Runnable
接口和Callable
接口完成计算1+2+3+…1000并在主线程中打印结果的任务,体会区别。
1.1 使用Runnable接口
public class ThreadDemo01 {private static int result = 0;public static void main(String[] args) throws InterruptedException {Thread t = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 1; i <= 1000; i++) {result += i;}}});// 启动线程t.start();t.join();// 打印结果System.out.println("result: " + result); // 500500}
}
上述代码实现Runnable接口来启动一个线程,这段代码可以实现累加1-1000的任务,但是不够优雅!因为需要额外借助一个成员变量result来保存结果,当业务量繁多时,如果有多个线程完成各自的计算任务,那么就需要更多的成员变量保存结果,因此现在想想带有返回值的线程也许是有必要的!下面我们来看看如何使用Callable接口完成
1.2 使用Callable接口
public class ThreadDemo02 {public static void main(String[] args) throws ExecutionException, InterruptedException {Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int result = 0;for (int i = 1; i <= 1000; i++) {result += i;}return result;}};// 创建FutureTask实例FutureTask<Integer> futureTask = new FutureTask<>(callable);// 创建线程并启动Thread t = new Thread(futureTask);t.start();// 阻塞并获取返回结果int result = futureTask.get();System.out.println("result: " + result); // 500500}
}
上述代码中我们使用了实现Callable的方式完成任务,值得一提的是,Callable实现类对象不能直接作为Thread构造方法的参数,我们需要借助一个中间人即 FutureTask 类,该类实现了Runnable接口,因此可以直接作为Thread构造方法的参数。
注:futureTask.get()方法带有阻塞功能,直到子线程完成返回结果才会继续运行
1.3 如何理解FutureTask和Callable
理解Callable:1、Callable与Runnable是相对的,都是描述一个任务,Callable描述带有返回值的任务,Runnable描述不带返回值的任务。2、Callable往往需要搭配FutureTask一起使用,FutureTask用于保存Callable的返回结果,因为Callable中的任务往往在另一个线程执行,具体什么时候执行并不确定
理解FutureTask:FutureTask顾名思义就是未来任务,即Callable中的任务是不确定何时执行完毕的,我们可以形象描述为去杨国福吃麻辣烫时等待叫号,但是什么时候叫号是不确定的,通常点餐完毕后服务员会给一个取号凭证,我们可以凭借这个取号凭证查看自己的麻辣烫有没有做好。
1.4 相关面试题
- 介绍下Callable是什么?
2. 信号量Semaphore
相信大学期间修过操作系统课的小伙伴对semaphore
这个单词并不陌生,这不就是OS的信号量机制可以实现PV操作的么?而JVM对OS提供的semaphore又进行了封装形成了Java标准库中的Semaphore
类
semaphore:信号量,用来表示可用资源的数目,本质上是一个计数器。
我们可以拿停车场的展示牌进行举例,当前留有空闲车位100个时,展示牌上就会显示100,表示可用资源的数目,当有汽车开进停车位时,相当于申请了一个可用资源,此时展示牌数目就会-1(称为信号量的P操作),当有汽车驶出停车位,相当于释放了一个可用资源,此时展示牌数目+1(称为信号量V操作)
Semaphore的信号量PV操作都是原子性的,可以直接在多线程环境中使用。
锁其实本质上就是特殊的信号量,加锁可以看作时信号量为0,解锁可以看作信号量为1,所以也有说法称"锁"其实是一个二元信号量,那么既然"锁机制"能够实现线程安全,信号量也可以用来保证线程安全!
2.1 Semaphore代码案例
Semaphore相关API:
- acquire:相当于P操作,申请一个可用资源
- release:相当于V操作,释放一个可用资源
代码举例:
- 创建Semaphore实例并初始化为4,表示有4个可用资源
- 创建20个线程,每个线程都尝试申请资源,sleep1秒后释放资源,观察程序运行状况
public class SemaphoreExample {public static void main(String[] args) {// 初始化信号量为4Semaphore semaphore = new Semaphore(4);// 创建20个线程for (int i = 0; i < 20; i++) {int id = i;Thread t = new Thread(() -> {try {semaphore.acquire(); // 申请资源System.out.println("线程" + id + "申请到了资源");Thread.sleep(1000); // 睡眠1ssemaphore.release(); // 释放资源System.out.println("线程" + id + "释放资源");} catch (InterruptedException e) {throw new RuntimeException(e);}});// 启动线程t.start();}}
}
执行结果:
其中可以发现当前四个线程申请资源后,此时第五个线程尝试申请资源后,信号量变为-1就会阻塞等待,直到其他线程释放资源后才可以继续申请!
3. CountDownLatch的使用
CountDownLatch:可以等待N个任务全部完成。类似于N个人进行跑步比赛,直到最后一个人跃过终点才会宣布比赛结束,公布最后成绩。
3.1 CountDownLatch相关API
CountDownLatch提供API:
- new CountDownLatch(int n):构造方法,初始化n表示有n个任务需要完成
- countDown():任务执行完毕后调用,内部计数器进行自减
- await():阻塞等待所有任务全部完成后继续执行,相当于等待内部计数器为0
3.2 CountDownLatch代码案例
CountDownLatch代码案例:
- 创建10个线程,并初始化CountDownLatch为10
- 每个线程随机休眠1-5秒,模拟比赛结束
- 主线程中使用await阻塞等待全部执行完毕
public class CountDownLatchExample {public static void main(String[] args) throws InterruptedException {Random random = new Random();// 初始化CountDownLatch为10CountDownLatch latch = new CountDownLatch(10);// 创建10个线程for (int i = 0; i < 10; i++) {int curId = i;Thread t = new Thread(() -> {System.out.println("线程" + curId + "开始执行...");try {Thread.sleep(random.nextInt(6) * 1000);System.out.println("线程" + curId + "结束执行...");latch.countDown(); // 计数器自减} catch (InterruptedException e) {throw new RuntimeException(e);}});// 启动线程t.start();}// 阻塞等待全部执行完毕latch.await();System.out.println("全部任务执行完毕!");}
}
执行结果:
此时我们可以看到调用countDownLatch.await()
方法时只有当所有的线程都执行完毕,即调用10次countDown
方法后(即内部计数器为0)时才会停止阻塞!
4. ReentrantLock类
ReentrantLock:位于java.util.concurrent
包下,被称为可重入互斥锁,和synchronized定位类似,都是用来实现互斥效果的可重入锁,保证线程安全。
4.1 ReentrantLock的用法
ReentrantLock相关API:
- lock():尝试加锁,如果加锁失败就阻塞等待
- tryLock(超时时间):尝试加锁,获取不到就阻塞等待一定时间,超时则放弃加锁
- unlock():解锁
注意:由于ReentrantLock需要手动释放锁,因此常常把unlock
方法放在finally
块中
ReentrantLock lock = new ReentrantLock();
-------------- 相关代码 --------------
lock.lock();
try {// do something...
} finally {lock.unlock();
}
4.2 ReentrantLock与synchronized的区别(经典面试题)
- synchronized是一个关键字,是JVM内部实现的(大概率使用C++语言实现),而ReentrantLock是一个标准库中提供的类,是在JVM外部实现的(基于Java实现)
- ReentrantLock使用lock和unlock一对方法进行手动加锁、解锁,相较于synchronized更加灵活,但是也容易遗漏释放锁的步骤。
- synchronized是非公平锁,而ReentrantLock默认是非公平锁,但是可以变成公平锁,只需要在构造方法中传入参数为true即可。
- ReentrantLock比synchronized具有更加精准的唤醒机制,synchronized使用Object类的wait/notify进行唤醒,随机唤醒一个等待线程,而ReentrantLock借助类Condition实现等待唤醒,可以精准控制唤醒某一个指定线程。
5. CAS机制
5.1 CAS的基本概念
CAS:全程为compare and swap
字面意思就是比较并且交换,一个CAS涉及以下操作:
我们假设内存中的原数据为V,旧的预期值为A,需要修改的新值为B
- 比较A与V是否相等(比较)
- 如果比较相等则将B写入内存替换V(交换)
- 返回操作是否成功。
CAS伪代码:
public boolean CAS(address, expectValue, swapValue) {if (&address == expectValue) {&address = swapValue;return true;}return false;
}
注意:CAS是一个硬件指令,其操作是原子性的,这个伪代码只是辅助理解CAS的工作流程
对CAS原子性的理解:CAS可以看做是乐观锁的一种实现方式,当多个线程同时对某个资源进行CAS操作时,只要一个线程操作成功返回true,其余线程全部返回false,但是不会阻塞。
5.2 CAS的常见应用
5.2.1 实现原子类
标准库中提供了java.util.concurrent.atomic
包,里面的类都是基于CAS的方式实现的,最典型的就是AtomicInteger类,其中的getAndIncrement
方法相当于i++
操作
AtomicInteger atomicInteger = new AtomicInteger(0);
// 相当于i++操作
atomicInteger.getAndIncrement();
下面是AtomicInteger类基于CAS方式的伪代码实现:
class AtomicInteger {private int value;public int getAndIncrement() {int oldValue = value;while (CAS(value, oldValue, oldvalue + 1) != true) {oldValue = value;}return oldValue;}
}
5.2.2 实现自旋锁
基于CAS也可以实现更加灵活的锁,获取到更多的控制权
下面是基于CAS的自旋锁伪代码实现:
class SpinLock {private Thread owner;public void lock() {while (!CAS(owner, null, Thread.currentThread())) {}}public void unlock() {this.owner = null;}
}
5.3 CAS的ABA问题
5.3.1 ABA问题概述
什么是ABA问题:假设现在有两个线程共用一个共享变量num,初始值为A,此时线程t1,期望使用CAS机制将num值修改为Z,线程t1的CAS判定流程如下:
- 如果此时
num == A
,那么就将内存中的num值修改为Z - 如果此时
num != A
,那么就不进行修改,重新判定
但是此时中间穿插了线程t2执行将num值修改为了B,但是又重新修改为了A,所以尽管t1线程比较的预期结果是一致的,但是很可能已经是别人的形状了!
ABA导致的问题
ABA问题有可能会导致严重的后果,比如说我去银行ATM机取钱,考虑采用的时CAS机制,目前我的账户余额为1500,我按下确定按钮想要取出500元钱,但是ATM机卡住了,因此我又按下一次确定按钮,此时正常情况如下:
正常情况:
此时结果是符合预期的!扣款线程t1进行扣款操作,此时余额变为1000元,扣款线程t2判断当前余额已经不为1500了,说明已经扣款成功!于是不进行后续扣款操作!但是如果中间出现其他线程汇款操作,就会出现ABA问题,导致严重后果!
极端情况:
此时扣款线程t1完成扣款操作后,余额变为1000,但是中间穿插了汇款线程t3,刚好往账户中存入金额500,此时扣款线程t3判断余额仍然为1500,因此又进行了多余的一次扣款操作!!!这是相当不合理的
5.3.2 ABA问题解决思路
为了解决ABA带来的问题,我们可以考虑使用 版本号 的思想解决:
- CAS读取数据的时候,同时也要读取版本号
- 如果当前版本号与读取版本号一致,修改数据同时将版本号+1
- 如果当前版本号高于读取版本号,则说明数据已经被修改,当前操作非法
6. 线程安全的集合类
Java提供的集合类大部分都是线程不安全的
Vector,Stack,HashTable是线程安全的(但是官方不推荐使用)
6.1 多线程环境使用ArrayList
- 自己加锁(使用synchronized或者ReentrantLock实现)
- 使用
Collections.synchronizedList
,这是标准库提供的基于synchronized实现的线程安全的类,本质上是在关键方法上加了synchronized
修饰 - 使用CopyOnWriteArrayList,这是一个借助写时拷贝机制实现的容器,常用于配置文件等不经常修改,占用内存较小等场景
写时拷贝机制的核心就是可以对原容器进行并发的读,涉及写操作则先对原容器进行拷贝,然后向新容器中添加元素,最后修改引用,实现了读写分离!
6.2 多线程环境使用队列
- 自己加锁实现(synchronized或者ReentrantLock实现)
- 使用标准库提供的BlockingQueue接口及其实现类
6.3 多线程环境使用哈希表
-
使用HashTable
只是在关键方法上加上synchronized进行修饰
-
ConcurrentHashMap(常考面试题)
相比于Hashtable做出了一系列优化(以JDK1.8为例)
- 优化方式一:读操作没有加锁,只有写操作才会加锁,加锁的方式仍然使用synchronized,但是并不是锁整个对象,而是锁"桶"(使用链表的头结点作为锁对象),大大降低了锁冲突的概率
- 优化方式二:对于一些变量例如哈希表元素个数size,使用CAS机制避免重量级锁出现
- 优化方式三:优化了扩容方式,采用"化整为零","蚂蚁搬家"的方式,扩容期间新老数组同时存在,一次搬运只搬运少量元素,因此新增操作只需要在新数组中插入即可,查询操作需要在新老数组同时查询,删除操作也需要新老数组同时删除
相关文章:

【Java多线程进阶】JUC常见类以及CAS机制
1. Callable的用法 之前已经接触过了Runnable接口,即我们可以使用实现Runnable接口的方式创建一个线程,而Callable也是一个interface,我们也可以用Callable来创建一个线程。 Callable是一个带有泛型的interface实现Callable接口必须重写cal…...

Python算法100例-1.7 最佳存款方案
完整源代码项目地址,关注博主私信’源代码’后可获取 1.问题描述2.问题分析3.算法设计4.完整的程序 1.问题描述 假设银行一年整存零取的月息为0.63%。现在某人手中有一笔钱,他打算在今后5年中的每年年底取出1000元,到第5年时刚…...
ADO世界之FIRST
目录 一、ADO 简介 二、ADO 数据库连接 1.创建一个 DSN-less 数据库连接 2.创建一个 ODBC 数据库连接 3.到 MS Access 数据库的 ODBC 连接 4.ADO 连接对象(ADO Connection Object) 三、ADO Recordset(记录集) 1.创建一个 …...

【COMP337 LEC 5-6】
LEC 5 Perceptron : Binary Classification Algorithm 8 感应器是 单个神经元的模型 突触连接的强度取决于接受外部刺激的反应 X input W weights a x1*w1x2*w2....... > / < threshold Bias MaxIter is a hyperparameter 超参数 which has to be chosen…...

力扣72. 编辑距离(动态规划)
Problem: 72. 编辑距离 文章目录 题目描述思路复杂度Code 题目描述 思路 由于易得将字符串word1向word2转换和word2向word1转换是等效的,则我们假定统一为word1向word2转换!!! 1.确定状态:我们假设现在有下标i&#x…...
linux tree命令找不到:如何使用Linux Tree命令查看文件系统结构
Linux tree命令是一个用于显示文件夹和文件的结构的工具,它可以帮助用户更好地理解文件系统的结构。如果你在linux系统上找不到tree命令,那么可能是因为你的系统中没有安装tree命令。 解决方案 Linux tree命令是一个用于显示文件夹和文件的结构的工具&…...
OJ_最大逆序差
题目 给定一个数组,编写一个算法找出这个数组中最大的逆序差。逆序差就是i<j时,a[j]-a[i]的值 c语言实现 #include <stdio.h> #include <limits.h> // 包含INT_MIN定义 int maxReverseDifference(int arr[], int size) { if (size…...
MyBatis-Plus 实体类里写正则让字段phone限制为手机格式
/* Copyright © 2021User:啾啾修车File:ToupiaoRecord.javaDate:2021/01/12 19:29:12 */ package com.jjsos.repair.toupiao.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomido…...

K8S之运用污点、容忍度设置Pod的调度约束
污点、容忍度 污点容忍度 taints 是键值数据,用在节点上,定义污点; tolerations 是键值数据,用在pod上,定义容忍度,能容忍哪些污点。 污点 污点是定义在k8s集群的节点上的键值属性数据,可以决…...

Sora爆火,普通人的10个赚钱机会
您好,我是码农飞哥(wei158556),感谢您阅读本文,欢迎一键三连哦。💪🏻 1. Python基础专栏,基础知识一网打尽,9.9元买不了吃亏,买不了上当。 Python从入门到精通…...

【C++】C++入门—初识构造函数 , 析构函数,拷贝构造函数,赋值运算符重载
C入门 六个默认成员函数1 构造函数语法特性 2 析构函数语法特性 3 拷贝构造函数特性 4 赋值运算符重载运算符重载赋值运算符重载特例:前置 与 后置前置:返回1之后的结果后置: Thanks♪(・ω・)ノ谢谢阅读&…...
沁恒CH32V30X学习笔记04--外部中断
外部中断 CH32V2x 和 CH32V3x 系列内置可编程快速中断控制器(PFIC– Programmable Fast Interrupt Controller),最多支持 255 个中断向量。当前系统管理了 88 个外设中断通道和 8 个内核中断通道 PFIC 控制器 88个外设中断,每个中断请求都有独立的触发和屏蔽控制位,有专…...

基础IO[三]
close关闭之后文件内部没有数据, stdout和stderr 他们一起重定向,只会重定向号文件描述符,因为一号和二号描述符虽然都是sydout,但是并不一样,而是相当于一个显示器被打开了2次。 分别重定向到2个文件的写法和直接写道…...
Leetcode 392 判断子序列
题意理解: 给定字符串 s 和 t ,判断 s 是否为 t 的子序列。 字符串的一个子序列是原始字符串删除一些(也可以不删除)字符而不改变剩余字符相对位置形成的新字符串。(例如,"ace"是"abcde&quo…...

基于微信小程序的校园跑腿系统的研究与实现,附源码
博主介绍:✌程序员徐师兄、7年大厂程序员经历。全网粉丝12w、csdn博客专家、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 🍅文末获取源码联系🍅 👇🏻 精彩专栏推荐订阅👇…...

VTK Python PyQt 监听键盘 控制 Actor 移动 变色
KeyPressInteractorStyle 在vtk 中有时我们需要监听 键盘或鼠标做一些事; 1. 创建 Actor; Sphere vtk.vtkSphereSource() Sphere.SetRadius(10)mapper vtk.vtkPolyDataMapper() mapper.SetInputConnection(Sphere.GetOutputPort()) actor vtk.vtkAc…...

力扣 第 124 场双周赛 解题报告 | 珂学家 | 非常规区间合并
前言 整体评价 T4的dp解法没想到,走了一条"不归路", 这个区间合并解很特殊,它是带状态的,而且最终的正解也是基于WA的case,慢慢理清的。 真心不容易,太难了。 T1. 相同分数的最大操作数目 I 思路: 模拟 c…...
2024年华为OD机试真题-生成哈夫曼树-Java-OD统一考试(C卷)
题目描述: 给定长度为n的无序的数字数组,每个数字代表二叉树的叶子节点的权值,数字数组的值均大于等于1。请完成一个函数,根据输入的数字数组,生成哈夫曼树,并将哈夫曼树按照中序遍历输出。 为了保证输出的二叉树中序遍历结果统一,增加以下限制:二叉树节点中,左节点权…...
【实战】二、Jest难点进阶(二) —— 前端要学的测试课 从Jest入门到TDD BDD双实战(六)
文章目录 一、Jest 前端自动化测试框架基础入门二、Jest难点进阶2.mock 深入学习 学习内容来源:Jest入门到TDD/BDD双实战_前端要学的测试课 相对原教程,我在学习开始时(2023.08)采用的是当前最新版本: 项版本babel/co…...
(一)【Jmeter】JDK及Jmeter的安装部署及简单配置
JDK的安装和环境变量配置 对于Linux、Mac和Windows系统,JDK的安装和环境变量配置方法略有不同。以下是针对这三种系统的详细步骤: 对于Linux系统: 下载适合Linux系统的JDK安装包,可以选择32位或64位的版本。 将JDK的安装包放置在服务器下,创建一个新的文件夹来存储JDK,…...

7.4.分块查找
一.分块查找的算法思想: 1.实例: 以上述图片的顺序表为例, 该顺序表的数据元素从整体来看是乱序的,但如果把这些数据元素分成一块一块的小区间, 第一个区间[0,1]索引上的数据元素都是小于等于10的, 第二…...
【C语言练习】080. 使用C语言实现简单的数据库操作
080. 使用C语言实现简单的数据库操作 080. 使用C语言实现简单的数据库操作使用原生APIODBC接口第三方库ORM框架文件模拟1. 安装SQLite2. 示例代码:使用SQLite创建数据库、表和插入数据3. 编译和运行4. 示例运行输出:5. 注意事项6. 总结080. 使用C语言实现简单的数据库操作 在…...

Maven 概述、安装、配置、仓库、私服详解
目录 1、Maven 概述 1.1 Maven 的定义 1.2 Maven 解决的问题 1.3 Maven 的核心特性与优势 2、Maven 安装 2.1 下载 Maven 2.2 安装配置 Maven 2.3 测试安装 2.4 修改 Maven 本地仓库的默认路径 3、Maven 配置 3.1 配置本地仓库 3.2 配置 JDK 3.3 IDEA 配置本地 Ma…...
MySQL JOIN 表过多的优化思路
当 MySQL 查询涉及大量表 JOIN 时,性能会显著下降。以下是优化思路和简易实现方法: 一、核心优化思路 减少 JOIN 数量 数据冗余:添加必要的冗余字段(如订单表直接存储用户名)合并表:将频繁关联的小表合并成…...
LOOI机器人的技术实现解析:从手势识别到边缘检测
LOOI机器人作为一款创新的AI硬件产品,通过将智能手机转变为具有情感交互能力的桌面机器人,展示了前沿AI技术与传统硬件设计的完美结合。作为AI与玩具领域的专家,我将全面解析LOOI的技术实现架构,特别是其手势识别、物体识别和环境…...

tauri项目,如何在rust端读取电脑环境变量
如果想在前端通过调用来获取环境变量的值,可以通过标准的依赖: std::env::var(name).ok() 想在前端通过调用来获取,可以写一个command函数: #[tauri::command] pub fn get_env_var(name: String) -> Result<String, Stri…...

MyBatis中关于缓存的理解
MyBatis缓存 MyBatis系统当中默认定义两级缓存:一级缓存、二级缓存 默认情况下,只有一级缓存开启(sqlSession级别的缓存)二级缓存需要手动开启配置,需要局域namespace级别的缓存 一级缓存(本地缓存&#…...
用鸿蒙HarmonyOS5实现中国象棋小游戏的过程
下面是一个基于鸿蒙OS (HarmonyOS) 的中国象棋小游戏的实现代码。这个实现使用Java语言和鸿蒙的Ability框架。 1. 项目结构 /src/main/java/com/example/chinesechess/├── MainAbilitySlice.java // 主界面逻辑├── ChessView.java // 游戏视图和逻辑├──…...

快速排序算法改进:随机快排-荷兰国旗划分详解
随机快速排序-荷兰国旗划分算法详解 一、基础知识回顾1.1 快速排序简介1.2 荷兰国旗问题 二、随机快排 - 荷兰国旗划分原理2.1 随机化枢轴选择2.2 荷兰国旗划分过程2.3 结合随机快排与荷兰国旗划分 三、代码实现3.1 Python实现3.2 Java实现3.3 C实现 四、性能分析4.1 时间复杂度…...
boost::filesystem::path文件路径使用详解和示例
boost::filesystem::path 是 Boost 库中用于跨平台操作文件路径的类,封装了路径的拼接、分割、提取、判断等常用功能。下面是对它的使用详解,包括常用接口与完整示例。 1. 引入头文件与命名空间 #include <boost/filesystem.hpp> namespace fs b…...