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

这篇具有很好参考价值的文章主要介绍了【JavaEE】JUC(java.util.concurrent)的常见类以及线程安全的集合类。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

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出现的问题

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

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

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

 

 📕补充:

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

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

3️⃣优化了扩容策略

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

🎉对于ConcurrentHashMap扩容的策略,是化整为零,它不会试图依次性的把所有的元素都搬运到新表当中去,而是每次搬运一部分。文章来源地址https://www.toymoban.com/news/detail-469319.html

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

到了这里,关于【JavaEE】JUC(java.util.concurrent)的常见类以及线程安全的集合类的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • javaEE初阶——多线程(九)——JUC常见的类以及线程安全的集合类

    T04BF 👋专栏: 算法|JAVA|MySQL|C语言 🫵 小比特 大梦想 此篇文章与大家分享多线程专题的最后一篇文章:关于JUC常见的类以及线程安全的集合类 如果有不足的或者错误的请您指出! 3.1Callable接口 Callable和Runnable一样,都是用来描述一个任务的 但是区别在于 ,用Callable描述的任务是有

    2024年04月25日
    浏览(43)
  • JUC并发编程-集合不安全情况以及Callable线程创建方式

    1)List 不安全 ArrayList 在并发情况下是不安全的 解决方案 : 1.Vector 2.Collections.synchonizedList() 3. CopyOnWriteArrayList 核心思想 是,如果有 多个调用者(Callers)同时要求相同的资源 (如内存或者是磁盘上的数据存储),他们 会共同获取相同的指针指向相同的资源 , 直到某个调用者

    2024年01月23日
    浏览(48)
  • 如何解决java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@7566d7cf r...

    Java中的 java.util.concurrent.RejectedExecutionException 异常表示无法将任务提交到线程池中执行。这通常是因为线程池处于关闭状态或者已经达到了最大线程数,无法再接受新的任务。 要解决这个异常,你可以考虑以下几种方法: 检查线程池的状态,确保它处于可以接受新任务的状态

    2024年02月13日
    浏览(53)
  • java.util.concurrent.Executionexception 异常

    今天运行时发生了如下报错。自己捣鼓半天也没发现问题出在哪儿,感谢大佬的帮助,记录下来防止再犯。。 caused by org.apache.flink.client.program.programInvocationException: Job failed。程序调用异常。网上找了很多解决方法,都没有能够解决这个问题。 直到在报错中发现了这一行: C

    2024年02月19日
    浏览(39)
  • 已解决java.util.concurrent.ExecutionException异常的正确解决方法,亲测有效!!!

    已解决java.util.concurrent.ExecutionException异常的正确解决方法,亲测有效!!! java.util.concurrent.ExecutionException java.util.concurrent.ExecutionException是Java多线程编程中常见的异常之一,它表示在执行一个Callable或者Runnable任务时,任务抛出了一个异常。 下滑查看解决方法 具体解决方法可

    2024年02月11日
    浏览(36)
  • 关于报错java.lang.reflect.InaccessibleObjectException: Unable to make field private java.util.concurrent

    java.lang.reflect.InaccessibleObjectException: Unable to make field private java.util.concurrent.Callable java.util.concurrent.FutureTask.callable accessible: module java.base does not \\\"opens java.util.concurrent\\\" to unnamed module @32eebfca 假如报这种错误,只需要在Run-Edit Configurations-Modify Options-add VM options中加  假如你报的是下面

    2024年02月15日
    浏览(91)
  • 【问题已解决】Unrecognized option: --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED

    今天在创建java项目时,运行报错,说无法成功创建java程序。 Unrecognized option: --add-opens=java.base/java.util.concurrent.atomic=ALL-UNNAMED Error: Could not create the Java Virtual Machine. Error: A fatal exception has occurred. Program will exit. 解决办法: 1、使用最新的jdk版本 2、在第三处,选择最新的jdk版本

    2024年02月13日
    浏览(51)
  • 【Java多线程】线程中几个常见的属性以及状态

    目录 Thread的几个常见属性 1、Id 2、Name名称 3、State状态 4、Priority优先级 5、Daemon后台线程 6、Alive存活   ID 是线程的唯一标识,由系统自动分配,不同线程不会重复。 用户定义的名称。该名称在各种调试工具中都会用到。 状态表示线程当前所处的一个情况。和进程一样,线程

    2024年02月19日
    浏览(43)
  • JUC面试(五)——Collection线程不安全

    当我们执行下面语句的时候,底层进行了什么操作 new ArrayListInteger(); 底层创建了一个空的数组,伴随着初始值为10 当执行add方法后,如果超过了10,那么会进行扩容,扩容的大小为原值的一半,也就是5个,使用下列方法扩容 Arrays.copyOf(elementData, netCapacity) 单线程环境 单线程环

    2023年04月23日
    浏览(31)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包