「java.util.concurrent并发包5」之 CopyOnWrite
一 概述
Copy-On-Write即写时复制的容器,是一种用于程序设计中的优化策略。通俗的理解是当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是我们可以对CopyOnWrite容器进行并发的读而不需要加锁。所以CopyOnWrite容器也是一种读写分离的思想,读和写使用不同的容器。
从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet
二 实现原理
核心就是读时候不加锁,写时候进行同步,copy容器 -> 引用变更,copyOnWriteArrayList用到底层native的System.arraycopy方法。
下面仿照源码写的MyCopyOnWriteArrayList(看起来👇的setArray是为了gc)
1 public class MyCopyOnWriteArrayList<E> { 2 3 private transient final ReentrantLock reentrantLock = new ReentrantLock(); 4 5 private volatile transient Object[] array; 6 7 public Object[] getArray() { 8 return array; 9 } 10 11 public void setArray(Object[] array) { 12 this.array = array; 13 } 14 15 public MyCopyOnWriteArrayList() { 16 setArray(new Object[0]); 17 } 18 19 public MyCopyOnWriteArrayList(Collection<? extends E> collection) { 20 Object[] elements = collection.toArray(); 21 if (elements.getClass() != Object[].class) { 22 elements = Arrays.copyOf(elements, elements.length, Object[].class); 23 } 24 setArray(elements); 25 } 26 27 public MyCopyOnWriteArrayList(E[] toCopyIn) { 28 setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); 29 } 30 31 public int size() { 32 return getArray().length; 33 } 34 35 public boolean isEmpty() { 36 return size() == 0; 37 } 38 39 /** 40 * 返回Object类型数组 41 */ 42 public Object[] toArray() { 43 Object[] elements = getArray(); 44 return Arrays.copyOf(elements, elements.length); 45 } 46 47 /** 48 * 返回传入的T类型数组 49 */ 50 @SuppressWarnings("unchecked") 51 public <T> T[] toArray(T a[]) { 52 Object[] elements = getArray(); 53 int len = size(); 54 if (a.length < len) { 55 return (T[]) Arrays.copyOf(elements, len, a.getClass()); 56 } else { 57 System.arraycopy(elements, 0, a, 0, len); 58 return a; 59 } 60 } 61 62 @SuppressWarnings("unchecked") 63 private E get(Object[] a, int index) { 64 return (E) a[index]; 65 } 66 67 public E get(int index) { 68 return get(getArray(), index); 69 } 70 71 public E set(int index, E element) { 72 final ReentrantLock reentrantLock = this.reentrantLock; 73 reentrantLock.lock(); 74 try { 75 Object[] elements = getArray(); 76 E oldValue = get(elements, index); 77 78 if (oldValue != element) { 79 int len = elements.length; 80 Object[] newElements = Arrays.copyOf(elements, len); 81 newElements[index] = element; 82 setArray(newElements); 83 } else { 84 // 意义何在,没太看懂 85 setArray(elements); 86 } 87 return oldValue; 88 } finally { 89 reentrantLock.unlock(); 90 } 91 } 92 93 public boolean add(E e) { 94 final ReentrantLock reentrantLock = this.reentrantLock; 95 reentrantLock.lock(); 96 try { 97 Object[] elements = getArray(); 98 int len = size(); 99 Object[] newElements = Arrays.copyOf(elements, len + 1); 100 newElements[len] = e; 101 setArray(newElements); 102 return true; 103 } finally { 104 reentrantLock.unlock(); 105 } 106 } 107 108 public E remove(int index) { 109 final ReentrantLock reentrantLock = this.reentrantLock; 110 reentrantLock.lock(); 111 try { 112 Object[] elements = getArray(); 113 int len = elements.length; 114 E oldValue = get(elements, index); 115 int numMoved = len - index - 1; 116 if (numMoved == 0) 117 setArray(Arrays.copyOf(elements, len - 1)); 118 else { 119 Object[] newElements = new Object[len - 1]; 120 System.arraycopy(elements, 0, newElements, 0, index); 121 System.arraycopy(elements, index + 1, newElements, index, 122 numMoved); 123 setArray(newElements); 124 } 125 return oldValue; 126 } finally { 127 reentrantLock.unlock(); 128 } 129 } 130 }
那么再自己实现一个MyCopyOnWriteHashMap
1 public class MyCopyOnWriteHashMap<K, V> { 2 3 private transient final ReentrantLock reentrantLock = new ReentrantLock(); 4 5 private volatile transient HashMap<K, V> hashMap; 6 7 public MyCopyOnWriteHashMap(int expectedSize) { 8 hashMap = Maps.newHashMapWithExpectedSize(expectedSize); 9 } 10 11 public V put(K k, V v) { 12 reentrantLock.lock(); 13 try { 14 HashMap<K, V> curHashMap = Maps.newHashMap(hashMap); 15 V newV = curHashMap.put(k, v); 16 hashMap = curHashMap; 17 return newV; 18 } finally { 19 reentrantLock.unlock(); 20 } 21 } 22 23 public void putAll(Map<? extends K, ? extends V> map) { 24 reentrantLock.lock(); 25 try { 26 HashMap<K, V> curHashMap = Maps.newHashMap(hashMap); 27 curHashMap.putAll(map); 28 hashMap = curHashMap; 29 } finally { 30 reentrantLock.unlock(); 31 } 32 } 33 34 public V get(K k) { 35 return hashMap.get(k); 36 } 38 }
三 应用场景和问题
CopyOnWrite并发容器用于读多写少的并发场景。比如黑白名单
注意一下每次copy的开销,可预见的size可以提前设定; 另外容器可以批量添加等,避免多次的容器复制扩容
由于CopyOnWrite的写时复制机制,写操作过程中,内存里会同时驻扎两个对象的内存,更新大对象时可能造成频繁的GC,会拖垮应用响应时间
CopyOnWrite容器只保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的数据立即被读取响应,请不要用这个容器。