前言
在开发过程中需要程序猿考虑线程安全的情况,java中ArrayList是线程不安全,对应的线程安全类是CopyOnWriteArrayList类,这里简单说一下这两个类。
1、ArrayList的遍历和fail-fast
对于集合的遍历通常有三种方式:for循环、foreach语法糖和Iterator迭代器,其实后两者是一样的,只不过写法不同而已,foreach其实也是使用了迭代器来实现的。我们经常有这样的需求,在遍历过程中需要对集合的元素进行增加或修改操作,这里先贴上一个错误的方式代码
1 List<String> list = new ArrayList<>(); 2 list.add("a"); 3 list.add("b"); 4 list.add("c"); 5 list.add("d"); 6 for (int i = 0;i < list.size(); i++ ) { 7 if("b".equals(list.get(i))){ 8 list.remove(i); 9 }else{ 10 System.out.println(list.get(i));//or do something 11 } 12 }
我们预期的打印结果是a、c、d,但实际结果却是a、d,元素c丢失了。造成这个问题的原因就是,当list遍历到第二个元素b的时候,调用remove方法,这时候list的结构发生了变化,长度减少了1,而此时i的值依然从1自增到了2,所以下一次打印结果就是get(2)。在遍历集合的时候对集合进行操作时不推荐使用传统的for循环方式,接下来我们再贴上foreach语法糖的代码
1 for (String str : list) { 2 if("b".equals(str)){ 3 list.remove(str); 4 }else{ 5 System.out.println(str); 6 } 7 }
运行一下发现报错了,抛出了这个异常java.util.ConcurrentModificationException,这个是ArrayList的fail-fast机制。那什么是fail-fast呢?我们知道ArrayList是线程不安全的,换句话说,如果有两个线程,一个线程对ArrayList遍历,另一个线程对ArrayList进行修改操作,那ArrayList会尽力抛出异常,告诉我们同时操作是不允许的。我们看一下ArrayList是如何抛出这个异常的,前面说过,foreach语法糖其实就是Iterator迭代器,我们看到在ArrayList类中有个内部类Itr,这个类中有个变量叫expectedModCount,它的初始值等于ArrayList类中的modCount,当调用迭代器中的next()方法时会比较这两个变量的值是否相等,如果不相等则抛出上述的异常。ArrayList每次的修改操作都会对modCount进行自增操作,所以每次调用remove()方法,modCount值都会发生变化,导致与expectedModCount不相等,从而抛出异常。
1 public E remove(int index) { 2 rangeCheck(index); 3 4 modCount++; 5 E oldValue = elementData(index); 6 7 int numMoved = size - index - 1; 8 if (numMoved > 0) 9 System.arraycopy(elementData, index+1, elementData, index, 10 numMoved); 11 elementData[--size] = null; // Let gc do its work 12 13 return oldValue; 14 }
这个是ArrayList的remove方法,这里的modCount进行了自增的操作。
@SuppressWarnings("unchecked") public E next() { checkForComodification(); int i = cursor; if (i >= size) throw new NoSuchElementException(); Object[] elementData = ArrayList.this.elementData; if (i >= elementData.length) throw new ConcurrentModificationException(); cursor = i + 1; return (E) elementData[lastRet = i]; }
这个是内部类Itr的next()方法,里面调用了checkForComodification()方法,这个方法就是校验modCount和expectedModCount是否相等。
1 final void checkForComodification() { 2 if (modCount != expectedModCount) 3 throw new ConcurrentModificationException(); 4 }
那么对于上述的需求我们到底应该怎么做呢,这里推荐大家使用Iterator迭代器来实现,在迭代过程中实现元素的remove操作
1 Iterator<String> it = list.iterator(); 2 while(it.hasNext()){ 3 String str = it.next(); 4 if("b".equals(str)){ 5 it.remove(); 6 }else{ 7 System.out.println(str); 8 } 9 }
我们发现这是可以的,而且结果也是正确的,理由就是在Itr中的remove()方法会将modCount和expectedModCount设置相等。
2、CopyOnWriteArrayList
上面说了fail-fast,这里简单说一说fail-safe。对于CopyOnWriteArrayList来说,一个线程遍历它,一个线程修改它的元素时不会出现上面的问题,fail-safe的原理就是每次对元素的修改都会在副本上进行,也就是说CopyOnWriteArrayList对于set、remove、add等请求会先将集合做一个复制,然后所有的增删改操作均在复制的集合上操作,然后再将结果写回来。我们看一下CopyOnWriteArrayList的源码
1 private volatile transient Object[] array;
在CopyOnWriteArrayList中这样一个属性array,它是真正存储数据的地方,这里再看一下add()方法的代码
1 public boolean add(E e) { 2 final ReentrantLock lock = this.lock; 3 lock.lock(); 4 try { 5 Object[] elements = getArray(); 6 int len = elements.length; 7 Object[] newElements = Arrays.copyOf(elements, len + 1); 8 newElements[len] = e; 9 setArray(newElements); 10 return true; 11 } finally { 12 lock.unlock(); 13 } 14 }
先进行加锁操作,然后获取数组,其实就是获取array属性的值,复制一份进行新增操作,再写回array属性,最后释放锁。其实我们从名字就可以猜测出来其原理,copyonwrite在redis持久化fork子进程时也有类似的应用。关于CopyOnWriteArrayList就简单说这么多,有不对的地方希望大家及时指出!