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