【JavaEE】JUC(java.util.concurrent)的常见类以及线程安全的集合类

news2024/11/28 22:53:30

目录

1、JUC(java.util.concurrent)的常见类

1.1、Callable接口的用法(创建线程的一种写法)

 1.2、ReentrantLock可重入互斥锁

1.2.1、ReentrantLock和synchronized的区别 

1.2.2、如何选择使用哪个锁

1.3、Semaphore信号量

1.4、CountDownLatch

 2、线程安全的集合类

2.1、多线程环境使用ArrayList

 2.2、多线程使用队列

2.3、多线程使用哈希表

2.3.1、HashTable和ConcurrentHashMap的区别


1、JUC(java.util.concurrent)的常见类

JUC就是取java.util.concurrent的三个单词的首字母。所以JUC中存放的就是Java多线程开发使用到的工具类。

1.1、Callable接口的用法(创建线程的一种写法)

  • Callable接口非常类似于Runnable接口,Runnable接口通过run方法描述一个任务,表示一个线程要干啥,但是run方法的返回值类型是void,不能返回一个任务的结果产出。
  • 而Callable方法是通过重写call()方法,来描述一个线程执行的任务,在完成结果之后,可以返回一个计算结果。

 这里我们通过一个代码来了解Callable接口

创建线程计算1+2+3+.....+1000,使用Callable版本

  • 创建一个匿名内部类,实现Callable接口,Callable带有泛型参数,泛型参数表示返回值的类型
  • 重写Callable的call方法,完成累加的过程,直接通过返回值返回计算结果。
  • 把callable实例使用FutuerTask包装一下
  • 创建线程,线程的构造方法传入FutureTask,此时新线程就会执行FutureTask内部的Callable的call方法,完成计算,计算结果就放到FutureTask对象中。
  • 在主线程中调用futureTask.get()能够阻塞等待新线程计算完毕,并获取到FutureTask中的结果。
public class TestDemo27 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建一个任务
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                int sum = 0;
                for (int i = 1; i < 1000; i++) {
                    sum += i;
                }
                return sum;
            }
        };

        //创建一个线程,来执行第一个任务
        //Thread构造方法 不能直接将callable对象作为参数,需要使用FutureTask类进行包装一下,将FutureTask对象作为参数传给Thread。
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        Thread t = new Thread(futureTask);
        t.start();
        System.out.println(futureTask.get());
    }
}

✨我们这里来理解一下FutureTask类的作用。

我们去餐馆吃饭,在我们将菜点了之后,服务员给后厨大厨一张小票,也给我们一张小票。让后厨大厨根据小票上的要求制作,让我们通过小票去领我们自己的饭。我们使用的FutureTask就相当于一个小票,我们此时将futureTask传给t线程,就相当于大厨通过小票知道他要怎样做。我们通过futureTask.get()获取计算出来的结果,也就是我们的饭。


❓❓❓在上述的代码中,执行任务在t线程,而获取任务执行结果在主线程,这怎么能够确定多线程执行时,t线程一定在主线程之前结束??

❗❗❗我们在主线程中futureTask调用get方法,这个get方法,就有相当于join的作用,他会阻塞等待t线程执行完毕,再去执行主线程中的get方法。

✨总结Callable

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

 1.2、ReentrantLock可重入互斥锁

ReentranLock这是锁的另一种实现方式,和synchronized定位类似,都是用来实现互斥效果,保证线程安全。

✨ReentrantLock的用法:

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

1.2.1、ReentrantLock和synchronized的区别 

  1. synchronized是一个关键字,是JVM内部实现的(大概率是基于C++实现),ReentranLock是标准库中的一个类,在JVM外实现的(基于Java实现)
  2. synchronized使用时不需要手动释放锁ReentrantLock使用时需要手动释放,使用起来更灵活,但是也容易遗漏unlock
  3. synchronized申请锁的失败时,会死等ReentrantLock可以通过trylock的方式等待一段时间就放弃。(让程序员更灵活的决定接下来咋做)
  4. synchronized是非公平锁ReentrantLock默认是非公平锁但是它提供了公平和非公平两种工作模式,可以通过构造方法传入一个true开启公平锁模式
  5. 更强大的唤醒机制,synchronized是通过Object的wait/notify实现等待-唤醒每次唤醒的是一个随机等待的线程ReentrantLock搭配Condition类实现等待-唤醒。Condition这个类也能起到等待通知的效果,可以更精确控制唤醒某个指定的线程。

1.2.2、如何选择使用哪个锁

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

1.3、Semaphore信号量

信号量:用来表示"可用资源的个数"。本质上就是一个计数器。

✨理解信号量

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

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

  1. 我们所说的锁,本质上是计数器为1的信号量可用资源只有做一个,取值只有1和0两种,也叫做二元信号量。 一个线程获取到锁,这个时候信号量为0,只有等到线程将该锁释放掉,这个时候信号量为1,其他线程才能获取到锁。
  2. 我们可以认为信号量是更广义的锁,他不仅能管理锁,这中非0即1的资源;也能管理多个资源。

1.4、CountDownLatch

  • 同时等待N个任务执行结束。
  • 就好比跑步比赛,6个选手依次就位,发令枪一响,就表示开始,当最后一个人冲过终点,才能公布成绩。

✨将上面的情况可以使用多线程的思路进行描述

  1. 主线程,创建10个线程。主线程创建一个CountDownLatch对象,构造方法参数写10(表示10个参赛选手),10个线程分别完成各自的任务。
  2. 主线程使用CountDownLatch.await方法,来阻塞等待所有线程都执行完任务
  3. 10个线程每个线程执行完都会调用一个CountDownLatch.countDown方法表示选手到达终点)
  4. 10个线程在调用countDown方法时,主线程调用的await方法会记录有几个线程调用了countDown方法(就相当于,裁判员在记录有几个选手已经过线了),当这10个线程都调用过countDown方法之后,此时主线程的await就会阻塞接触,接下来就可以进行后续工作了。

 2、线程安全的集合类

我们在数据结构中说到的ArrayList、LinkedList、HashMap、PriorityQueue都是线程不安全的集合类。在多线程环境下使用,有可能会出现问题。

 这些数据结构多线程不安全,但是还要使用,该做怎样的处理呢?

2.1、多线程环境使用ArrayList

1️⃣最直接的方法,就是使用锁(synchronized或ReentrantLock),手动保证.

多个线程去修改ArrayList此时就可能有问题,就可以给修改操作进行加锁。

2️⃣、可以使用Vector类来代替ArrayList类。

Vector类中的关键方法都是带有synchronized的,这样可以保证在多线程环境下,这个类是安全的。但是Java官方明确表示,将Vector这个类标记为不建议使用的类。

3️⃣、 使用collections.synchronizedList(new list集合类)

  • collections.synchronizedList它就相当于一个外壳,将我们想要使用的list集合类,放在它里面,让list集合类当中的关键操作都带上synchronized。
  • synchronizedList是标准库提供的一个基于synchronized进行线程同步的List.
  • synchronizedList的关键操作上都带有synchronized

4️⃣、 使用CopyOnWriteArrayList(支持"写时拷贝"的集合类)

CopyOnWrite容器即写时复制的容器。

  • 当我们往一个容器里添加元素的时候,不直接往当前容器中添加,而是先将当前容器进行Copy,复制出一个新的容器,然后往新的容器里添加元素。
  • 添加完元素之后,在将原容器的引用指向新的容器。(引用的赋值操作,本身就是原子的)

所以CopyOnWrite容器也是一种读写分离的思想,读和写是不同的容器。

多线程读ArrayList是,此时没有线程安全的问题,但是当一些线程读,一些线程修改的时候,就会出现线程安全问题,但是使用CopyOnWriteArrayList,就不会产生线程安全问题了,读和写相互不影响。


  • 优点:这样做的好处就是,修改的同时对于读操作,是没有任何影响的,读的时候就会读取原来的旧数据,不会出现,读一个带有"修改了一半"的中间版本,也就是说适合于读多写少的情况,也适合数据小的情况,在我们日常配置数据的时候,经常就会用到这类操作。这种策略也叫做"双缓冲区策略"。就像我们在打游戏的时候,显卡就是采用的这种方式,显示器在读前一帧的画面的时候,显卡在画下一帧的画面。读的时候,在旧的集合中读,写的时候在新的集合中写,两种不会产生影响。
  • 缺点:占用内存较多,新写的数据不能第一时间读取到。

 2.2、多线程使用队列

我们之前说过的BlockingQueue就是线程安全的,在之前线程池的博客中已经说到了,这里就不过多说明了。

2.3、多线程使用哈希表

HashMap本身不是线程安全的。

🧨在多线程环境下使用哈希表可以使用:

1️⃣HashTable(虽然线程安全,但是不建议使用)

HashTable只是简单的把关键方法加上了synchronized关键字。

2️⃣ConcurrentHashMap(建议使用)

2.3.1、HashTable和ConcurrentHashMap的区别

1️⃣加锁粒度的不同(触发锁冲突的频率)

HashTable是针对整个哈希表加锁,任何的增删改查操作,都会触发加锁,也就都会可能有锁竞争。

🎉我们通过下面的场景来展现HashTable出现的问题

🎉此时我们通过下面的场景来展现ConcurrentHashMap在遇到与HashTable相同的问题时,它的处理方式,以及优点。

 

 📕补充:

上述情况是从Java1.8开始的,在Java1.7及其之前,ConcurrentHashMap使用"分段锁",目的和上述类似,相当于是好几个链表共用一把锁(这个设定,不科学,效率不够高,代码写起来也比较麻烦)

2️⃣ConcurrentHashMap更充分的利用了CAS机制(无锁编程),比如获取或更新元素个数,就可以直接使用CAS完成,不必加锁。

3️⃣优化了扩容策略

🎉对于HashTable,如果元素太多,就会涉及到扩容,扩容需要重新申请内存空间,搬运元素(把元素从旧的哈希表上删除,插入到新的哈希表上)。如果旧的HashTable中的元素非常多,搬运一次,成本就很高。刚好给HashTable中插入(put)元素的时候,负载因子超过了阈值,一次性搬运全部数据就会导致put操作非常的卡顿。

🎉对于ConcurrentHashMap扩容的策略,是化整为零,它不会试图依次性的把所有的元素都搬运到新表当中去,而是每次搬运一部分。

  • 当put触发扩容,此时就会直接创建更大的内存空间,但是并不会直接把所有元素都搬运过去,而是值搬运一小部分,这个时候的搬运速度就会比较快。
  • 此时就相当于存在两份hash表了,此时插入元素操作,就会直接往新表中插入元素;删除元素,就会删除旧表当中的元素;查找元素,就会新表和旧表一起都查。并且每次操作过程中,都搬运一部分元素,直至最后搬运完成。

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

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

相关文章

pta(浙大第四版)五道经典练习题③

目录 ①7-4 IP地址转换 ②、查找日期 ③藏头词 四、IP地址转换 五、删除链表值为偶数的节点 ①7-4 IP地址转换 题述&#xff1a;IP地址转换&#xff1a;一个IP地址是用四个字节&#xff08;每个字节8个位&#xff09;的二进制码组成。输入32位二进制字符串&#xff0c;输…

探索iOS转场动画

iOS提供图像转场动画&#xff0c;可实现酷炫的转场特效。动画包括&#xff1a;溶解、折叠、复印机、暴露、翻页、波纹、滑动等等。 一、溶解动画 CIDissolveTransition提供溶解动画&#xff0c;我们来看看对应的转场动画效果&#xff1a; 在CIFilter指定CIDissolveTransition…

Qt线程基础,多线程使用注意点,目前支持的线程种类。

Qt线程基础 一、什么是线程&#xff1f;二、GUI线程和工作线程三、同时访问数据四、使用线程1、何时使用线程的替代品2、应该用哪种Qt线程技术&#xff1f; 六、Qt中的多线程技术1、QThread:带有可选事件循环的低级API2、QThreadPool和QRunnable:重用线程 七、Qt Concurrent:使…

集成学习以及随机森林介绍

一、集成学习简介 1.什么是集成学习&#xff1f; 集成学习&#xff08;Ensemble Learning&#xff09;是一种机器学习方法&#xff0c;通过将多个弱学习器&#xff08;weak learner&#xff09;组合在一起来构建一个更强大的学习器&#xff08;strong learner&#xff09;。 …

C语言进阶——字符函数和字符串函数(下)

在前面我们已经学习了strlen、strcpy、strcat、strcmp几个库函数&#xff0c;今天我们继续学习剩余的库函数。 上期链接&#xff1a; C语言进阶——字符函数和字符串函数&#xff08;上&#xff09;_wangjiushun的博客-CSDN博客 目录&#xff1a; 3、长度受限制的字符串函数…

Redis(四)持久化策略

文章目录 持久化策略1、为什么Redis需要持久化2、Redis提供的两种持久化方式(1)RGB持久化详解概述RGB持久化的两种触发策略手动触发实例测试&#xff1a;自动触发实例测试&#xff1a; 查看rdb的状态信息info Persistence rdb模式的优缺点 (2)AOF持久化详解AOF持久化步骤&#…

近期复盘 | 想多了都是问题,想开了都是答案

文章目录 &#x1f339;四月坚持背单词&#xff0c;五月坚持利用AI写文章&#x1f60a;六月会坚持干什么&#x1f64c;23年7月&#xff1a;毕业两年&#xff0c;参保两年&#x1f440;强制存储&#xff0c;消费降级&#xff0c;开源节流&#x1f61c;好好深耕能力&#x1f381;…

JavaScript 进阶 (一)

目录 作用域 局部作用域 函数作用域 块作用域 全局作用域 作用域链 JS垃圾回收机制 闭包 变量提升 函数进阶 函数提升 函数参数 箭头函数 基本语法 箭头函数参数 箭头函数this 解构赋值 数组解构 对象解构 遍历数组 forEach 方法&#xff08;重点&#xff09; …

shell SNAT与DNAT

文章目录 SNATSNAT原理与应用SNAT实验 DNATDNAT原理与应用DNAT实验 SNAT SNAT原理与应用 SNAT 应用环境&#xff1a;局域网主机共享单个公网IP地址接入Internet&#xff08;私有不能早Internet中正常路由&#xff09; SNAT原理&#xff1a;修改数据包的源地址。 SNAT转换前提…

文心一言 VS 讯飞星火 VS chatgpt (23)-- 算法导论4.2 5题

五、V.Pan 发现一种方法&#xff0c;可以用 132 464 次乘法操作完成 68 x 68 的矩阵相乘&#xff0c;发现另一种方法&#xff0c;可以用 143 640 次乘法操作完成 70 x 70 的矩阵相乘&#xff0c;还发现一种方法&#xff0c;可以用155 424次乘法操作完成 72 x 72 的矩阵相乘。当…

数据安全治理科技产品能力-数据安全复合治理框架和模型解读(2)

数据治理,数据安全治理行业在发展,在实践,所以很多东西是实践出来的,哪有什么神仙理论指导,即使有也是一家之说,但为了提高企业投产比,必要的认知是必须的,落地数据安全治理科技水平差异直接决定产品和项目是否可持续性,当前和未来更需要专业和有效创新。数据安全治理…

自动驾驶业内动态简讯

1. 引言 参与自动驾驶领域相关研发工作已有多年&#xff0c;针对该领域的快速发展&#xff0c;收集业内各大科技公司最新进展和技术突破&#xff0c;供伙伴们交流探讨。 闲话少说&#xff0c;直接开始吧! 2. 博世 据新闻介绍&#xff0c;博世在德国道路上测试L4级无人驾驶汽…

java 区分缺陷Defects/感染Infections/失败Failure

java 区分缺陷Defects/感染Infections/失败Failure 缺陷Defects 软件故障总是从代码中一个或多个缺陷的执行开始。 缺陷只是一段有缺陷、不正确的代码。 缺陷可能是程序语句的一部分或完整部分&#xff0c;也可能对应于不存在但应该存在的语句。 尽管程序员要对代码中的缺陷负…

基于SSM的甜品店商城系统

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取项目下载方式&#x1f345; 一、项目背景介绍&#xff1a; 随着社会经济的发展和…

Altium Designer 相同电路多组复制布线

在进行设计开发的时候&#xff0c;总会遇到相同的电路&#xff0c;或者模块&#xff0c;这些电路可以使用相同的布局和走线。我们可以画好其中一部分&#xff0c;然后直接复制&#xff0c;就可以提高效率。下面记录我自己的实际操作过程&#xff0c;有一些地方遇到了问题&#…

Android | Android OS 源码结构

参考&#xff1a;AndroidXRef (http://androidxref.com/)版本&#xff1a;Pie - 9.0.0_r3 整体结构 对于 Android OS 的源码目录来说&#xff0c;各个版本的结构大同小异&#xff0c;随不同版本特性会有个别目录差异。编译后会额外产生一个 out 文件夹用于存储编译产生的文件。…

Unity使用SteamVR2.0实现基本功能(瞬移,抓取物品,射线点击,UI交互等)

基础设置 把SteamVR的Player预制件拖到一个空场景,删掉场景内原本的相机 一.瞬移 新建一个Plane,当做地板找到SteamVR的人物瞬移控制器 Teleporting ,把它拖到场景里 1. 范围移动 我们需要在可以移动的区域,也就是碰撞器上,挂TeleportArea脚本 这个脚本会自动修改你的材质球…

抖音seo源码--开源,支持二开不加密

抖音seo源码&#xff0c;抖音seo矩阵系统源码技术搭建&#xff0c;抖音seo源码技术开发思路梳理搭建 开发思路&#xff1a; 抖音seo源码如何搭建&#xff1f;抖音seo排名优化系统软件部分源码分析&#xff0c;代码打包中。。。 场景&#xff1a;在 python 中&#xff0c;你可…

windows下修改PyCharm默认terminal 在Git Bash中使用conda

windows下修改PyCharm默认terminal & 在Git Bash中使用conda windows下修改PyCharm默认terminal在Git Bash中使用conda windows下修改PyCharm默认terminal PyCharm的terminal默认使用的是powershell&#xff0c;但是conda环境是通过cmd运行的&#xff0c;因此可以将PyChar…

安捷伦N5182A是德KEYSIGHT N5182B 100KHZ至3G/6G信号发生器

Agilent N5182A、Keysight N5182A MXG 射频矢量信号发生器&#xff0c;100 kHz - 3 GHz 或 6 GHz ​Keysight N5182A (Agilent) MXG 射频矢量信号发生器具有快速频率、幅度和波形切换、带电子衰减器的高功率和高可靠性 – 所有这些都集成在两个机架单元 (2RU) 中。Keysight N5…