JUC并发编程学习(五)集合类不安全
集合类不安全
List不安全
单线程情况下集合类和很多其他的类都是安全的,因为同一时间只有一个线程在对他们进行修改,但是如果是多线程情况下,那么集合类就不一定是安全的,可能会出现一条线程正在修改的同时另一条线程启动来对这个集合进行修改,这种情况下就会导致发生并发修改异常(在jdk11的环境下多次测试该代码发现并无问题,但是学习教程中有该异常。原因:线程数量不够)
package org.example.unsafe;
import java.util.ArrayList;
import java.util.UUID;
public class Test1 {
public static void main(String[] args) {
ArrayList<String> sts = new ArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
sts.add(UUID.randomUUID().toString().substring(0, 5));
System.out.println(sts);
},String.valueOf(i)).start();
}
}
}
重现该异常,通过for循环开更多线程
package org.example.unsafe;
import java.util.ArrayList;
import java.util.UUID;
public class Test1 {
public static void main(String[] args) {
MidiFireList midiFireList = new MidiFireList();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
midiFireList.midi();
}, "A").start();
}
for (int i = 0; i < 10; i++) {
new Thread(() -> {
midiFireList.midi();
}, "B").start();
}
for (int i = 0; i < 10; i++) {
new Thread(() -> {
midiFireList.midi();
}, "C").start();
}
}
}
class MidiFireList {
ArrayList<String> sts = new ArrayList<>();
public void midi() {
sts.add(Thread.currentThread() + ":" + UUID.randomUUID().toString().substring(0, 5));
System.out.println(sts);
}
}
成功重现异常
解决List的并发修改异常
1、通过使用List的子类Vector来操作,Vector默认时线程安全的,所以不会出现以上情况,Vector时jdk1.0时期就出现的,它的add方法使用了synchronized关键字来保证线程安全。
class MidiFireList {
//使用了线程安全的Vector集合类
List<String> sts = new Vector<>();
public void midi() {
sts.add(Thread.currentThread() + ":" + UUID.randomUUID().toString().substring(0, 5));
System.out.println(sts);
}
}
2、通过所有集合的父类Collections类的线程安全的方法创建一个ArraryList。
class MidiFireList {
List<String> sts = Collections.synchronizedList(new ArrayList<String>());
public void midi() {
sts.add(Thread.currentThread() + ":" + UUID.randomUUID().toString().substring(0, 5));
System.out.println(sts);
}
}
3、通过JUC包下的CopyOnWriteArrayList类来创建一个ArrayList,他内部的方法通过同步代码块和lock锁实现了线程安全的各种操作,缺点时少量线程操作时成本太高(CopyOnWrite写入时复制,COW思想,是计算机程序设计领域中的一种优化策略),在写入时复制一份,避免覆盖导致数据问题,读写分离思想
CopyOnWriteArrayList和Vector的在线程安全方面的区别,为什么要用CopyOnWriteArrayList
CopyOnWriteArrayList对比Vector,我们可以通过源码来看
CopyOnWriteArrayList:
Vector:
jdk1.8时的CopyOnWriteArrayList:
其实在jdk11之后的区别只在于同步代码块和同步方法的区别,可参考同步代码块和同步方法有什么区别 • Worktile社区和瞄一眼CopyOnWriteArrayList(jdk11) - 傅晓芸 - 博客园 (cnblogs.com)这两篇文章。
但是在jdk1.8时,CopyOnWriteArrayList的方法时单纯的通过Lock锁来实现同步的,没有使用synchronized关键字,因为会影响性能。
Set不安全
Set的不安全问题与List一样,解决方案如下
1、通过Collections的同步方法来创建一个线程安全的Set
class MidiFireList {
Set<String> set = Collections.synchronizedSet(new HashSet<>());
public void midi() {
set.add(Thread.currentThread() + ":" + UUID.randomUUID().toString().substring(0, 5));
System.out.println(set);
}
}
2、通过CopyOnWriteArraySet类来创建线程安全的Set
class MidiFireList {
Set<String> set = new CopyOnWriteArraySet<>();
public void midi() {
set.add(Thread.currentThread() + ":" + UUID.randomUUID().toString().substring(0, 5));
System.out.println(set);
}
}
HashSet的底层就是HashMap,他就不是一个新的东西
HashSet的add方法就时HashMap的put方法封装了一下
map的key是无法重复的,所以HashSet是无序的
Map不安全
Map解决方案
package org.example.unsafe;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class MapTest {
public static void main(String[] args) {
// HashMap是这样用的吗?不是工作中不用HashMap
// 默认等价于什么? new HashMap<>(16,0.75);
Map<String, String> map = new ConcurrentHashMap<>();
// 加载因子、初始化容量
for (int i = 0; i < 50; i++) {
new Thread(()->{
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 5));
System.out.println(map);
},String.valueOf(i)).start();
}
}
}
注意Map的并发类为ConcurrentHashMap