【Java EE初阶十一】多线程进阶二(CAS等)

1. 关于CAS

        CAS: 全称Compare and swap,字面意思:”比较并交换“,且比较交换的是寄存器和内存;

        一个 CAS 涉及到以下操作:

        下面通过语法来进一步进项说明:

        下面有一个内存M,和两个寄存器A,B;

        CAS(M,A,B):该条指令意味着如果M和A中的值相同,则将M 和B中的值进行交换,在完成上述操作之后,返回true;如果M和A中的值不相同,则不用发生任何交换,同时返回false;

        综上所述,交换的本质就是当寄存器和内存中的值一样时,将其他寄存器中不同与内存中的值赋给内存;

1.1 CAS伪代码

        下面写的代码是伪代码,该段代码不能被顺利的编译运行,但是可以用来辅助理解上述所说 CAS 的工作流程.

boolean CAS(address, expectValue, swapValue) {if (&address == expectedValue) {&address = swapValue;return true;}return false;
}

        CAS其实是一个cpu指令(一条cpu指令就能满足上述比较交换的逻辑),说明单个cpu指令是原子的。故此可以使用CAS完成一些操作(给编写线程安全的代码,引入了新的思路并且不涉及线程阻塞),进一步代替“加锁”;      

基于CAS实现线程安全的方式,也称为“无锁编程”,其优缺点如下:

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

        缺点:

                1、代码会更加复杂,不好理解;

                2、只能够适合一些特定的场景,不如加锁方式更加普遍;

Cas本质上是cpu提供的指令->又被操作系统封装提供成api->又被jvm封装,也被提供成api->被程序员使用了;

1.2 CAS 有哪些应用

1.2.1 实现原子类

        Int++操作不是原子的(load,add,save),其中AtomicInteger,基于CAS的方式对int进行封装了,此时进行int++(基于cas指令来实现的)就是原子的操作了

结论:原子类里面是基于cas来实现的;下面是简化的代码: 

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

        通过在多线程t1和t2的分析中了解cas的简单原理:

        我们所说的“线程不安全”本质上是进行自增的过程中,被其他线程的自增行为穿插执行了;但是CAS是让这里的自增不要被穿插执行,其核心思路类似于加锁,但是加锁是通过阻塞的方式避免穿插,CAS则是通过重试的方式避免被穿插; 

1.2.2  实现自旋锁

1.2.2  关于ABA问题

        CAS进行操作的关键,是通过值“有没有发生变化”作为“有没有其他线程穿插执行的”判定依据,但是在一些的极端的情况下,我们的值本来是正常情况下的A的成为A->B->A,针对第一个要判断的线程来说,看起来由于值没有变二判定没有其他线程进行穿插执行,但是事实上我们已经存在线程穿插执行的问题了。

        如下图所示,虽然使用cas语句进行判定的时候内存中和寄存器中的数值一样,但是我们不能确定内存中的值是始终没有发生变化还是发生变化之后被其他线程又成功改回来了;

2. JUC的相关类

        JUC(java.util.concurrent),且Concurrent:并发的意思,这个包里面的内容,主要就是一些多线程相关的组件;

2.1 Callable 接口

        该接口也是一种创建线程的方式,适合于想让某个线程执行一个逻辑,并且返回结果的时候;相对而言,runnable不关注结果,代码举例如下:

        代码示例: 创建线程计算 1 + 2 + 3 + ... + 1000, 使用 Callable 版本

        创建一个匿名内部类, 实现 Callable 接口. Callable 带有泛型参数. 泛型参数表示返回值的类型.

        重写 Callable 的 call 方法, 完成累加的过程. 直接通过返回值返回计算结果.

        把 callable 实例使用 FutureTask 包装一下.

        创建线程, 线程的构造方法传入 FutureTask . 此时新线程就会执行 FutureTask 内部的 Callable 的 call 方法, 完成计算. 计算结果就放到了 FutureTask 对象中.

        在主线程中调用 futureTask.get() 能够阻塞等待新线程计算完毕. 并获取到 FutureTask 中的结果.

        

Callable<Integer> callable = new Callable<Integer>() {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i <= 1000; i++) {sum += i;}return sum;}
};
FutureTask<Integer> futureTask = new FutureTask<>(callable);
Thread t = new Thread(futureTask);
t.start();
int result = futureTask.get();
System.out.println(result)
}

下面图解主要是关于futuretask的讲解:

        理解 Callable:

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

2.2 ReentrantLock

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

        ReentrantLock 的用法:

        lock(): 加锁, 如果获取不到锁就死等.

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

        unlock(): 解锁

ReentrantLock lock = new ReentrantLock(); 
-----------------------------------------
lock.lock();   
try {    // working    
} finally {    lock.unlock()    
}  

        ReentrantLock相对于synchronized的优势:

        1、ReentrantLock,在加锁的时候,有两种方式lock(加锁失败就会阻塞等待) 和trylock(加锁失败就会放弃);

        2、ReentrantLock还通过了公平锁的实现(默认情况下是非公平锁)

        3、ReentrantLock提供了更强大的等待通知机制,主要是搭配了condition类,实现等待通知的;

        总的来说,我们在加锁的时候,首选synchronized(会有优化锁的策略),因为ReentrantLock使用起来更加复杂,尤其是容易忘记解锁;

2.3 信号量 Semaphore

        Semaphore 信号量, 本质上就是一个计数器. 用来表示 "可用资源的个数".每次申请一个可用资源,就需要让计数器-1(p操作);每次释放一个可用资源,就需要让计数器+1(v操作),操作系统,提供了信号量实现,同时操作系统也提供了api;jvm封装了这样的api,就可以在java代码中使用了;

        理解信号量:

        可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源. 当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作) 当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作) 如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

        Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.

        关于semaphore的代码如下:

Semaphore semaphore = new Semaphore(4);
Runnable runnable = new Runnable() {@Overridepublic void run() {try {System.out.println("申请资源");semaphore.acquire();System.out.println("我获取到资源了");Thread.sleep(1000);System.out.println("我释放资源了");semaphore.release();} catch (InterruptedException e) {e.printStackTrace();}}
};
for (int i = 0; i < 20; i++) {Thread t = new Thread(runnable);t.start();
}

 2.4 CountDownLatch

        同时等待 N 个任务执行结束.主要适用于,多个线程来完成一系列任务的时候,用来衡量任务的进度是否完成。

        比如需要把一个很大的任务,拆分成多个小任务,让这些小任务并发的去执行。就可以使用CountDownLatch来判定说当前的这些任务是否都完全全部完成了;

        Eg:下载一个文件,就可以使用多线程下载;相比之下,有一些专业的下载工具(往往和资源服务器之间只有一个连接,服务器往往会对于连接传输的速度有一定的限制),就可成倍的提升下载速度(IDM),多线程下载(每个线程都建立一个连接,此时就需要把整个大任务进行分割)

        CountDownLatch 主要有两个方法:

  1. await,该方法调用的时候就会阻塞,就会等待其他线程完成任务,当所有的线程都完全的完成了任务之后,此时这个await才会返回,才会继续往下走;
  2. CountDown,告诉CountDownLatch,我当前的一个子任务已经完成了

结果如下:

3 线程安全的集合类

3.1 多线程环境使用 ArrayList:

3.1.1 Collections.synchronizedList(new ArrayList);

        synchronizedList 是标准库提供的一个基于 synchronized 进行线程同步的 List. synchronizedList 的关键操作上都带有 synchronized

3.1.2  使用 CopyOnWriteArrayList

        写时拷贝;

        比如,两个线程使用同一个arraylist,可能会读,也可能会修改;

        如果要是两个线程读,则可以直接进行读操作即可;

        如果某个线程需要进行修改,就把arraylist临时复制出一个副本,进行修改的线程就修改这个副本,与此同时,另外一个线程任然可以从原来的数据文件上读取数据,一旦这边修改的临时文件修改完毕,就会使用修改好的这份数据文件来代替原来的数据文件。 

该方法的局限性:

  1. 当前操作的ArrayList不能太大(拷贝成本,不能太高)
  2. 更适用于一个线程去修改,而不能是多个线程去同时修改(多个线程读,一个线程修改)

        这种场景特别适用于服务器的配置更新~~,可以通过配置文件来描述配置的详细内容(本身就不会很大),配置的内容会被读取到内存中,再有其他的线程读取这里的内容,但是修改这个配置内容,往往只能有一个线程来修改;

应用场景:使用某个命令让服务器重新加载配置,就可以使用写时拷贝的方式;

3.2 多线程环境使用哈希表  

3.2.1 Hashtable

        HashMap 本身不是线程安全的. 在多线程环境下使用哈希表可以使用:  ConcurrentHashMap

        Hashtable保证线程安全,主要是给关键方法加上synchronized(类似于给this加锁),同时只有两个线程在操作同一个Hashtable就会出现锁冲突

        如上图所示,当两个不同的key映射到同一个数组下标上,就会出现hash冲突,使用链表来解决hash冲突;

        按照上述这样的方式来操作 ,并且在不考虑触发扩容的前提下,操作不同的链表的时候就是线程安全的,相比之下,如果两个线程操作的是同一个链表,才会比较容易发生线程安全的问题;故此连个线程,操作的是不同的链表,就根本不用加锁,只要在操作同一个链表的时候才需要进行加锁;

3.2.2 ConcurrentHashMap

        ConcurrentHashMap相比于 Hashtable 做出了一系列的改进和优化,简单如下所示:

1、 ConcurrentHashMap最核心的改进,就是把一个全局的大锁,改进成了每个链表独立的一把小锁,这样就大幅度的降低了锁冲突的概率(一个hash表有很多这样的链表,两个线程恰好同时访问一个链表的概率比较少)--->就是把每一个链表的头结点作为锁对象,synchronized可以使用任何对象作为锁对象

2、充分利用了cas的特性,把一些不必要加锁的环节给省略了,比如需要使用变量记录hash表中的元素个数,就可以使用原子操作(cas)修改元素个数;

3、 ConcurrentHashMap,还有一个激进的操作,针对读操作没有进行加锁,读和读之间,读和写之间,都不会有锁竞争;写和写之间是需要进行加锁的

q:是否会存在“读到一个修改了一半的数值呢”这种情况?

a:ConcurrentHashMap 在底层编码的过程中,比较谨慎的处理了一些细节,修改数值的时候就会避免使用++,--这种非原子的操作,使用=进行修改的时候,本身就是原子的,读的时候,要么读到的就是之前所写的旧的数值,要么读到的就是重写修改后的数值,不会出现一个修改到一半的数值;

4、ConcurrentHashMap针对扩容操作进行了单独的优化

        本身Hashtable和HashMap在扩容的时候,都是需要把所有单独的元素都拷贝一遍的(如果元素较多的话,就会比较耗时)即1000个用户访问,且只有一个人在访问的时候触发扩容遇到卡顿,所以就需要化整为零的进行复制,一旦需要扩容,我们旧分为很多次进行搬运复制,每次只用复制一小部分防治这一个单次访问遇到卡顿;

        当然,ConcurrentHashMap基本的使用方法和普通的HasMap完全一样

ps:本篇的内容到这里就结束啦,如果对你有所帮助的话,就请一键三连哦哦!!!

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

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

相关文章

SeaTunnel 2.3.4 Cluster in K8S

参考&#xff1a;seatunnel k8s运行zeta引擎&#xff08;cluster-mode模式&#xff09;_apache seatunnel zeta 启动-CSDN博客 以上参考使用的是2.3.3版本 下载2.3.4版本, 上dlcdn.apache.org下载 &#xff0c;官网下载有问题 wget https://dlcdn.apache.org/seatunnel/2.3.4/…

吴恩达机器学习-可选实验:梯度下降逻辑回归(Gradient Descent for Logistic Regression)

文章目录 目标数据集Logistic梯度下降梯度下降实现计算梯度&#xff0c;代码描述 另一个数据集 目标 在本实验中&#xff0c;你将: 更新逻辑回归的梯度下降在一个熟悉的数据集上探索梯度下降使用梯度下降给逻辑回归更新参数 import copy, math import numpy as np %matplotl…

Go微服务: 基于GRPC结合Consul实现微服务调用

基于GRPC结合Consul实现微服务调用 1 &#xff09;环境准备 基于 go workspace 准备3个包: protos&#xff0c;server, client新建 demo目录&#xff0c;其内部结构如下├── protos │ ├── go.mod │ └── users │ ├── users.proto │ …

怎么判断你的模型是好是坏?模型性能评估指标大全!

模型性能评估指标&#xff0c;大家一定不陌生&#xff01;很多小伙伴们都说难&#xff0c;但是它真的很重要很重要很重要&#xff01;它会对我们的模型有很多的指导&#xff0c;也会给我们真正做模型的时候提供一些指导性的思想&#xff0c;不然我们看到别人的东西只能跟着人家…

centos7.9升级ssh和openssl

一、环境 [roottmp179 package]# ssh -V OpenSSH_7.4p1, OpenSSL 1.0.2k-fips 26 Jan 2017 [roottmp179 package]# cat /etc/redhat-release CentOS Linux release 7.9.2009 (Core) 二、 升级前准备 mkdir /opt/package cd /opt/package wget https://www.openssl.org/source…

【linux】冯诺依曼体系与操作系统的理解

本篇文章是进程的预备知识&#xff0c;但也不仅仅是进程的预备知识&#xff0c; 也可以更好地帮助我们理解整个计算机体系。 目录 冯诺依曼体系结构&#xff1a;进一步理解操作系统&#xff1a; 冯诺依曼体系结构&#xff1a; 关于这张图先进行一下必要的解释&#xff1a; 输…

怎样通过IT服务台来增强IT项目管理?

当下&#xff0c;越来越多的企业和组织重视IT项目管理的重要性。而如何通过IT服务台来增强和提升IT项目管理效率&#xff0c;成为了许多企业领导和IT专业人员共同关注的话题。如何充分利用IT服务台&#xff0c;以促进IT项目管理水平的提升和项目成功率的增加变得至关重要。 1…

Codeforces Round 924 (Div. 2)---->B. Equalize

总思路&#xff1a;首先我们做这题的时候有两个点一定要知道&#xff1a; 1.当数组中有重复元素的时候&#xff0c;只有其中的一个才能贡献一个相同元素&#xff0c;其他的都不行&#xff08;因为是排列&#xff0c;一个数只出现一次&#xff09;&#xff0c;所以我们可以用使…

怎么免费下载无水印视频素材?赶快收藏这六个网站。

今天来教大家怎么下载无水印视频素材&#xff0c;其中一些是免费的&#xff0c;并且可以在商业项目中使用&#xff0c;这些网站都是无水印视频素材&#xff0c;可以放心使用。 蛙学网&#xff1a; 网站的内容非常丰富多彩&#xff0c;包括风景&#xff0c;夜景&#xff0c;食物…

论文阅读:Editing Large Language Models: Problems, Methods, and Opportunities

Editing Large Language Models: Problems, Methods, and Opportunities 论文链接 代码链接 摘要 由于大语言模型&#xff08;LLM&#xff09;中可能存在一些过时的、不适当的和错误的信息&#xff0c;所以有必要纠正模型中的相关信息。如何高效地修改模型中的相关信息而不影…

【JS】自动下拉网页刷新,当出现指定关键字,就打印出来

批量检查域名是否可以注册 1、有的网站数据是通过下拉发生请求&#xff0c;间隔x毫秒自动下拉 2、查找某个关键字&#xff0c;找到就打印出来 3、打印数据自动去重 4、当连续n次下拉&#xff0c;没有新div元素出来&#xff0c;就停止该循环 var map {}; var count 0; var l…

qt如何将QHash中的数据有序地放入到QList中

在qt中&#xff0c;要将QHash中的数据有序地放入到QList中&#xff0c;首先要明白&#xff1a; 我们可以遍历QHash中的键值对&#xff0c;并将其按照键的顺序或值的大小插入到QList中&#xff0c;直接用for循环即可。 #include <QCoreApplication> #include <QHas…

java学习(Arrays类和System类)

目录 目录 一.Arrays类 二.System常见方法 三、Biglnteger和BigDecimal&#xff08;高精度&#xff09; 1.Biglnter的常用方法 2.BigDecimal常见方法 3.日期类 1)第一代日期类 2&#xff09;第二代日期类 3)第三代日期类 一.Arrays类 Arrays包含了一系 列静态方法&am…

11、Linux-安装和配置Redis

目录 第一步&#xff0c;传输文件和解压 第二步&#xff0c;安装gcc编译器 第三步&#xff0c;编译Redis 第四步&#xff0c;安装Redis服务 第五步&#xff0c;配置Redis ①开启后台启动 ②关闭保护模式&#xff08;关闭之后才可以远程连接Redis&#xff09; ③设置远程…

12双体系Java学习之局部变量和作用域

局部变量 局部变量的作用域 参数变量

理解记忆相关

foreach循环 在 Java 中&#xff0c;foreach 循环&#xff08;也称为增强型 for 循环&#xff09;是一种简洁的语法&#xff0c;用于遍历数组或集合&#xff08;如 List、Set、Map 等&#xff09;。以下是 foreach 循环的基本用法&#xff1a; 遍历数组&#xff1a; String[] …

在 Python 中从键盘读取用户输入

文章目录 如何在 Python 中从键盘读取用户输入input 函数使用input读取键盘输入使用input读取特定类型的数据处理错误从用户输入中读取多个值 getpass 模块使用 PyInputPlus 自动执行用户输入评估总结 如何在 Python 中从键盘读取用户输入 原文《How to Read User Input From t…

Rust:为 Trait 定义默认的方法

当你提到“指定 trait 的实现”并使用 :: 符号时&#xff0c;你可能是指在某些情况下&#xff0c;你想直接通过 trait 而不是具体的类型来调用方法。这在 trait 提供了默认方法实现时尤其有用&#xff0c;因为你可以不依赖任何具体的类型实现来调用这些方法。 然而&#xff0c…

AI写真变现项目丨超级训练营SOP手册

出品方&#xff1a; 吴东子团队 x AI破局俱乐部 以下只是该SOP手册的部分介绍&#xff0c;AI写真变现项目上手到变现全流程&#xff0c;需要完整手册的可以dd我。 AI写真 首先什么是AI写真&#xff0c;顾名思义的话可以说成是用AI生成写真照&#xff0c;我们先暂且这么理解&am…