我是靠谱客的博主 专注狗,这篇文章主要介绍Java篇 - 并发容器之CopyOnWriteArrayList的偷天换日,现在分享给大家,希望可以做个参考。

嗨,又和大家见面了,今天来讲讲java中的并发容器的最后一个:CopyOnWriteArrayList。

 

1.  CopyOnWriteArrayList介绍

  • CopyOnWriteArrayList是List的一种线程安全的实现,它使用的思路是"CopyOnWrite",目前的实现类有CopyOnWriteArrayList和CopyOnWriteArraySet。所有的写操作,包括:add,remove,set等都会触发底层数组的拷贝,从而在写的过程中不会影响读,避免了使用synchronized等进行读写操作的线程同步。
  • CopyOnWrite对于写操作的代价很大,所以它不适合于写操作很多的场景。当读操作远远大于写操作时,可以选择CopyOnWriteArrayList。
  • 迭代器以"快照"方式实现,在迭代器创建时,引用指向List当前状态的底层数组,所以在迭代器使用的整个生命周期中,其内部数据不会被改变;并且集合在遍历过程中进行修改,也不会抛出ConcurrentModificationException。迭代器在遍历过程中,不会感知集合的add,remove,set等操作。
  • 因为迭代器指向的是底层数组的"快照",因此也不支持对迭代器本身的修改操作,包括add,remove,set等操作,如果使用这些操作,将会抛出UnsupportedOperationException。

上面大概了解了一下CopyOnWriteArrayList,可能这样看不够深刻,那么我们来对照代码验证上面的结论。

 

2.  CopyOnWriteArrayList源码分析

  • 2.1 类定义
复制代码
1
2
3
public class CopyOnWriteArrayList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable { }

CopyOnWriteArrayList实现了List,所以,它是一个集合,支持相关的添加、删除、修改、遍历等功能;

CopyOnWriteArrayList实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在CopyOnWriteArrayList中,我们即可以通过元素的序号快速获取元素对象,这就是快速随机访问;

CopyOnWriteArrayList实现了Cloneable接口,即实现clone()函数,它能被克隆;

CopyOnWriteArrayList实现了Serializable接口,它支持序列化。

 

  • 2.2 变量定义
复制代码
1
2
3
4
5
6
7
8
9
final transient ReentrantLock lock = new ReentrantLock(); private transient volatile Object[] array; final Object[] getArray() { return array; } final void setArray(Object[] a) { array = a; }

定义了一个重入锁,这个锁是用在写操作上的,保证同步,同一时刻只有一个线程在写,而读是没有加锁的,读的时候读的还是旧的那个数组,而写的时候会拷贝一个数组副本,写完之后重新赋值给旧的数组。所以CopyOnWriteArrayList是写写互斥,读写不互斥,读读不互斥。

同时定义了一个对象数组,用来存放元素。对象数组的读写通过getArray()和setArray(),一个用来获取当前数组对象,一个用来将新的拷贝好的数组赋值给当前数组对象。

 

  • 2.3 构造器
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
/** * 无参构造器,默认创建一个大小为0的数组 */ public CopyOnWriteArrayList() { setArray(new Object[0]); } /** * 将一个集合转换成CopyOnWriteArrayList */ public CopyOnWriteArrayList(Collection<? extends E> c) { Object[] elements; // 如果c本来就为CopyOnWriteArrayList类型,直接转换 if (c.getClass() == CopyOnWriteArrayList.class) elements = ((CopyOnWriteArrayList<?>)c).getArray(); else { elements = c.toArray(); // 双重校验,具体可以看Vector源码分析那章 if (elements.getClass() != Object[].class) elements = Arrays.copyOf(elements, elements.length, Object[].class); } // 设置数组 setArray(elements); } /** * 将一个数组转换成CopyOnWriteArrayList */ public CopyOnWriteArrayList(E[] toCopyIn) { setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class)); }

需要说明的是集合转换成CopyOnWriteArrayList的那个构造器,做了两次类型校验,具体原因可以参考我前面写的Vector那章:https://blog.csdn.net/u014294681/article/details/85323638

 

  • 2.4 读方法
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public int size() { return getArray().length; } public boolean isEmpty() { return size() == 0; } private static int indexOf(Object o, Object[] elements, int index, int fence) { if (o == null) { for (int i = index; i < fence; i++) if (elements[i] == null) return i; } else { for (int i = index; i < fence; i++) if (o.equals(elements[i])) return i; } return -1; } private static int lastIndexOf(Object o, Object[] elements, int index) { if (o == null) { for (int i = index; i >= 0; i--) if (elements[i] == null) return i; } else { for (int i = index; i >= 0; i--) if (o.equals(elements[i])) return i; } return -1; } public boolean contains(Object o) { Object[] elements = getArray(); return indexOf(o, elements, 0, elements.length) >= 0; } public int indexOf(Object o) { Object[] elements = getArray(); return indexOf(o, elements, 0, elements.length); } public int indexOf(E e, int index) { Object[] elements = getArray(); return indexOf(e, elements, index, elements.length); } public int lastIndexOf(Object o) { Object[] elements = getArray(); return lastIndexOf(o, elements, elements.length - 1); } public int lastIndexOf(E e, int index) { Object[] elements = getArray(); return lastIndexOf(e, elements, index); } @SuppressWarnings("unchecked") private E get(Object[] a, int index) { return (E) a[index]; } public E get(int index) { return get(getArray(), index); }

可以看到,所有的读方法是没有加锁的,而且底层采用数组实现,所以读的效率是很高的。

 

  • 2.5 写方法
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
/** * 更新指定位置的元素 */ public E set(int index, E element) { // 加锁 final ReentrantLock lock = this.lock; lock.lock(); try { // 先获取旧数组 Object[] elements = getArray(); // 获取旧的元素 E oldValue = get(elements, index); // 如果旧的元素和新设置的元素不相等 if (oldValue != element) { int len = elements.length; // 先拷贝一个和旧数组同样大小的数组 Object[] newElements = Arrays.copyOf(elements, len); // 设置新值 newElements[index] = element; // 更新数组引用 setArray(newElements); } else { setArray(elements); } // 返回旧值 return oldValue; } finally { lock.unlock(); } } /** * 添加一个元素 */ public boolean add(E e) { // 先加锁 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; // 新数组的大小为旧数组大小 + 1 Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; // 更新数组引用 setArray(newElements); return true; } finally { lock.unlock(); } } /** * 添加一个元素到指定位置 */ public void add(int index, E element) { // 先加锁 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; // 如果找不到这个索引位置则抛出异常 if (index > len || index < 0) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len); Object[] newElements; int numMoved = len - index; // 如果不需要移动数组,则将该元素插入到数组尾部,数组得先扩容1 if (numMoved == 0) newElements = Arrays.copyOf(elements, len + 1); else { // 否则新创建一个数组,并复制旧数据 newElements = new Object[len + 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index, newElements, index + 1, numMoved); } newElements[index] = element; // 更新数组引用 setArray(newElements); } finally { lock.unlock(); } } /** * 移除指定位置的元素 */ public E remove(int index) { // 先加锁 final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; E oldValue = get(elements, index); int numMoved = len - index - 1; // 如果不需要移动数组,则直接截掉最后一个元素生成一个新的数组 if (numMoved == 0) setArray(Arrays.copyOf(elements, len - 1)); else { // 否则创建新数组,拷贝旧元素 Object[] newElements = new Object[len - 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index + 1, newElements, index, numMoved); // 更新数组引用 setArray(newElements); } return oldValue; } finally { lock.unlock(); } }

可以看到,它所有的写操作都是生成新的数组,新增元素扩容是旧数组长度+1,在写的时候不会去影响旧的数组。而且所有的写操作都加锁了,保证同一个时刻只有一个线程修改元素。如果写的时候,有线程来读,读的是写之前的旧数组,所CopyOnWrite只能保证数据最终的一致性,不能保证数据的实时一致性。

并且由于是数组实现,本身插入删除的效率就很低,因为涉及到数组元素的移动。而且加上拷贝这一特性,使得CopyOnWriteArrayList写操作的时间复杂度和空间复杂度都很高。

 

  • 2.6 排序
复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void sort(Comparator<? super E> c) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); Object[] newElements = Arrays.copyOf(elements, elements.length); @SuppressWarnings("unchecked") E[] es = (E[])newElements; Arrays.sort(es, c); setArray(newElements); } finally { lock.unlock(); } }

CopyOnWriteArrayList支持Comparator排序的,先加锁,然后拷贝一个新的数组,再调用Arrays.sort方法排序,最后更新数组的引用。

 

  • 2.7 迭代遍历
复制代码
1
2
3
4
5
6
7
8
9
10
public void forEach(Consumer<? super E> action) { if (action == null) throw new NullPointerException(); Object[] elements = getArray(); int len = elements.length; for (int i = 0; i < len; ++i) { @SuppressWarnings("unchecked") E e = (E) elements[i]; action.accept(e); } }

可以看到,如果是使用增强for循环遍历CopyOnWriteArrayList元素的话,它是直接拿到当前数组去操作遍历的,而这期间的写操作又不会影响这个数组。所以在迭代器使用的整个生命周期中,其内部数据不会被改变;并且集合在遍历过程中进行修改,也不会抛出ConcurrentModificationException。

 

再看看它的迭代器:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public Iterator<E> iterator() { return new COWIterator<E>(getArray(), 0); } public ListIterator<E> listIterator() { return new COWIterator<E>(getArray(), 0); } static final class COWIterator<E> implements ListIterator<E> { private final Object[] snapshot; private int cursor; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } public boolean hasNext() { return cursor < snapshot.length; } public boolean hasPrevious() { return cursor > 0; } @SuppressWarnings("unchecked") public E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++]; } @SuppressWarnings("unchecked") public E previous() { if (! hasPrevious()) throw new NoSuchElementException(); return (E) snapshot[--cursor]; } public int nextIndex() { return cursor; } public int previousIndex() { return cursor-1; } public void remove() { throw new UnsupportedOperationException(); } public void set(E e) { throw new UnsupportedOperationException(); } public void add(E e) { throw new UnsupportedOperationException(); } @Override public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); Object[] elements = snapshot; final int size = elements.length; for (int i = cursor; i < size; i++) { @SuppressWarnings("unchecked") E e = (E) elements[i]; action.accept(e); } cursor = size; } }

很明显,迭代器使用的是当前数组的快照,就是它本身。内部的add,set,remove操作直接抛异常,就是为了让调用者不能在迭代期间修改元素。

 

 

3.  CopyOnWriteArrayList总结

优点:读操作远远多于写操作的场景下使用,比如说缓存,能实现同步和效率的双赢。

缺点:

  • 1. 内存占有问题: 很明显,两个数组同时驻扎在内存中,如果实际应用中,数据比较多,而且比较大的情况下,占用内存会比较大,针对这个其实可以用ConcurrentHashMap来代替。
  • 2. 数据一致性: CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果你希望写入的的数据,马上能读到,请不要使用CopyOnWrite容器。

 

 

4.  CopyOnWriteArrayList使用

先看这样一段代码:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) { final List<String> list = new ArrayList<>(); list.add("a"); list.add("b"); list.add("c"); Thread t = new Thread(new Runnable() { int count = -1; @Override public void run() { while (true) { list.add(String.valueOf(count++)); } } }); t.setDaemon(true); t.start(); try { Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } for (String s : list) { System.out.println(s); } }

执行直接报错:

Exception in thread "main" java.util.ConcurrentModificationException
    at java.util.ArrayList$Itr.checkForComodification(ArrayList.java:901)
    at java.util.ArrayList$Itr.next(ArrayList.java:851)
    at io.kzw.advance.csdn_blog.ConcurrentTest.main(ConcurrentTest.java:105)
 

我们把ArrayList换成CopyOnWriteArrayList:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void main(String[] args) { final List<String> list = new CopyOnWriteArrayList<>(); list.add("a"); list.add("b"); list.add("c"); Thread t = new Thread(new Runnable() { int count = -1; @Override public void run() { while (true) { list.add(String.valueOf(count++)); } } }); t.setDaemon(true); t.start(); try { Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } for (String s : list) { System.out.println(s); } }

执行OK,正常打印。

 

再来试试迭代器:

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void main(String[] args) { final List<String> list = new CopyOnWriteArrayList<>(); list.add("a"); list.add("b"); list.add("c"); Thread t = new Thread(new Runnable() { int count = -1; @Override public void run() { while (true) { list.add(String.valueOf(count++)); } } }); t.setDaemon(true); t.start(); try { Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String next = iterator.next(); System.out.println(next); } }

可以正常打印输出。

但是如果对迭代器操作add,set或者remove呢?

复制代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public static void main(String[] args) { final List<String> list = new CopyOnWriteArrayList<>(); list.add("a"); list.add("b"); list.add("c"); Thread t = new Thread(new Runnable() { int count = -1; @Override public void run() { while (true) { list.add(String.valueOf(count++)); } } }); t.setDaemon(true); t.start(); try { Thread.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } Iterator<String> iterator = list.iterator(); while (iterator.hasNext()) { String next = iterator.next(); System.out.println(next); if (next.equals("100")) { iterator.remove(); } } }

可以看到,中间报错了:

Exception in thread "main" java.lang.UnsupportedOperationException
 

这些例子都能验证上述的代码。

最后

以上就是专注狗最近收集整理的关于Java篇 - 并发容器之CopyOnWriteArrayList的偷天换日的全部内容,更多相关Java篇内容请搜索靠谱客的其他文章。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(68)

评论列表共有 0 条评论

立即
投稿
返回
顶部