单线程环境下
单线程环境的 ArrayList 是不会有问题的
public class ArrayListNotSafeDemo {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
for(String element : list) {
System.out.println(element);
}
}
}
多线程环境
为什么 ArrayList 是线程不安全的 ?因为在进行写操作的时候,方法上为了保证并发性,是没有添加 synchronized 修饰,所以并发写的时候,就会出现问题 。
ArrayList 不安全的案列
当我们同时启动 100个线程去操作 List 的时候
public class Demo5 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
for(int i = 0;i < 100;i++){
new Thread(() ->{
//往集合中添加元素【add 方法并未 synchronized 修饰】
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
}
这个时候出现了错误,也就是java.util.ConcurrentModificationException
[ 并发修改的异常 ]
导致原因
并发修改导致:一个人正在写入,另一个人过来抢夺,导致数据不一致异常 !
解决方案
方案一:Vector
第一种方法,就是不用 ArrayList 这种不安全的 List 实现类,而采用 Vector,线程安全的
关于 Vector 如何实现线程安全的,而是在方法上加了锁,即synchronized
这样就每次只能够一个线程进行操作,所以不会出现线程不安全的问题,但是因为加锁了,导致并发性下降 。
方案二:Collections.synchronizedList()
List<String> list = Collections.synchronizedList(new ArrayList<>());
采用 Collections 集合工具类,在 ArrayList 外面包装一层 同步 机制 。
方案三:使用 JUC 工具类中的 CopyOnWriteArrayList 类
CopyOnWriteArrayList:写时复制,主要是一种读写分离的思想
CopyOnWrite 容器即写时复制的容器。往一个容器添加元素的时候,不会直接往当前容器添加,而是现将当前容器 Object [ ] 进行 Copy ,复制出一个新的容器 Object [ ] ,然后再往新的容器中添加元素,添加完元素后,再将原容器的引用指向新的容器;这样做的好处是可以对 CopyOnWrite 容器进行并发读,而不需要加锁,因为当前容器并不需要添加元素。所以 CopyOnWrite 容器也是一种读写分离的思想!
就是写的时候,把 ArrayList 扩容一个出来,然后把值填写上去,在通知其他的线程,ArrayList 的引用指向扩容后的
查看底层 add 方法源码
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
首先需要加锁
final ReentrantLock lock = this.lock;
lock.lock();
然后在末尾扩容一个单位
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
然后在把扩容后的空间,填写上需要 add 的内容
newElements[len] = e;
最后把内容 set 到 Array 中
HashSet 线程不安全
异常类型
线程不安全。并发情况下也会发生 ConcurrentModificationException
异常;
解决方案
方案一:Collections.synchronizedSet(new HashSet<>())
方案二:使用 JUC 工具类中的 CopyOnWriteArraySet 类
底层还是使用 CopyOnWriteArrayList 进行实例化
补充:
同理 HashSet 的底层结构就是 HashMap
思考: 但是为什么我调用 HashSet.add() 的方法,只需要传递一个元素,而 HashMap 是需要传递 key-value 键值对 ?
首先我们查看 hashSet 的 add 方法
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
我们能发现但我们调用 add 的时候,存储一个值进入map中,只是作为key进行存储,而 value 存储的是一个Object 类型的常量,也就是说 HashSet 只关心key,而不关心 value 。
HashMap 线程不安全案列
同理 HashMap 在多线程环境下,也是不安全的,并发情况下也会发生 ConcurrentModificationException
异常;
public static void main(String[] args) {
Map<String, String> map = new HashMap<>();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
map.put(Thread.currentThread().getName(), UUID.randomUUID().toString().substring(0, 8));
System.out.println(map);
}, String.valueOf(i)).start();
}
}
解决方案
方案一:使用 HashTable
与 Vector 类似,属于 HashMap 线程安全的实现类,里面方面同样加了 synchronized
修饰,效率较低 。
方案二:Collections.synchronizedMap(new HashMap<> ())
方案三:使用 JUC 工具类下的 ConcurrentHashMap
Map<String, String> map = new ConcurrentHashMap<>();