fail-fast机制
在java集合类中,使用modCount
来检查数组的状态.
当在迭代集合的时候,(通常会实现 iterator()方法
来获取迭代对象,或者 foreach
),
集合里面的数据,在其他地方被修改了,这个时候 modCount
就会被修改,与迭代过程的modCount
不一致.将抛出ConcurrentModificationException
异常,这个过程就叫 fail-fast
List 篇
主要介绍 ArrayList
,Vector
和LinkedList
,CopyOnWriteArrayList
ArrayList
是一个数组长度可自增长的线程不安全的列表.内部使用数组实现. Object[] elementData
.
- 默认的长度为 10.
- fail-fast
- 自动扩容(每次扩容为原来的1.5倍)
- 线程不安全
- 使用连续控件存储
- 继承自
List
,RandomAccess
等. - 允许添加
null
值
时间复杂度 :
根据索引查询,复杂度为 O(1)
.
根据元素查询,复杂度为 O(N)
.
插入和删除,如果在列首复杂度为 O(N)
;在列尾复杂度为 O(1)
;平均复杂为 O(N)
插入和删除非列尾数据, 将会导致数组移动数据.
RandomAccess 接口
RandomAccess
是一个标记接口(没有任何方法).
如果实现了 RandomAccess
接口,表示该集合可以进行随机快速访问(通常底层是 数组形式的集合),如 ArrayList
.
可以快速访问的集合,使用
for (int i=0, n<size; i++)
list.get(i);
未实现 RandomAccess
接口的集合,如LinkedList
,使用迭代器效率更高.
for (Iterator i=list.iterator(); i.hasNext(); )
i.next();
使用 instanceof RandomAccess
来判断是否 支持随机快速访问.
Vector
是一个数组长度可自增长的线程安全的列表,内部使用数组实现.
- 默认的长度为 10.
- fail-fast
- 自动扩容(可设置每次扩容的增量值,默认扩容为原来的2倍.)
- 线程安全(
synchronized
) - 使用连续控件存储
- 继承自
RandomAccess
,List
- 允许添加
null
值
线程安全的vector
,照样可以触发 fail-fast
.
时间复杂度与 arraylist
一样.
在不涉及线程安全的前提下,arraylist
效率更高.
fun main(args: Array<String>) {
val vector = Vector(listOf(1, 2, 3, 4, 5))
singleThread(vector)
multiThread(vector)
multiThreadSynchronized(vector)
}
/**
* 单线程下,迭代抛出 ConcurrentModificationException
*/
fun singleThread(vector: Vector<Int>) {
val it = vector.iterator()
while (it.hasNext()) {
println(it.next())
vector.add(1)
}
}
/**
* 多线程下,迭代抛出 ConcurrentModificationException
*/
fun multiThread(vector: Vector<Int>) {
thread {
repeat(10000) {
vector.add(it)
}
}
thread {
vector.forEach { println(it) }
}
}
/**
* 多线程下,修改vector是添加同步锁,避免同时迭代同时修改
*/
fun multiThreadSynchronized(vector: Vector<Int>) {
thread {
synchronized(vector) {
repeat(10000) {
vector.add(it)
}
}
}
thread {
vector.forEach { println(it) }
}
}
Stack
继承自 vector
,线程安全,栈结构,先进后出
.
LinkedList
是以双向链表方式实现的非线程安全的集合.
- 一个元素节点包含 上一个节点和下一个节点的引用,以及自身的值.
- 内存空间可不连续
- 线程不安全
- fail-fast
- 继承自
List
,Deque
等. - 允许添加
null
值
时间复杂度:
删除和插入元素,复杂度为 O(1)
.
查询时,由于采用双向链表,判断离哪头近,从哪头开始找.
最糟糕的情况是O(N/2)
,所以它的复杂度为 O(N)
.
CopyOnWriteArrayList
CopyOnWriteArrayList
是并发包中的实现.使用 ReenterLock
和System.arraycopy
来实现数组读写的线程安全.
在读取的时不加锁,所以可以支持多线程同时读取数据,并且同时读取数不受限制.
在写入(add,set,remove等操作)的时候,使用 ReenterLock
锁住方法,然后操作数据时先将原数组拷贝一份,对拷贝数据进行操作,操作完后将拷贝的原来的数据引用指向拷贝的数组引用,实现数据的更新操作,更新完后释放锁. 和其名字操作一致(写时拷贝).
没有设置初始长度的方法和扩容的策略. 因为该数组在每次 添加数据是, 都会使用 System.arraycopy
拷贝一份比当前数据 +1 的数组.
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
ArrayList,Vector和LinkedList的区别
ArrayList
和Vector
内部都是使用数组实现的.它们的主要区别在于
-
ArrayList
是线程不安全的,Vector
是线程安全的. -
ArrayList
自动扩容时固定增长为原来的1.5倍,不可指定增量值.Vector
可指定每次的增量值,如果没指定,则扩容为原来的2倍.
在不考虑线程安全的情况下,优先使用 ArrayList
,如果在使用前,能够预估大概有元素,创建时指定个数,效果更好.
LinkedList
使用双向链表实现,也是线程不安全的.与ArrayList
的区别在于
- 实现方式不同.
ArrayList
使用数组,Vector
使用双向链表. - 内存空间管理不同,
LinkedList
可以是不连续的空间,ArrayList
需要连续的内存空间. -
LinkedList
在删除和插入上的性能较好,ArrayList
在查询上的效率更好. -
ArrayList
一个item只存自己的值,比较省空间,LinkedList
一个item除了自己的值,还需要存放上一个和下一个元素的引用,比较费空间.
如果主要需要对列表进行遍历,以及增删操作,使用LinkedList
其他情况使用ArrayList
.
如果需要用到线程安全的列表,请使用 CopyOnWriteArrayList
Map篇
Map
是以键值对形式存储的集合.
主要介绍 HashMap
,TreeMap
和 Hashtable
,ConcurrentHashMap
HashMap
HashMap
内部混合使用数组,链表,红黑树的结构来构建的键值对集合.
HashMap
的本质基础是基于数组的. 数组的各个元素,使用单向链表的结构.(Node(int hash, K key, V value, Node<K,V> next)
).
HashMap
通过哈希函数(hash()
)来完成key
对index
的映射.
当出现hash后index冲突(index位置已经存在不同的key的键值对)时,数据将在index位置,使用单向链表或红黑树(当链表长度大于等8个时,链表中的所有元素改用红黑树存放)来存放元素.
当使用掉的索引 占用了 数组长度的一定比例时(填装因子
),HashMap
将进行扩容(扩容为原来的两倍,key与index重新映射).
HashMap
的性能瓶颈:
- 优良的 哈希函数.
理想的状态是,每一个键值对的存放,能够比较均匀适当的分布在数组的索引上,尽量避免过多的hash值冲突.
// java 8 中的实现
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
// index = (n - 1) & hash , 2的次方来说就是取余操作.
// 由于 (n -1)的二进制 高位都是0,地位都是1.
// hashcode 与 它进行 & 操作的话, hashcode的高位变化低位不变,则计算出来的index值讲不变.
// 为了消除高位变化 不影响 index 的值, 于是将 hashcode 的 高16位 移动到低16位,再和 hashcode进行 ^ 操作.
// 这样高位的变化,也能影响index值, 降低了冲突的概率.
- 填装因子
过低的填装因子,如填装因子=0.5,容量剩余一半的时候就扩容,可能导致空间上的浪费.
过高的填装因子,可能导致为了命中未使用的索引,而频繁出现 冲突.HashMap
实现中,填装因子默认使用 0.75
的值.
特点 :
- 默认的长度为 16,默认填装因子为 0.75.
- fail-fast
- 自动扩容(默认扩容为原来的2倍.)
- 线程不安全
- 混合使用数组,链表和红黑树
- 继承自
Map
-
key
和value
都可以添加null
- 插入和遍历顺序不一致
时间复杂度 :
查询,插入和删除操作的平均时间复杂度为 O(1)
.
极端情况下(全部发生冲突),若长度小于8,使用单项链表,复杂度为 O(n)
,如果长度大于等于8,使用红黑树,复杂度为O(logn)
LinkedHashMap
继承自 HashMap
, 底层使用哈希表与双向链表来保存元素,与HashMap
的差异就在于 访问顺序上.
构造中,accessOrder
默认为 false,代表按照插入顺序进行迭代;为 true,代表以访问顺序进行迭代(最常访问的在前,最不经常访问的在后LRU
)。
TreeMap
TreeMap
内部使用 红黑树
来实现的键值对集合.与HashMap相比,TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序。其中,可以使用元素的自然顺序,也可以使用集合中自定义的比较器来进行排序.
特点 :
- fail-fast
- 线程不安全
- 使用红黑树实现
-
key
和value
都可以添加null
- 插入和遍历顺序不一致
- 支持对
key
排序 - 继承自
NavigableMap
,拥有强大的搜索功能.
时间复杂度 :
红黑树
是自平衡的二叉搜索树, 查询,添加删除 效率都是 O(logn)
.
HashTable
HashTable
内部使用数组和单项链表实现的键值对集合.
HashTable
的哈希函数 (hash & 0x7FFFFFFF) % tab.length
,只是对key的hashcod
进行取整和取余操作.
HashTable默认的初始大小为11,之后每次扩充为原来的2n+1。
也就是说,HashTable的链表数组的默认大小是一个素数、奇数。之后的每次扩充结果也都是奇数。
由于HashTable会尽量使用素数、奇数作为容量的大小。当哈希表的大小为素数时,简单的取模哈希的结果会更加均匀。(这个是可以证明出来 的,可参考:http://zhaox.github.io/algorithm/2015/06/29/hash)
特点 :
- 默认的长度为 11,默认填装因子为 0.75.
- fail-fast
- 自动扩容(扩容为原来的2n+1)
- 线程安全
- 混合使用数组,链表
- 继承自
Map
-
key
和value
都不能使用null
值,否则抛出空指针异常. - 插入和遍历顺序不一致
时间复杂度 :
和HashMap
一致
ConcurrentHashMap (java.util.concurrent)
和HashMap
一样,是内部使用数组,链表,红黑树的结构来构建的键值对集合,因为需要保证线程安全,所以内部实现更加复杂.
哈希函数 (h ^ (h >>> 16)) & HASH_BITS
, 和HashMap
类似,与高位进行异或操作的方式,来消除高位数据变化导致索引不变的情况,多加了一步正数的操作.
当冲突的链表中个数超过8个, 如果数组长度小于64,则扩容,否则,将链表数据转化为 红黑树结构存储.
ConcurrentHashMap
采用 CAS(Compare and Swap)
原子类型操作,来保证线程的安全性. 较 jdk1.7 的分段锁机制性能更好.
特点 :
- 默认长度为16,无默认的装填因子
构造函数为 : ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)
没有默认的装填因子,传入 初始容量和装填因子后,会被算法调整为 2次方,最小容量为 16.
并发级别,表示 可以同时对数据进行写操作的线程数, 最大可达16. 读取不受限制.
- 没有 fail-fast, 将保证线程安全.
- 自动扩容为原来的 2 倍.
- 线程安全
- 混合使用数组,链表和红黑树
- 继承自
ConcurrentMap
-
key
和value
都不可以添加null
- 插入和遍历顺序不一致
时间复杂度 :
和HashMap
一致
HashMap
和HashTable
, ConcurrentHashMap
对比
HashMap
线程不安全 , HashTable
线程安全,但还是存在fail-fast
问题 , ConcurrentHashMap
线程安全,不存在fail-fast
问题.
HashMap
和 ConcurrentHashMap
内部都是用 数组,链表 加红黑树来构建.(较高效) HashTable
使用数组加链表.
HashMap
和 ConcurrentHashMap
都是用 高位异或的哈希算法. HashTable
采用 数组长度为素数,直接取余的哈希算法.
HashMap
允许插入 null
键值对, HashTable
, ConcurrentHashMap
不允许.
结论 : 不考虑线程安全的情况下使用
HashMap
, 线程安全则使用ConcurrentHashMap
.
如果知道 大概会存多少数据,指定 初始容量 会更高效, 避免扩容和重新hash映射产生的效率问题.
Set篇
Set
是没有重复元素的单元素集合.
TreeSet
内部使用 TreeMap
实现.
HashSet
内部使用 HashMap
实现. 当构造参数为三个时 LinkedHashMap
实现.
LinkedHashSet
继承自 HashSet
,但是都是调用 HashSet
中有三个构造参数的LinkedHashMap
来实现.