前言
在电商微服务项目中,多线程并发访问共享数据时,可能会出现并发问题导致程序崩溃、数据异常等情况。为了避免这些问题,Java中提供了多种并发控制方法,其中Copy-On-Write(COW)机制就是一种常用的技术。本文将详细介绍COW机制的概念、如何保证线程安全、相对于锁机制的优势。
一、概念:
1、什么是Copy on Write (COW)
Copy on Write (COW) 是一种在并发编程中常用的技术,它可以在不使用锁的情况下,实现对共享数据的并发访问。在 Java 中,Copy on Write 通常用于对 List、Map 等集合进行并发访问。
2、Copy on Write (COW)实现思想
Copy on Write 的基本思想是,当需要修改某个共享数据时,先将原始数据复制一份,并在副本上进行修改,修改完成后再将副本的引用赋值给原始数据的引用。当一个程序退出时,它所修改的部分会被保留下来,其他程序仍然可以访问它们。这样做的好处是,当多个线程同时读取共享数据时,它们各自持有原始数据的引用,不会发生互相干扰的情况;而在需要修改共享数据时,只有一个线程在修改,其他线程在读取,因此也不需要加锁。
3、Copy on Write (COW)优缺点
COW技术的主要优点是节省内存和提高效率。由于只有一个副本需要被修改,所以它比传统的复制算法更节省内存。此外,由于其他程序可以继续访问原始的共享内存,所以COW技术也提高了程序的效率。
COW技术的主要缺点是可能会导致数据不一致的问题。如果一个程序修改了共享内存,但另一个程序没有正确地复制该内存区域,那么后者可能会读取到错误的数据。为了解决这个问题,通常需要使用一些同步机制来确保所有程序都正确地处理共享内存。
4、COW如何保证线程安全
COW机制通过避免加锁、延迟复制等策略来保证线程安全。它的基本流程如下:
- 当需要对共享数据进行写操作时,先将原数据进行拷贝;
- 在新的数据副本上执行写操作;
- 写完后,利用原子操作将旧的数据结构替换为新的数据结构。
由于多个线程在修改时对应不同的数据副本,因此不会出现并发访问共享数据的情况。当然,这也意味着每次修改都会创建一个新的数组,可能会带来空间和时间的开销。
5、COW相对锁机制有什么优势
相对于传统的锁机制,COW机制有以下优势:
- 读操作无需加锁,提高了读取效率;
- 可以避免锁竞争、死锁等问题,提高了程序的稳定性;
- 可以避免多个线程同时读取同一份数据而引起的并发问题。
- 但是,需要明确的是,COW机制不适用于经常进行写操作、数据量较大的场景。
二、CopyOnWrite容器
从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。CopyOnWrite容器非常有用,可以在非常多的并发场景中使用到。
1、什么是CopyOnWrite容器
CopyOnWrite容器即写时复制的容器。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以CopyOnWrite容器也是一种读写分离的思想,读和写不同的容器。
2、CopyOnWriteArrayList的实现原理
- 先来看一下CopyOnWriteArrayList 的源码究竟是如何实现的。
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();
}
}
- 可以发现在添加的时候是需要加锁的,否则多线程写的时候会Copy出N个副本出来。
public E get(int index) {
return get(getArray(), index);
}
读的时候不需要加锁,如果读的时候有多个线程正在向ArrayList添加数据,读还是会读到旧的数据,因为写的时候不会锁住旧的ArrayList。
3、CopyOnWriteArraySet容器
CopyOnWriteArraySet基于CopyOnWriteArrayList实现,其唯一的不同是在add时调用的是CopyOnWriteArrayList的addIfAbsent(若没有则增加)方法
它是线程安全的无序的集合,可以将它理解成线程安全的HashSet。有意思的是,CopyOnWriteArraySet和HashSet虽然都继承于共同的父类AbstractSet;但是,HashSet是通过“散列表(HashMap)”实现的,而CopyOnWriteArraySet则是通过“动态数组(CopyOnWriteArrayList)”实现的,并不是散列表。
CopyOnWriteArraySet示例
- 下面,我们通过一个例子去对比HashSet和CopyOnWriteArraySet。
import java.util.*;
import java.util.concurrent.*;
/*
* CopyOnWriteArraySet是“线程安全”的集合,而HashSet是非线程安全的。
* 下面是“多个线程同时操作并且遍历集合set”的示例
* (01) 当set是CopyOnWriteArraySet对象时,程序能正常运行。
* (02) 当set是HashSet对象时,程序会产生ConcurrentModificationException异常。
*/
public class CopyOnWriteArraySetTest1 {
// TODO: set是HashSet对象时,程序会出错。
//private static Set<String> set = new HashSet<String>();
private static Set<String> set = new CopyOnWriteArraySet<String>();
public static void main(String[] args) {
// 同时启动两个线程对set进行操作!
new MyThread("ta").start();
new MyThread("tb").start();
}
private static void printAll() {
String value = null;
Iterator iter = set.iterator();
while(iter.hasNext()) {
value = (String)iter.next();
System.out.print(value+", ");
}
System.out.println();
}
private static class MyThread extends Thread {
MyThread(String name) {
super(name);
}
@Override
public void run() {
int i = 0;
while (i++ < 10) {
// “线程名” + "-" + "序号"
String val = Thread.currentThread().getName() + "-" + (i%6);
set.add(val);
// 通过“Iterator”遍历set。
printAll();
}
}
}
}
结果说明:
由于set是集合对象,因此它不会包含重复的元素。
如果将源码中的set改成HashSet对象时,程序会产生ConcurrentModificationException异常。
4、自定义CopyOnWriteMap
JDK 没有提供自己的 CopyOnWriteMap 我们可以自己实现自己的 CopyOnWriteMap
public class CopyOnWriteMap<K,V> implements Map<K,V>,Cloneable {
private volatile Map<K,V> internalMap;
public CopyOnWriteMap() {
internalMap = new HashMap<K, V>();
}
public V put(K key, V value) {
synchronized (this) {
Map<K,V> newMap = new HashMap<K,V>(internalMap);
V val =newMap.put(key, value);
internalMap = newMap;
return val;
}
}
public void putAll(Map<? extends K, ? extends V> m) {
synchronized (this) {
Map<K,V> newMap = new HashMap<K,V>(internalMap);
newMap.putAll(m);
internalMap = newMap;
}
}
public V get(Object key) {
return internalMap.get(key);
}
}
三、CopyOnWrite的应用场景
CopyOnWrite并发容器用于读多写少的并发场景。比如白名单,黑名单,商品类目的访问和更新场景,假如我们有一个搜索网站,用户在这个网站的搜索框中,输入关键字搜索内容,但是某些关键字不允许被搜索。这些不能被搜索的关键字会被放在一个黑名单当中,黑名单每天晚上更新一次。当用户搜索时,会检查当前关键字在不在黑名单当中,如果在,则提示不能搜索。实现代码如下:
import java.util.Map;
import com.koo.my.CopyOnWriteMap;
/**
* 黑名单服务
*
*
*/
public class BlackListServiceImpl {
private static CopyOnWriteMap<String, Boolean> blackListMap = new CopyOnWriteMap<String, Boolean>(
1000);
public static boolean isBlackList(String id) {
return blackListMap.get(id) == null ? false : true;
}
public static void addBlackList(String id) {
blackListMap.put(id, Boolean.TRUE);
}
/**
* 批量添加黑名单
*
* @param ids
*/
public static void addBlackList(Map<String,Boolean> ids) {
blackListMap.putAll(ids);
}
}
代码很简单,但是使用CopyOnWriteMap需要注意两件事情:
-
减少扩容开销。根据实际需要,初始化CopyOnWriteMap的大小,避免写时CopyOnWriteMap扩容的开销。
-
使用批量添加。因为每次添加,容器每次都会进行复制,所以减少添加次数,可以减少容器的复制次数。如使用上面代码里的addBlackList方法。
源码下载:
gitee.com/charlinchenlin/koo-erp