前言
Java多线程编程是现代软件开发的重要组成部分,然而,多线程环境下数据的安全性一直是一个棘手的问题。本文将探讨如何通过线程安全集合来解决这一挑战。我们将深入研究Java中的Concurrent包,介绍诸如ConcurrentHashMap、CopyOnWriteArrayList等强大的数据结构,它们为多线程应用提供了高效的数据管理方式。无论您是初学者还是有经验的开发人员,都将从本文中学到如何确保数据在多线程环境下的安全性,为您的Java多线程应用程序打造坚实的基础。
线程安全集合
CopyOnWriteArrayList重点
- 线程安全的ArrayList,加强版读写分离。
- 写有锁,读无锁,读写之间不阻塞,优于读写锁。
- 写入时,先copy一个容器副本、再添加新元素,最后替换引用。
- 使用方式与ArrayList无异。
示例:
public class TestCopyOnWriteArrayList { public static void main(String[] args) { //1创建集合 CopyOnWriteArrayList<String> list=new CopyOnWriteArrayList<>(); //2使用多线程操作 ExecutorService es=Executors.newFixedThreadPool(5); //3提交任务 for(int i=0;i<5;i++) { es.submit(new Runnable() { @Override public void run() { for(int j=0;j<10;j++) { list.add(Thread.currentThread().getName()+"...."+new Random().nextInt(1000)); } } }); } //4关闭线程池 es.shutdown(); while(!es.isTerminated()) {} //5打印结果 System.out.println("元素个数:"+list.size()); for (String string : list) { System.out.println(string); } } }
CopyOnWriteArrayList如何做到线程安全的
CopyOnWriteArrayList使用了一种叫写时复制的方法,当有新元素添加到CopyOnWriteArrayList时,先从原有的数组中拷贝一份出来,然后在新的数组做写操作,写完之后,再将原来的数组引用指向到新数组。
当有新元素加入的时候,如下图,创建新数组,并往新数组中加入一个新元素,这个时候,array这个引用仍然是指向原数组的。
当元素在新数组添加成功后,将array这个引用指向新数组。
CopyOnWriteArrayList
的整个add操作都是在锁的保护下进行的。 这样做是为了避免在多线程并发add的时候,复制出多个副本出来,把数据搞乱了,导致最终的数组数据不是我们期望的。
CopyOnWriteArrayList
的add
操作的源代码如下:
public boolean add(E e) { //1、先加锁 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; //2、拷贝数组 Object[] newElements = Arrays.copyOf(elements, len + 1); //3、将元素加入到新数组中 newElements[len] = e; //4、将array引用指向到新数组 setArray(newElements); return true; } finally { //5、解锁 lock.unlock(); } }
由于所有的写操作都是在新数组进行的,这个时候如果有线程并发的写,则通过锁来控制,如果有线程并发的读,则分几种情况:
1、如果写操作未完成,那么直接读取原数组的数据;
2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据。
可见,CopyOnWriteArrayList
的读操作是可以不用加锁的。
CopyOnWriteArraySet
示例:
public class TestCopyOnWriteArraySet { public static void main(String[] args) { //1创建集合 CopyOnWriteArraySet<String> set=new CopyOnWriteArraySet<>(); //2添加元素 set.add("pingguo"); set.add("huawei"); set.add("xiaomi"); set.add("lianxiang"); set.add("pingguo"); //3打印 System.out.println("元素个数:"+set.size()); System.out.println(set.toString()); } }
ConcurrentHashMap重点
- 初始容量默认为16段(Segment),使用分段锁设计。
- 不对整个Map加锁,而是为每个Segment加锁。
- 当多个对象存入同一个Segment时,才需要互斥。
- 最理想状态为16个对象分别存入16个Segment,并行数量16。
- 使用方式与HashMap无异。
注意⚠️: ConcurrentHashMap jdk1.8之前才是分段锁,1.8之后没有使用分段锁了,而是乐观锁加Synchronized,这里注意一下
示例:
public class TestConcurrentHashMap { public static void main(String[] args) { //1创建集合 ConcurrentHashMap<String, String> hashMap=new ConcurrentHashMap<String, String>(); //2使用多线程添加数据 for(int i=0;i<5;i++) { new Thread(new Runnable() { @Override public void run() { for(int k=0;k<10;k++) { hashMap.put(Thread.currentThread().getName()+"--"+k, k+""); System.out.println(hashMap); } } }).start(); } } }
最后
本期结束咱们下次再见👋~
🌊 关注我不迷路,如果本篇文章对你有所帮助,或者你有什么疑问,欢迎在评论区留言,我一般看到都会回复的。大家点赞支持一下哟~ 💗