2.2 fail-fast和fail-safe源码剖析
fail-fast源码分析
当使用增强for迭代list集合时,会先创建一个Itr对象(属于Iterator类),为属性expectedModCount初始化.
expectedModCount的初始值为list中的modCount (modCount 是list的成员变量,记录list被修改的次数)
之后每次迭代list就是用Itr对象.先判断hasNext()是否有值,如果则调用NEXT()方法进行获取.
NEXT()方法执行时,会先判断是否存在并发修改.如果存在则抛出异常,不存在则返回需要的元素.
分析演示场景: 迭代过程中为list增加了一个元素,导致modCount变成了5,但是expectedModCount还为4.所以Next()中验证是否存在修改时就会报异常.
private class Itr implements Iterator<E> { int cursor; // 返回下一个元素的下标 int lastRet = -1; // 返回最后一个元素的下标,如果没有则返回-1 int expectedModCount = modCount;//迭代器对象刚进行迭代时修改的初始次数(也就是循环开始时的修改次数) Itr() {}//构造方法 public boolean hasNext() {//判断是否还有下一个元素 return cursor != size;//如果5个元素 size:5 当遍历完最后一个元素后再次迭代时,cursor=5,返回false. } 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];//返回需要的元素 } //检查是否在迭代过程中进行了修改 final void checkForComodification() { if (modCount != expectedModCount)//如果进行了修改modCount就会增加,导致这俩属性值不相等,抛出并发修改异常 throw new ConcurrentModificationException(); } }
fail-safe源码分析
当使用增强for迭代list集合时,会先创建一个COWIterator对象,将需要迭代的集合赋值给 snapshot,cursor初始化为0.
之后每次迭代list就是使用COWIterator对象,先hasNext()判断是否有值.然后调用Next()获取值.
**分析演示场景:**当进行for遍历时,会把需要遍历的集合在COWIterator中存一份.之后每次遍历都是使用COWIterator中的数组对象. 当在遍历过程中进行add操作时,会创建一个新的数组,将老的元素和新的元素放入,再赋值给list集合中的数组对象.这个过程也被称为 写时复制(读写分离)
所以最后输出结果是:遍历出的没有新元素E,list直接输出有新元素E
//COWIterator static final class COWIterator<E> implements ListIterator<E> { /** 数组的快照 */ private final Object[] snapshot; /** 返回元素的下一个坐标. */ private int cursor; //构造器 COWIterator(Object[] es, int initialCursor) { cursor = initialCursor; snapshot = es; } //判断是否有下一个袁旭 public boolean hasNext() { return cursor < snapshot.length; } //获取下一个元素的值. public E next() { if (! hasNext()) throw new NoSuchElementException(); return (E) snapshot[cursor++];//cursor指针后移 } } //CopyOnWriteArrayList类 public class CopyOnWriteArrayList<E> implements List<E>{ final transient Object lock = new Object(); //数组对象用于存放元素 private transient volatile Object[] array; /** * Gets the array. Non-private so as to also be accessible * from CopyOnWriteArraySet class. */ final Object[] getArray() { return array; } //添加元素 public boolean add(E e) { synchronized (lock) { Object[] es = getArray();//获取之前的数组对象 int len = es.length;//获取长度 es = Arrays.copyOf(es, len + 1);//创建一个 len + 1 的新数组 es[len] = e;//将新的值放在最后一个位置上. setArray(es);//将新数组对象赋值给list中的array return true; } } }
2.3 补充:Vector底层使用的是哪种迭代机制呢?
Vector其实就比ArrayList多了个同步机制,也就是每个方法加上了synchronized.
经过Debug发现,Vector使用的也是Itr作为迭代器,其迭代类型属于:Fail-Fast.
代码演示
private static void testVector(){ Vector <Student> vector = new Vector <>(); vector.add(new Student("A")); vector.add(new Student("B")); vector.add(new Student("C")); vector.add(new Student("D")); for (Student student : vector) { System.out.println(student); } System.out.println(vector); }
3. LinkedList
3.1 对比LinkedList 和 ArrayList 的区别
基于双向链表,无需连续内存
随机访问慢(要沿着链表遍历)
头尾插入删除性能高
无法很好的利用CPU缓存,无法很好的配合局部性原理,占用内存多,
ArrayList
基于数组,需要连续内存
随机访问快(指根据下标访问)
尾部插入、删除性能可以,其它部分插入、删除都会移动数据,因此性能会低 (但是性能也可以秒杀LinkedList)
可以很好的利用CPU缓存,配合局部性原理,占用内存少,
3.2 代码对比两者性能
3.2.1 对比继承和实现关系
根据继承和实现关系,可以看出 ArrayList比LinkedList多实现一个RandomAccess接口.
public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable {} public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable {}
RandomAccess是个标记接口, 主要用于标记该类使用下标进行访问的,还是利用指针进行访问的.
/* *<pre> * for (int i=0, n=list.size(); i < n; i++) * list.get(i); * </pre> * 上面这种循环比下面这种循环效率更高: * <pre> * for (Iterator i=list.iterator(); i.hasNext(); ) * i.next(); * </pre> * * @since 1.4 */ public interface RandomAccess {//标记接口,不做任何实现. }
3.2.2 对比随机访问性能
public static void main(String[] args) { int n = 1000; int insertIndex = n; //产生1000个随机数,测试对比ArrayList和LinkedList的随机访问性能 for (int i = 0; i < 5; i++) {//测试五次(JVM要预热一下,次数越多越准确O!) int[] array = randomArray(n); List<Integer> list1 = Arrays.stream(array).boxed().collect(Collectors.toList()); LinkedList<Integer> list2 = new LinkedList<>(list1); randomAccess(list1, list2, n / 2);//访问中间的数 } } //随机访问方法 static void randomAccess(List<Integer> list1, LinkedList<Integer> list2, int mid) { StopWatch sw = new StopWatch(); sw.start("ArrayList"); list1.get(mid); sw.stop(); sw.start("LinkedList"); list2.get(mid); sw.stop(); System.out.println(sw.prettyPrint()); }
测试结果:
3.2.3 对比插入功能
- 头部插入对比
private static void addFirst(List<Integer> list1, LinkedList<Integer> list2) { StopWatch sw = new StopWatch(); sw.start("ArrayList"); list1.add(0, 100); sw.stop(); sw.start("LinkedList"); list2.addFirst(100); sw.stop(); System.out.println(sw.prettyPrint()); }
结果:LinkedList远超于ArrayList.
- 尾部插入对比
private static void addLast(List<Integer> list1, LinkedList<Integer> list2) { StopWatch sw = new StopWatch(); sw.start("ArrayList"); list1.add(100); sw.stop(); sw.start("LinkedList"); list2.add(100); sw.stop(); System.out.println(sw.prettyPrint()); }
结果:ArrayList更快一些
- 中间插入
private static void addMiddle(List<Integer> list1, LinkedList<Integer> list2, int mid) { StopWatch sw = new StopWatch(); sw.start("ArrayList"); list1.add(mid, 100); sw.stop(); sw.start("LinkedList"); list2.add(mid, 100); sw.stop(); System.out.println(sw.prettyPrint()); }
结果:ArrayList远超于LinkedList (因为LinkedList的迭代过程非常缓慢)
总结:
传统的 ArrayList:增删慢,查询快 LinkedList:增删快,查询慢 说法是完全错误的.
ArrayList除了头部插入比LinkedList效率低,其他方面都是超于LinkedList. 所以实际项目开发中,建议使用ArrayList.
3.2.4 对比占用内存
没有CPU缓存前:数据的运算,先将数据从硬盘读到CPU,然后CPU执行计算,计算完写到硬盘.但是CPU的计算速度是很快的(1s执行亿次级),例如CPU执行指令<1nm,但是读数据到CPU和写数据到硬盘却需要几百nm,CPU利用率极低.
有了CPU缓存:数据的运算,先将数据从硬盘加载到缓存中,然后CPU从缓存中读数据(可以提高到10nm),接着CPU进行运算,运算完将结果写到缓存,由缓存将数据写入硬盘.CPU的效率可以大大提高.
局部性原理: 读取元素时,其相邻元素也有很大概率被访问到.
对于ArrayList,会将其相邻元素也加载进入缓存,由于其元素在内存是是连续的.下次使用就不用再重新加载了,直接从缓存中取.
而对于LinkedList,由于其数据存储并不是连续的.虽然也将即将使用的数据及其相邻内存的数据加入缓存,但是后面需要的数据可能并不在当前的缓存中.而且一般缓存也是很小的,后面加入的数据可能覆盖前面的.所以需要经常将数据加入缓存. 从而无法很好的配合局部性原理.
如果有收获!!! 希望老铁们来个三连,点赞、收藏、转发。
创作不易,别忘点个赞,可以让更多的人看到这篇文章,顺便鼓励我写出更好的博客