【Java多线程进阶】JUC常见类以及CAS机制

news2024/12/23 12:38:01

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() {
            @Override
            public 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>() {
            @Override
            public 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 相关面试题

  1. 介绍下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:

  1. acquire:相当于P操作,申请一个可用资源
  2. release:相当于V操作,释放一个可用资源

代码举例:

  1. 创建Semaphore实例并初始化为4,表示有4个可用资源
  2. 创建20个线程,每个线程都尝试申请资源,sleep1秒后释放资源,观察程序运行状况
public class SemaphoreExample {
    public static void main(String[] args) {
        // 初始化信号量为4
        Semaphore 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); // 睡眠1s
                    semaphore.release(); // 释放资源
                    System.out.println("线程" + id + "释放资源");
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            // 启动线程
            t.start();
        }
    }
}

执行结果
image.png
其中可以发现当前四个线程申请资源后,此时第五个线程尝试申请资源后,信号量变为-1就会阻塞等待,直到其他线程释放资源后才可以继续申请!

3. CountDownLatch的使用

CountDownLatch:可以等待N个任务全部完成。类似于N个人进行跑步比赛,直到最后一个人跃过终点才会宣布比赛结束,公布最后成绩。

3.1 CountDownLatch相关API

CountDownLatch提供API:

  1. new CountDownLatch(int n):构造方法,初始化n表示有n个任务需要完成
  2. countDown():任务执行完毕后调用,内部计数器进行自减
  3. await():阻塞等待所有任务全部完成后继续执行,相当于等待内部计数器为0

3.2 CountDownLatch代码案例

CountDownLatch代码案例:

  1. 创建10个线程,并初始化CountDownLatch为10
  2. 每个线程随机休眠1-5秒,模拟比赛结束
  3. 主线程中使用await阻塞等待全部执行完毕
public class CountDownLatchExample {
    public static void main(String[] args) throws InterruptedException {
        Random random = new Random();
        // 初始化CountDownLatch为10
        CountDownLatch 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("全部任务执行完毕!");
    }
}

执行结果
image.png
此时我们可以看到调用countDownLatch.await()方法时只有当所有的线程都执行完毕,即调用10次countDown方法后(即内部计数器为0)时才会停止阻塞!

4. ReentrantLock类

ReentrantLock:位于java.util.concurrent包下,被称为可重入互斥锁,和synchronized定位类似,都是用来实现互斥效果的可重入锁,保证线程安全。

4.1 ReentrantLock的用法

ReentrantLock相关API:

  1. lock():尝试加锁,如果加锁失败就阻塞等待
  2. tryLock(超时时间):尝试加锁,获取不到就阻塞等待一定时间,超时则放弃加锁
  3. unlock():解锁

注意:由于ReentrantLock需要手动释放锁,因此常常把unlock方法放在finally块中

ReentrantLock lock = new ReentrantLock();
--------------  相关代码  --------------
lock.lock();
try {
    // do something...
} finally {
    lock.unlock();
}

4.2 ReentrantLock与synchronized的区别(经典面试题)

  1. synchronized是一个关键字,是JVM内部实现的(大概率使用C++语言实现),而ReentrantLock是一个标准库中提供的类,是在JVM外部实现的(基于Java实现)
  2. ReentrantLock使用lock和unlock一对方法进行手动加锁、解锁,相较于synchronized更加灵活,但是也容易遗漏释放锁的步骤。
  3. synchronized是非公平锁,而ReentrantLock默认是非公平锁,但是可以变成公平锁,只需要在构造方法中传入参数为true即可。
  4. ReentrantLock比synchronized具有更加精准的唤醒机制,synchronized使用Object类的wait/notify进行唤醒,随机唤醒一个等待线程,而ReentrantLock借助类Condition实现等待唤醒,可以精准控制唤醒某一个指定线程。

5. CAS机制

5.1 CAS的基本概念

CAS:全程为compare and swap字面意思就是比较并且交换,一个CAS涉及以下操作:

我们假设内存中的原数据为V,旧的预期值为A,需要修改的新值为B

  1. 比较A与V是否相等(比较)
  2. 如果比较相等则将B写入内存替换V(交换)
  3. 返回操作是否成功。

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线程比较的预期结果是一致的,但是很可能已经是别人的形状了!
image.png

ABA导致的问题

ABA问题有可能会导致严重的后果,比如说我去银行ATM机取钱,考虑采用的时CAS机制,目前我的账户余额为1500,我按下确定按钮想要取出500元钱,但是ATM机卡住了,因此我又按下一次确定按钮,此时正常情况如下:
正常情况
image.png
此时结果是符合预期的!扣款线程t1进行扣款操作,此时余额变为1000元,扣款线程t2判断当前余额已经不为1500了,说明已经扣款成功!于是不进行后续扣款操作!但是如果中间出现其他线程汇款操作,就会出现ABA问题,导致严重后果!
极端情况
image.png
此时扣款线程t1完成扣款操作后,余额变为1000,但是中间穿插了汇款线程t3,刚好往账户中存入金额500,此时扣款线程t3判断余额仍然为1500,因此又进行了多余的一次扣款操作!!!这是相当不合理的

5.3.2 ABA问题解决思路

为了解决ABA带来的问题,我们可以考虑使用 版本号 的思想解决:

  • CAS读取数据的时候,同时也要读取版本号
  • 如果当前版本号与读取版本号一致,修改数据同时将版本号+1
  • 如果当前版本号高于读取版本号,则说明数据已经被修改,当前操作非法

6. 线程安全的集合类

Java提供的集合类大部分都是线程不安全的

Vector,Stack,HashTable是线程安全的(但是官方不推荐使用)

6.1 多线程环境使用ArrayList

  1. 自己加锁(使用synchronized或者ReentrantLock实现)
  2. 使用Collections.synchronizedList,这是标准库提供的基于synchronized实现的线程安全的类,本质上是在关键方法上加了synchronized修饰
  3. 使用CopyOnWriteArrayList,这是一个借助写时拷贝机制实现的容器,常用于配置文件等不经常修改,占用内存较小等场景

写时拷贝机制的核心就是可以对原容器进行并发的读,涉及写操作则先对原容器进行拷贝,然后向新容器中添加元素,最后修改引用,实现了读写分离!

6.2 多线程环境使用队列

  1. 自己加锁实现(synchronized或者ReentrantLock实现)
  2. 使用标准库提供的BlockingQueue接口及其实现类

6.3 多线程环境使用哈希表

  1. 使用HashTable

    只是在关键方法上加上synchronized进行修饰

  2. ConcurrentHashMap(常考面试题)

    相比于Hashtable做出了一系列优化(以JDK1.8为例)

    • 优化方式一:读操作没有加锁,只有写操作才会加锁,加锁的方式仍然使用synchronized,但是并不是锁整个对象,而是锁"桶"(使用链表的头结点作为锁对象),大大降低了锁冲突的概率
    • 优化方式二:对于一些变量例如哈希表元素个数size,使用CAS机制避免重量级锁出现
    • 优化方式三:优化了扩容方式,采用"化整为零","蚂蚁搬家"的方式,扩容期间新老数组同时存在,一次搬运只搬运少量元素,因此新增操作只需要在新数组中插入即可,查询操作需要在新老数组同时查询,删除操作也需要新老数组同时删除

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

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

相关文章

【sgCreateTableData】自定义小工具:敏捷开发→自动化生成表格列数据数组[基于el-table]

源码 <template><!-- 前往https://blog.csdn.net/qq_37860634/article/details/136141769 查看使用说明 --><div :class"$options.name"><div class"sg-head">表格数据生成工具</div><div class"sg-container&quo…

Nginx (window)2024版 笔记 下载 安装 配置

前言 Nginx (engine x) 是一款轻量级的 Web 服务器 、反向代理&#xff08;Reverse Proxy&#xff09;服务器及电子邮件&#xff08;IMAP/POP3&#xff09;代理服务器。 反向代理方式是指以代理服务器来接受 internet 上的连接请求&#xff0c;然后将请求转发给内部网络上的服…

java8-使用流-2

筛选各异的元素 流还支持一个叫作aistinct的方法&#xff0c;它会返回一个元素各异(根据流所生成元素的hashcode和eguals方法实现)的流。例如&#xff0c;以下代码会筛选出列表中所有的偶数&#xff0c;并确保没有重复。图5-2直观地显示了这个过程。 List<Integer>number…

「算法」滑动窗口

前言 算法需要多刷题积累经验&#xff0c;所以我行文重心在于分析解题思路&#xff0c;理论知识部分会相对简略一些 正文 滑动窗口属于双指针&#xff0c;这两个指针是同向前行&#xff0c;它们所夹的区间就称为“窗口” 啥时候用滑动窗口&#xff1f; 题目涉及到“子序列…

Unity所有关于旋转的方法详解

前言&#xff1a;欧拉角和四元数的简单描述 我们在Inspector面板上看到的rotation其实是欧拉角&#xff0c; 我们将Inspector面板设置成Debug模式&#xff0c;此时看到的local Rotation才是四元数。 Unity中的欧拉旋转是按照Z-X-Y顺规执行的旋转&#xff0c;一组欧拉旋转过程中…

C++11---lambda表达式

lambda表达式 lambda表达式概念lambda表达式语法lambda表达式各部分说明 lambda表达式交换两个数lambda表达式底层原理lambda表达式的底层原理 lambda表达式之间不能相互赋值 lambda表达式概念 lambda表达式是一个匿名函数&#xff0c;恰当使用lambda表达式可以让代码变得简洁…

Linux CPU 性能分析工具火焰图(Flame Graphs)认知

写在前面 博文内容为 《BPF Performance Tools》 读书笔记整理详细了解小伙伴可以访问作者官网&#xff1a;https://www.brendangregg.com/flamegraphs.html有油管上分享的作者在USENIX ATC 2017 的视屏理解不足小伙伴帮忙指正 不必太纠结于当下&#xff0c;也不必太忧虑未来&a…

Day-01-01

项目准备 项目介绍 1. 业务功能 本项目包括了用户端、机构端、运营端。 2. 技术选型 各层简要说明&#xff1a; 环境配置 1. 版本信息 在开发过程中&#xff0c;个人使用gitee作为版本控制工具。 2. 虚拟机安装说明 根据所提供资料&#xff0c;虚拟机IP地址已设置为192.1…

OpenAI Sora出炉,视频鉴赏,详细介绍,小白看过来~~立即尝试Sora,开启您的AI视频创作之旅吧!

OpenAI最新推出的视频创作的颠覆性产品&#xff1a;Sora&#xff0c;它开启了该行业的新纪元&#xff0c;吊打目前一众视频制作工具。 无论是专业人士还是爱好者&#xff0c;都可以轻松创作出高质量的视频内容。 Sora同样是一个根据文本指令创建逼真而富有想象力的场景的人工智…

图表示学习 Graph Representation Learning chapter2 背景知识和传统方法

图表示学习 Graph Representation Learning chapter2 背景知识和传统方法 2.1 图统计和核方法2.1.1 节点层次的统计和特征节点的度 节点中心度聚类系数Closed Triangles, Ego Graphs, and Motifs 图层次的特征和图的核节点袋Weisfieler–Lehman核Graphlets和基于路径的方法 邻域…

Flex布局简介及微信小程序视图层View详解

目录 一、Flex布局简介 什么是flex布局&#xff1f; flex属性 基本语法和常用属性 Flex 布局技巧 二、视图层View View简介 微信小程序View视图层 WXML 数据绑定 列表渲染 条件渲染 模板 WXSS 样式导入 内联样式 选择器 全局样式与局部样式 WXS 示例 注意事项…

阅读笔记(SOFT COMPUTING 2018)Seam elimination based on Curvelet for image stitching

参考文献&#xff1a; Wang Z, Yang Z. Seam elimination based on Curvelet for image stitching[J]. Soft Computing, 2018: 1-16. 注&#xff1a;SOFT COMPUTING 大类学科小类学科Top期刊综述期刊工程技术 3区 COMPUTER SCIENCE, ARTIFICIAL INTELLIGENCE 计算机&#xf…

二次元风格404页面源码

大气二次元风格带背景音乐404页面模板 蓝奏云&#xff1a;https://wfr.lanzout.com/ivaYi1odtjhe

中科院一区论文复现,改进蜣螂算法,Fuch映射+反向学习+自适应步长+随机差分变异,MATLAB代码...

本期文章复现一篇发表于2024年来自中科院一区TOP顶刊《Energy》的改进蜣螂算法。 论文引用如下&#xff1a; Li Y, Sun K, Yao Q, et al. A dual-optimization wind speed forecasting model based on deep learning and improved dung beetle optimization algorithm[J]. Ener…

Docker容器运行

1、通过--name参数显示地为容器命名&#xff0c;例如:docker run --name “my_http_server” -d httpd 2、容器重命名可以使用docker rename。 3、两种进入容器的方法&#xff1a; 3.1、Docker attach 例如&#xff1a; 每间隔一秒打印”Hello World”。 Sudo docker run…

高程 | 类与对象(c++)

文章目录 &#x1f4da;面向对象程序设计的基本特点&#x1f407;抽象——概括问题&#xff0c;抽出公共性质并加以描述。&#x1f407;封装——将抽象所得数据和行为相结合&#xff0c;形成一个有机的整体&#xff0c;形成“类”。&#x1f407;继承——在原有类特性的基础上&…

小白必看,总结前端所有主流的构建工具,webpack / vite / roollup / esbuild,包含源码,建议关注+收藏

前言 本篇文章旨在总结前端常见的构建工具&#xff0c;构建工具是前端工程化中的重要的组成部分。 在实际项目中&#xff0c;我们初始化项目&#xff0c;一般是使用脚手架命令一键生成的&#xff0c;比如说使用 create-vue 初始化 vue 项目的时候&#xff0c;就会默认使用 vi…

高效宣讲管理:Java+SpringBoot实战

✍✍计算机编程指导师 ⭐⭐个人介绍&#xff1a;自己非常喜欢研究技术问题&#xff01;专业做Java、Python、微信小程序、安卓、大数据、爬虫、Golang、大屏等实战项目。 ⛽⛽实战项目&#xff1a;有源码或者技术上的问题欢迎在评论区一起讨论交流&#xff01; ⚡⚡ Java实战 |…

Stable Diffusion 模型下载:Beautiful Realistic Asians(美丽真实的亚洲人)

本文收录于《AI绘画从入门到精通》专栏&#xff0c;专栏总目录&#xff1a;点这里。 文章目录 模型介绍生成案例案例一案例二案例三案例四案例五案例六案例七案例八案例九案例十 下载地址 模型介绍 Beautiful Realistic Asians&#xff08;BRA&#xff09;模型是由作者自己训练…

流程结构

章节目录&#xff1a; 一、选择结构1.1 if 语句1.2 if...else 语句1.3 嵌套 if1.4 if...else if...else1.5 三目运算符1.6 switch 语句 二、循环结构2.1 while 语句2.2 do...while 语句2.3 for 语句2.4 嵌套循环 三、跳转语句3.1 break 语句3.2 continue 语句3.3 goto 语句 四、…