【JUC基础】11. 并发下的集合类

这篇具有很好参考价值的文章主要介绍了【JUC基础】11. 并发下的集合类。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

目录

 1、前言

2、并发下的ArrayList

2.1、传统方式

2.1.1、程序正常运行

2.1.2、程序异常

2.1.3、运行期望值不符

2.2、加锁

2.3、synchronizedList

2.4、CopyOnWriteArrayList

3、并发下的HashSet

3.1、CopyOnWriteArraySet

3.2、HashSet底层是什么?

4、并发下的HashMap

4.1、传统方式

4.2、ConcurrentHashMap

4.3、ConcurrentHashMap底层结构

5、小结


 1、前言

我们直到ArrayList,HashMap等是线程不安全的容器。但是我们通常会频繁的在JUC中使用集合类,那么应该如何确保线程安全?

2、并发下的ArrayList

2.1、传统方式

如果在JUC中直接使用ArrayList,可能会引发一系列问题。先来看一段代码:


public class ArrayListTest {

    // 创建一个集合类
    static List<Integer> list = new ArrayList<>(10);

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {

            // 每次执行前将list清空
            list.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + list.size());
        }

    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                list.add(i);
            }
        }
    }
}

执行结果:

【JUC基础】11. 并发下的集合类

我们看到执行了10次,居然会出现3种不同的结果。

2.1.1、程序正常运行

从上述的运行结果可以看出,运行10次,有概率出现程序正常运行,也得到了期望的20000这个数值。这说明在JUC中使用ArrayList集合,有概率成功,并不一定每次都会出现问题。

2.1.2、程序异常

可以看到上面其中一次运行结果出现了报错,抛出了ArrayIndexOutOfBoundsException异常。这是因为ArrayList我们设置初始容量为10,在多线程操作中要进行扩容。而在扩容过程中,内部的一致性被破坏,由于没有锁机制,另外一个线程访问到了不一致的内部状态,导致数组越界。

2.1.3、运行期望值不符

相比上面程序异常,程序异常会显式抛出异常信息,还相对容易排查。而这个问题较为隐蔽,从执行结果来看,大部分都是这个问题。也就是运行结果并不是我们所期望的结果。JUC学到这里,应该多少都直到这个就是典型的线程不安全导致的结果。由于多线程访问冲突,使得list容器大小的变量被多线程不正常访问,两个线程对list中的同一个位置进行赋值导致的。

2.2、加锁

上面说到list没有锁机制,出现了多线程问题。那么要解决此类问题,肯定是直接加锁, 我们顺便把集合数量改大点。改造后代码:


public class ArrayListTest {

    // 创建一个集合类
    static List<Integer> list = new ArrayList<>(10);

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            list.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放1000000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到2000000个,并且打印list.sizes()=2000000
            System.out.println("最终集合数量:" + list.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000000; i++) {
                synchronized (list) {
                    list.add(i);
                }
            }
        }
    }
}

运行结果:

【JUC基础】11. 并发下的集合类

说明线程安全问题被解决。

2.3、synchronizedList

相比上面直接加synchronized方法的解决方式,JDK提供了一种自带synchronized的集合,来保证线程安全。如vector也是如此。

改造代码:


public class ArrayListTest {

    // 创建一个集合类,Collections.synchronizedList来保证线程安全
    static List<Integer> list = Collections.synchronizedList(new ArrayList<>(10));

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            list.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + list.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 1000000; i++) {
                list.add(i);
            }
        }
    }
}

同样执行结果:

【JUC基础】11. 并发下的集合类

2.4、CopyOnWriteArrayList

JUC也给我们提供了一种线程安全的变体ArrayList。根据名字就可以直到他是采用复制“快照”的方式,性能上是会有一定开销的。这里在实验过程中,明显感觉得到结果的速度变慢了。

【JUC基础】11. 并发下的集合类

改造后代码:


public class ArrayListTest {

    // 创建一个集合类,CopyOnWriteArrayList,写入时复制。
    // 当多个线程调用的时候,对list进行写入操作时,将数据拷贝避免由于多线程同时操作而被覆盖。可以简单理解成读写分离操作。
    // 这个类的操作使用的是lock锁,相比上述的两种synchronized来实现同步,性能更高
    static List<Integer> list = new CopyOnWriteArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            list.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + list.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                list.add(i);
            }
        }
    }
}

运行结果:

【JUC基础】11. 并发下的集合类

那么他是如何保证线程安全的呢?我们查看他的源码发现:

【JUC基础】11. 并发下的集合类

在他的setArra方法中,对array加了transient和volatile修饰,从而保证了线程安全。

transient:被transient修饰的属性,是不会被序列化的。后面有机会单独详细讲

volatile:防止指令重排,以及保证可见性。他是java中一种轻量的同步机制,相比synchronized来说,volatile更轻量级。后面单独会讲

3、并发下的HashSet

HashSet和ArrayList存在同样的问题。

public class HashSetTest {

    static Set<Integer> hashSet = new HashSet<>();

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            hashSet.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + hashSet.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                hashSet.add(i);
            }
        }
    }
}

执行结果:

【JUC基础】11. 并发下的集合类

与ArrayList类似,当然也存在加锁的方式。同样采用JDK提供的方式:

Collections.synchronizedSet(new HashSet<>());

3.1、CopyOnWriteArraySet

同样JUC也提供了类似CopyOnWriteArrayList的方式。

【JUC基础】11. 并发下的集合类

改造后代码:

public class HashSetTest {

    static Set<Integer> hashSet = new CopyOnWriteArraySet<>();

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            hashSet.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + hashSet.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                hashSet.add(i);
            }
        }
    }

}

运行结果:

【JUC基础】11. 并发下的集合类

3.2、HashSet底层是什么?

细心的网友有没有发现,这里的运行结果也不是我们期望的20000。而是10000。那么是不是说明这里其实并不能保证线程安全?JDK出bug了?

这里就涉及到HashSet的底层存储结构了。我们跟进去看下HashSet源码:

【JUC基础】11. 并发下的集合类

我们可以看到HashSet的底层结构其实是个HashMap,而HashSet存储的是使用了HashMap的key。这就保证了HashSet的存储是不能重复的。

hashSet的add方法使用的就是HashMap的put方法:

【JUC基础】11. 并发下的集合类

而我们上面两个线程都同时从0开始存储,因而被去重导致期望结果是10000。而CopyOnWriteArraySet虽然实现存储结构是CopyOnWriteArrayList,但他保留了Hashset的去重结构,在add的时候使用了AddIfAbsent,因而输出的结果值为10000。

【JUC基础】11. 并发下的集合类

要验证这个结果其实也很简单,我们把hashSet.add()中的值,改为不重复的,比如使用雪花id来填充:

public class HashSetTest {

    static Set<String> hashSet = new CopyOnWriteArraySet<>();
    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            hashSet.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + hashSet.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                hashSet.add(IdUtil.getSnowflakeNextIdStr());
            }
        }
    }
}

那么结果就是我们想要的20000了:

【JUC基础】11. 并发下的集合类

4、并发下的HashMap

4.1、传统方式

public class HashMapTest {

    static Map<String, Object> hashMap = new HashMap<>();

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            hashMap.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + hashMap.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                hashMap.put(IdUtil.getSnowflakeNextIdStr(), i);
            }
        }
    }

}

运行结果:

【JUC基础】11. 并发下的集合类

同样也存在线程安全问题。与ArrayList类似,当然也存在加锁的方式。同样采用JDK提供的方式:

Collections.synchronizedMap(new HashMap<>());

4.2、ConcurrentHashMap

与CopyOnWriteArrayList或者set类似,JUC也提供了线程安全的Map集合。只是换个了名字:ConcurrentHashMap。

【JUC基础】11. 并发下的集合类

改造后代码:

public class HashMapTest {

    static Map<String, Object> hashMap = new ConcurrentHashMap<>();

    public static void main(String[] args) throws InterruptedException {
        // 这里执行10次,对比10次结果
        for (int i = 0; i < 10; i++) {
            // 每次执行前将list清空
            hashMap.clear();

            // 创建两个线程,分别往list里面存放数据,每个线程存放10000个
            Thread thread1 = new Thread(new MyThread());
            Thread thread2 = new Thread(new MyThread());
            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            // 当两个线程执行完毕后,期望值应该是list会扩容到20000个,并且打印list.sizes()=20000
            System.out.println("最终集合数量:" + hashMap.size());
        }
    }

    // 操作集合list线程
    static class MyThread implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 10000; i++) {
                hashMap.put(IdUtil.getSnowflakeNextIdStr(), i);
            }
        }
    }

}

运行结果:

【JUC基础】11. 并发下的集合类

4.3、ConcurrentHashMap底层结构

那么JUC为什么不叫CopyOnWriteHashMap,而改名叫ConcurrentHashMap呢?因为他们两者的实现方式完全不一样。 前面讲到CopyOnWriteArrayList是采用复制快照的方式,实现类似读写分离的方式来确保数值不会被覆盖。

而ConcurrentHashMap却采用了分段锁的机制来确保线程安全。具体的后面专门来讲。这里只需要记住ConcurrentHashMap是可以保证线程安全即可。

可以初步看到源码中采用了分段,并添加了synchronized同步块代码,来确保高性能下的线程安全。

【JUC基础】11. 并发下的集合类

5、小结

学到这里,我们发现java下的集合类大部分都不是线程安全的。而为了确保线程安全,我们可以采取多种措施,包括JDK也提供了多种方式来确保集合在多线程中的线程安全问题。而很多时候,因为集合线程不安全导致的问题是很隐蔽的,如上述示例代码所示,并不会每次都显式的抛出异常信息,只是会让你每次的结果不一致,而每次运行结果未必都会复现。所以针对此类问题,需要谨慎对待。文章来源地址https://www.toymoban.com/news/detail-470484.html

到了这里,关于【JUC基础】11. 并发下的集合类的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

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

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

相关文章

  • JUC并发编程——集合类不安全及Callable(基于狂神说的学习笔记)

    List不安全 CopyOnWriteArrayList与vector对比,以下来自CSDN智能助手的回答: Java中的CopyOnWriteArrayList和Vector都是线程安全的动态数组,可以在多线程环境下使用。 CopyOnWriteArrayList使用了一种特殊的写时复制机制,它在对数组进行修改时,会创建一个新的副本,而不是直接在原数组上

    2024年02月07日
    浏览(54)
  • JUC 高并发编程基础篇

    • 1、什么是 JUC • 2、Lock 接口 • 3、线程间通信 • 4、集合的线程安全 • 5、多线程锁 • 6、Callable 接口 • 7、JUC 三大辅助类: CountDownLatch CyclicBarrier Semaphore • 8、读写锁: ReentrantReadWriteLock • 9、阻塞队列 • 10、ThreadPool 线程池 • 11、Fork/Join 框架 • 12、CompletableFuture 1 什么

    2024年02月07日
    浏览(49)
  • java基础 -02java集合之 List,AbstractList,ArrayList介绍

    在正式List之前,我们先了解我们补充上篇Collection接口的拓展实现,也就是说当我我们需要实现一个不可修改的Collection的时候,我们只需要拓展某个类,也就是AbstractCollection这个类,他是Collection接口的骨干实现,并以最大限度的实现了减少此接口所需要的工作; 如上两图进行

    2024年01月20日
    浏览(41)
  • 【并发编程】JUC并发编程(彻底搞懂JUC)

    如果你对多线程没什么了解,那么从入门模块开始。 如果你已经入门了多线程(知道基础的线程创建、死锁、synchronized、lock等),那么从juc模块开始。 新手学技术、老手学体系,高手学格局。 JUC实际上就是我们对于jdk中java.util .concurrent 工具包的简称 ,其结构如下: 这个包

    2024年02月20日
    浏览(48)
  • 《Git入门实践教程》前言+目录

    版本控制系统(VCS)在项目开发中异常重要,但和在校大学生的交流中知道,这个重要方向并未受到重视。具备这一技能,既是项目开发能力的体现,也可为各种面试加码。在学习体验后知道,Git多样化平台、多种操作方式、丰富的资源为业内人士提供了方便的同时,也造成

    2024年02月10日
    浏览(66)
  • JUC集合类

    在 Java 并发编程中,JUC(Java Util Concurrent)包提供了一些并发安全的集合类,用于在多线程环境下进行共享数据的操作,以解决多线程间的竞争条件和线程安全问题。以下是一些常见的 JUC 集合类: ConcurrentHashMap:线程安全的哈希表,支持高并发的读写操作,适合在多线程环境

    2023年04月15日
    浏览(30)
  • FPGA学习实践之旅——前言及目录

    很早就有在博客中记录技术细节,分享一些自己体会的想法,拖着拖着也就到了现在。毕业至今已经半年有余,随着项目越来越深入,感觉可以慢慢进行总结工作了。趁着2024伊始,就先开个头吧,这篇博客暂时作为汇总篇,记录在这几个月以及之后从FPGA初学者到也算有一定

    2024年02月03日
    浏览(55)
  • 第九章 JUC并发编程

    http://t.csdn.cn/UgzQi 使用 AQS加 Lock 接口实现简单的不可重入锁 早期程序员会自己通过一种同步器去实现另一种相近的同步器,例如用可重入锁去实现信号量,或反之。这显然不够优雅,于是在 JSR166(java 规范提案)中创建了 AQS,提供了这种通用的同步器机制。 AQS 要实现的功能

    2023年04月08日
    浏览(37)
  • JUC并发编程(二)

    JUC并发编程(续) 接上一篇笔记:https://blog.csdn.net/weixin_44780078/article/details/130694996 五、Java内存模型 JMM 即 Java Memory Model,它定义了主存、工作内存抽象概念,底层对应着CPU寄存器、缓存、硬件内存、CPU 指令优化等。 JMM 体现在以下几个方面: 原子性:保证指令不会受到线程

    2024年02月05日
    浏览(92)
  • 【JUC并发编程】

    本笔记内容为狂神说JUC并发编程部分 目录 一、什么是JUC 二、线程和进程 1、概述  2、并发、并行 3、线程有几个状态  4、wait/sleep 区别 三、Lock锁(重点)  四、生产者和消费者问题 五、八锁现象 六、集合类不安全  七、Callable ( 简单 ) 八、常用的辅助类(必会) 1、CountDown

    2024年02月09日
    浏览(38)

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

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

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

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

二维码1

领取红包

二维码2

领红包