JAVA多线程-集合类线程不安全问题
2022/1/27 22:34:31
本文主要是介绍JAVA多线程-集合类线程不安全问题,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!
ArrayList线程不安全
案例
package JUC; import java.util.ArrayList; import java.util.List; import java.util.UUID; /** * @author zhaolimin * @date 2021/11/13 * @apiNote ArrayList类不安全测试 */ public class ArrayListNotSafeDemo { public static void main(String[] args) { List<String> list = new ArrayList<>(); for (int i = 0; i < 50; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); }, String.valueOf(i)).start(); } // java.util.ConcurrentModificationException 出现了快速失败机制 } }
故障现象
- 抛出 java.util.ConcurrentModificationException 出现了并发修改期望值与修改值不同,快速失败机制。
故障原因
- 多线程添加操作下,出现了快速失败机制。
- 原因是add方法对于ArrayList来说是线程不安全的。
- 并发争抢修改导致的问题,一个线程正在写,另一个线程抢夺,导致数据不一致问题。
解决方案
将ArrayList类换成Vector类
- 可以解决,但是并发性急剧下降。
public static void main(String[] args) { List<String> list = new Vector<>(); for (int i = 0; i < 50; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); }, String.valueOf(i)).start(); } }
运用集合工具类Collections
Collections.synchronizedList方法
- 可以解决,但是并发性急剧下降。
public static void main(String[] args) { List<String> list = Collections.synchronizedList(new ArrayList<>()); for (int i = 0; i < 50; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); }, String.valueOf(i)).start(); } }
运用JUC下的CopyOnWriteArrayList类(重点)
- 主要用于读多写少的场景。
- 写时复制:
- 读的是原数组,锁的是副本。
- 一般来说读写不能并发,但使用写时复制的话,就允许了,提高了并发能力。
- 缺点:
- 典型的空间换取时间。
- 会产生大量无效的对象引用碎片。
public static void main(String[] args) { List<String> list = new CopyOnWriteArrayList<>(); for (int i = 0; i < 50; i++) { new Thread(() -> { list.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(list); }, String.valueOf(i)).start(); } }
CopyOnWriteArrayList类核心内容选读
- 与添加有关的方法
/** 追加一个特殊的元素到这个List集合的尾部。 */ public boolean add(E e) { // 用 ReentrantLock 加锁 final ReentrantLock lock = this.lock; lock.lock(); // 注:JDK9以后已经改为 synchronized 加锁了。 try { // 拿到旧表 Object[] elements = getArray(); // 拿到旧表的长度 int len = elements.length; // 在旧表的基础上拷贝并长度 + 1 新表 Object[] newElements = Arrays.copyOf(elements, len + 1); // 在新表的末尾追加要添加的元素 newElements[len] = e; // 将当前表设置为新表 setArray(newElements); // 返回添加成功 return true; } finally { // 解锁 lock.unlock(); } }
HashSet线程不安全
案例
package JUC; import java.util.HashSet; import java.util.UUID; /** * @author zhaolimin * @date 2021/11/14 * @apiNote HashSet线程不安全测试。 */ public class HashSetNotSafeDemo { public static void main(String[] args) { HashSet<String> strings = new HashSet<>(); for (int i = 0; i < 50; i++) { new Thread(() -> { strings.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(strings); }, String.valueOf(i)).start(); } } }
故障现象
- 抛出 java.util.ConcurrentModificationException 出现了并发修改异常,快速失败机制。
故障原因
- 期望修改次数与真实修改次数值不同。
- 多线程添加操作下,出现了快速失败机制。
- 原因是add方法对于HashSet来说是线程不安全的。
- 并发争抢修改导致的问题,一个线程正在写,另一个线程抢夺,导致数据不一致问题。
解决方案
运用集合工具类Collecitons
Collections.synchronizedSet方法
- 线程安全但是并发性下降。
public static void main(String[] args) { Set<String> strings = Collections.synchronizedSet( new HashSet<>()); for (int i = 0; i < 50; i++) { new Thread(() -> { strings.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(strings); }, String.valueOf(i)).start(); } }
运用JUC下的CopyOnWriteArraySet类(重点)
- 同样是使用了写时复制类。
public static void main(String[] args) { CopyOnWriteArraySet<String> strings = new CopyOnWriteArraySet<>(); for (int i = 0; i < 50; i++) { new Thread(() -> { strings.add(UUID.randomUUID().toString().substring(0,8)); System.out.println(strings); }, String.valueOf(i)).start(); } }
CopyOnWriteArraySet类核心内容选读
- 属性
private final CopyOnWriteArrayList<E> al; // 可以看到数据结构其实是一个 CopyOnWriteArrayList 类的对象 // 让我联想到了 HashSet 和 HashMap 的关系。 public CopyOnWriteArraySet() { // 调用的是 CopyOnWriteArrayList 的构造函数 al = new CopyOnWriteArrayList<E>(); }
HashMap线程不安全
案例
package JUC; import java.util.*; /** * @author zhaolimin * @date 2021/11/14 * @apiNote HashMap线程不安全 */ public class HashMapNotSafeDemo { public static void main(String[] args) { HashMap<String, String> map = new HashMap<>(); for (int i = 0; i < 50; i++) { new Thread(() -> { map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,8)); System.out.println(map); }, String.valueOf(i)).start(); } } }
故障现象
- 抛出 java.util.ConcurrentModificationException 出现了并发修改异常,快速失败机制。
故障原因
- 期望修改次数与真实修改次数值不同。
- 多线程添加操作下,出现了快速失败机制。
- 原因是add方法对于HashSet来说是线程不安全的。
- 并发争抢修改导致的问题,一个线程正在写,另一个线程抢夺,导致数据不一致问题。
解决方案
运用集合工具类Collecitons
public static void main(String[] args) { Map<Object, Object> objectObjectMap = Collections.synchronizedMap(new HashMap<>()); for (int i = 0; i < 40; i++) { new Thread(() -> { objectObjectMap.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,8)); System.out.println(objectObjectMap); }, String.valueOf(i)).start(); } }
运用JUC下的ConcurrentHashMap类(重点)
-
该类详解看之前的笔记。
public static void main(String[] args) { ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(); for (int i = 0; i < 40; i++) { new Thread(() -> { map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0,8)); System.out.println(map); }, String.valueOf(i)).start(); } }
传值问题 (重要基本功)
案例
- person类
public class Person { private Integer id; private String personName; public Person() { } public Person(String personName) { this.personName = personName; } public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getPersonName() { return personName; } public void setPersonName(String personName) { this.personName = personName; } }
- 测试类
public class TestTransferValueDemo { public void changeValue1(int age) { age = 30; } public void changeValue2(Person person) { person.setPersonName("zlm"); } public void changeValue3(String str) { str = "wl"; } // main线程在栈内存启动 public static void main(String[] args) { TestTransferValueDemo testTransferValueDemo = new TestTransferValueDemo(); // 基本数据类型 int age = 20; testTransferValueDemo.changeValue1(age); System.out.println("-------- age = " + age); Person person = new Person("xxx"); testTransferValueDemo.changeValue2(person); System.out.println("-------- personName = " + person.getPersonName()); String str = "xxx"; testTransferValueDemo.changeValue3(str); System.out.println("-------- str = " + str); } }
- 打印结果
/* -------- age = 20 -------- personName = zlm -------- str = xxx */
为什么会出现这样的结果
-
栈管运行,堆管存储。
-
栈空间是线程私有,堆的是数据共享。
-
age的结果:
- 是因为自始至终,基本数据类型都是传递的数据副本,副本改变根本影响不到原件的值,所谓的值传递。
-
person的结果:
- 传递的是引用,函数参数以及main中引用,指向的都是同一堆内存中的地址。
- 也就是对象在堆内存中的地址,修改了那个地址上的值。
-
str的结果:
- String 类型的 字符数组 是 final 修饰的,说明是一个不可更改的常量,只能新建。
- 对于 changeValue3 方法中的 str,它原本是和 main 中的 str 一样指向常量池里的 “xxx”。
- 但是 changeValue3 中的 str 又被强行指向了一个叫 “wl” 的字符串常量,但是在常量池里并没有这个字符串。
- 由于字符串是常量,不能修改只能新建。
- 于是常量池中会新建一个字符串常量 “wl” 然后 changeValue3 中的 str 将指向这个新建 “wl” 字符串。
- 由于 “wl” 字符串和 “xxx” 字符串在常量池中的地址不同,就导致了 main 中的 str 和 changeValue3 中的 str 不是指向同一个常量池地址。
- 而题中我们要打印的是 main 中的 str 所指的堆内存常量池中的内容为 “xxx”,而不是 “wl”,
- 所以我们打印的是 “xxx”。
- 对于
String str = new String("hello world");
运行期创建就存储在堆中。 - 对于
String str = "hello world;"
编译期创建的放在常量池中。
总结
- String类型特殊,需要单独看待。
- 引用类型复制的是地址。
- 对于基础类型的变量和常量:变量和引用存储在栈中,常量存储在常量池中。
码云仓库同步笔记,可自取欢迎各位star指正:https://gitee.com/noblegasesgoo/notes
如果出错希望评论区大佬互相讨论指正,维护社区健康大家一起出一份力,不能有容忍错误知识。 —————————————————————— 爱你们的 noblegasesgoo
这篇关于JAVA多线程-集合类线程不安全问题的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2025-01-08如何用关键链方法突破项目管理瓶颈?
- 2025-01-08电商人必看!6 款提升团队协作与客户满意度软件!
- 2025-01-08电商团队管理混乱?快用这 6 款软件优化协作流程!
- 2025-01-08短剧制作效率低?试试这5款任务管理工具
- 2025-01-08高效应对电商高峰,6 款团队协作软件大揭秘!
- 2025-01-08为什么外贸人都爱上了在线协作工具?
- 2025-01-08提升工作效率,从这些任务管理工具开始
- 2025-01-08新年电商订单暴增,必备的 6 款可视化协作办公软件有哪些?
- 2025-01-08短剧制作经理必备技能与工具全解析
- 2025-01-08在线协作让年货大促轻松应对!