前言:CopyOnWriteArrayList是ArrayList的线程安全版本,在写入时会copy一份数据,然后写完再设置成新的数据。适用于读多写少的并发场景
▎COW
CopyOnWrite 简称COW,写入时复制,是计算机程序设计领域的一种优化策略。
核心思想:
多个调用者共同去访问一个资源(指向同一个读指针),如果有人试图修改资源的内容,系统会复制一份专用副本给该调用者,去修改这个副本,而对于其他人来说访问的资源还是原来的,不会发生变化。
原理:
COW的处理过程中需维持一个为读请求使用的“指针”,并在新数据写入完成后,更新这个指针以提升读写并发能力。因此,COW也间接提供了数据更新过程中的原子性。在保证数据的完整性同时还保证了一定的读写效率
▎案例分析:
多线程环境对list进行写操作,会引发 java.util.ConcurrentModificationException 并发修改异常!
public static void main(String[] args) {
/**解决方案:
1.使用Vector,底层add源码增加了锁 public synchronized boolean add(E e){...}
2.使用Collections.synchronizedList(list) 将其包装成一个线程安全的List
3.使用CopyOnWriteArrayList,底层使用了可重入锁,保证并发完全 */
//List<String> list = new ArrayList<>(); 报错:并发修改异常
//List<String> list = new Vector<>();
//List<String> list = Collections.synchronizedList(new ArrayList<>());
List<String> list = new CopyOnWriteArrayList<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
list.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(list);
},String.valueOf(i)).start();
}
}
▎结果分析
ArrayList 之所以存在并发异常,是底层没有做任何锁的操作
① Vector 在add操作时,加入了synchronized锁,保证并发安全
② Collections.synchronizedList (mutex) 使用的是synchronized代码块对mutex对象加锁,指定锁定的对象
③ CopyOnWriteArrayList 加入了可重入锁,保证并发安全
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 上锁,只允许一个线程进入
lock.lock();
try {
// 获得当前数组对象
Object[] elements = getArray();
int len = elements.length;
// 1. 旧数组元素拷贝到一个新的数组中,新数组的长度是:旧数组长度+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 2. 追加元素到新数组末尾(下标是从0开始)。上述新数组的长度的最后一个下标=新元素
newElements[len] = e;
// 3. 指向新数组
setArray(newElements);
return true;
} finally {
// 释放锁
lock.unlock();
}
}
▎CopyOnWriteArraySet
CopyOnWriteArrayList 与 CopyOnWriteArraySet 同理,就不多展示了
public static void main(String[] args) {
// Set<String> set = new HashSet<>();
// Set<String> set = Collections.synchronizedSet(new HashSet<>());
Set<String> set = new CopyOnWriteArraySet<>();
for (int i = 0; i < 10; i++) {
new Thread(()->{
set.add(UUID.randomUUID().toString().substring(0,5));
System.out.println(set);
},String.valueOf(i)).start();
}
}
Set 原理
// Set的底层实际上是HashMap
public HashSet() {
map = new HashMap<>();
}
// 使用 HashMap 的 key 保存 HashSet 中所有元素
public boolean add(E e) {
return map.put(e, PRESENT)==null; //PRESENT是固定值,目的是确保key不重复
}
▎总结
HashSet本质是在内部维护一个HashMap对象,所有数据交给HashMap处理,LinkedHashSet是HashSet的子类,也是一个空壳,构造方法都调用HashSet的一个default构造方法,LinkedHashSet也是对LinkedHashMap包装了一层,TreeSet也是基于TreeMap实现的
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
☞ 常见问题
一、Vector 和 CopyOnWriteArrayList 区别?
- Vector 每个方法都加了锁,CopyOnWriteArrayList 读操作未加锁,读性能高于Vector
- Vector 每次扩容的大小都是原数组的2倍,CopyOnWriteArrayList无需扩容,通过cow思想满足容量要求
二、HashSet的特点:
- 无序的
- 不允许元素重复
- 最多只有一个null值
- 不是同步的,不安全