Java 集合 - List 总结
# arrayList和LinkedList区别
- ArrayList 底层是用数组实现的顺序表,是随机存取类型,可自动扩增,并且在初始化时,数组的长度是 0,只有在增加元素时,长度才会增加。默认是 10,不能无限扩增,有上限,在查询操作的时候性能更好
- LinkedList 底层是用链表来实现的,是一个双向链表,注意这里不是双向循环链表,顺序存取类型。在源码中,似乎没有元素个数的限制。应该能无限增加下去,直到内存满了在进行删除,增加操作时性能更好
两个都是线程不安全的,在 iterator 时,会发生 fail-fast:快速失效。
# ArrayList和Vector的区别
- ArrayList 线程不安全,在用 iterator,会发生 fail-fast
- Vector 线程安全,因为在方法前加了 Synchronized 关键字。也会发生 fail-fast
fail-fast 和 fail-safe 区别和什么情况下会发生
简单的来说:在 java.util 下的集合都是发生 fail-fast,而在 java.util.concurrent 下的发生的都是 fail-safe。
fail-fast
快速失败,例如在 ArrayList 中使用迭代器遍历时,有另外的线程对 ArrayList 的存储数组进行了改变,比如 add、delete、等使之发生了结构上的改变,所以 Iterator 就会快速报一个 java.util.ConcurrentModificationException
异常(并发修改异常),这就是快速失败。
fail-safe
安全失败,在 java.util.concurrent 下的类,都是线程安全的类,他们在迭代的过程中,如果有线程进行结构的改变,不会报异常,而是正常遍历,这就是安全失败。
为什么在 java.util.concurrent 包下对集合有结构的改变,却不会报异常?
在 concurrent 下的集合类增加元素的时候使用 Arrays.copyOf()
来拷贝副本,在副本上增加元素,如果有其他线程在此改变了集合的结构,那也是在副本上的改变,而不是影响到原集合,迭代器还是照常遍历,遍历完之后,改变原引用指向副本,所以总的一句话就是如果在此包下的类进行增加删除,就会出现一个副本。所以能防止 fail-fast,这种机制并不会出错,所以我们叫这种现象为 fail-safe。
Vector 也是线程安全的,为什么是 fail-fast 呢?
这里搞清楚一个问题,并不是说线程安全的集合就不会报 fail-fast,而是报 fail-safe,你得搞清楚前面所说答案的原理,出现 fail-safe 是因为他们在实现增删的底层机制不一样,就像上面说的,会有一个副本,而像 ArrayList、LinekdList、Verctor 等,他们底层就是对着真正的引用进行操作,所以才会发生异常。
既然是线程安全的,为什么在迭代的时候,还会有别的线程来改变其集合的结构呢(也就是对其删除和增加等操作)?
首先,我们迭代的时候,根本就没用到集合中的删除、增加,查询的操作,就拿 Vector 来说,我们都没有用那些加锁的方法,也就是方法锁放在那没人拿,在迭代的过程中,有人拿了那把锁,我们也没有办法,因为那把锁就放在那边。
举例说明 fail-fast 和 fail-safe 的区别
- fail-fast
- fail-safe
通过 CopyOnWriteArrayList 这个类来做实验,不用管这个类的作用,但是他确实没有报异常,并且还通过第二次打印,来验证了上面我们说创建了副本的事情。
原理是在添加操作时会创建副本,在副本上进行添加操作,等迭代器遍历结束后,会将原引用改为副本引用,所以我们在创建了一个 List 的迭代器,结果打印的就是 123444 了,
证明了确实改变成为了副本引用,后面为什么是三个 4,原因是我们循环了 3 次,不就添加了 3 个 4 吗。如果还感觉不爽的话,看下 add 的源码。
# 为什么现在都不提倡使用Vector了
- Vector 实现线程安全的方法是在每个操作方法上加锁,这些锁并不是必须要的,在实际开发中,一般都是通过锁一系列的操作来实现线程安全,也就是说将需要同步的资源放一起加锁来保证线程安全
- 如果多个 Thread 并发执行一个已经加锁的方法,但是在该方法中,又有 Vector 的存在,Vector 本身实现中已经加锁了,那么相当于锁上又加锁,会造成额外的开销
- 就如上面第三个问题所说的,Vector 还有 fail-fast 的问题,也就是说它也无法保证遍历安全,在遍历时又得额外加锁,又是额外的开销,还不如直接用 ArrayList,然后再加锁呢
总结:Vector 在不需要进行线程安全的时候,也会加锁,也就导致了额外开销,所以在 JDK1.5 之后就被弃用了,现在如果要用到线程安全的集合,都是从 java.util.concurrent 包下去拿相应的类。