CopyOnWriteArrayList线程安全分析
CopyOnWriteArrayList是开发过程中常用的一种并发容器,多用于读多写少的并发场景。但是CopyOnWriteArrayList真的能做到完全的线程安全吗? 答案是并不能。
一、CopyOnWriteArrayList原理
我们可以看出当我们向容器添加或删除元素的时候,不直接往当前容器添加删除,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加删除元素,添加删除完元素之后,再将原容器的引用指向新的容器,整个过程加锁,保证了写的线程安全。
1 public boolean add(E e) { 2 synchronized (lock) { 3 Object[] elements = getArray(); 4 int len = elements.length; 5 Object[] newElements = Arrays.copyOf(elements, len + 1); 6 newElements[len] = e; 7 setArray(newElements); 8 return true; 9 } 10 } 11 12 public E remove(int index) { 13 synchronized (lock) { 14 Object[] elements = getArray(); 15 int len = elements.length; 16 E oldValue = get(elements, index); 17 int numMoved = len - index - 1; 18 if (numMoved == 0) 19 setArray(Arrays.copyOf(elements, len - 1)); 20 else { 21 Object[] newElements = new Object[len - 1]; 22 System.arraycopy(elements, 0, newElements, 0, index); 23 System.arraycopy(elements, index + 1, newElements, index, 24 numMoved); 25 setArray(newElements); 26 } 27 return oldValue; 28 } 29 }
而因为写操作的时候不会对当前容器做任何处理,所以我们可以对容器进行并发的读,而不需要加锁,也就是读写分离。
1 public E get(int index) { 2 return get(getArray(), index); 3 }
一般来讲我们使用时,会用一个线程向容器中添加元素,一个线程来读取元素,而读取的操作往往更加频繁。写操作加锁保证了线程安全,读写分离保证了读操作的效率,简直完美。
二、数组越界
但想象一下如果这时候有第三个线程进行删除元素操作,读线程去读取容器中最后一个元素,读之前的时候容器大小为i,当去读的时候删除线程突然删除了一个元素,这个时候容器大小变为了i-1,读线程仍然去读取第i个元素,这时候就会发生数组越界。
测试一下,首先向CopyOnWriteArrayList里面塞10000个测试数据,启动两个线程,一个不断的删除元素,一个不断的读取容器中最后一个数据。
1 public void test(){ 2 for(int i = 0; i<10000; i++){ 3 list.add("string" + i); 4 } 5 6 new Thread(new Runnable() { 7 @Override 8 public void run() { 9 while (true) { 10 if (list.size() > 0) { 11 String content = list.get(list.size() - 1); 12 }else { 13 break; 14 } 15 } 16 } 17 }).start(); 18 19 new Thread(new Runnable() { 20 @Override 21 public void run() { 22 while (true) { 23 if(list.size() <= 0){ 24 break; 25 } 26 list.remove(0); 27 try { 28 Thread.sleep(10); 29 } catch (InterruptedException e) { 30 e.printStackTrace(); 31 } 32 } 33 } 34 }).start(); 35 }
运行,可以看出删除到第7个元素的时候就发生了数组越界:
从上可以看出CopyOnWriteArrayList并不是完全意义上的线程安全,如果涉及到remove操作,一定要谨慎处理。