Collection分为Set
, List
, Queue
。
Set
包括SortedSet
(TreeSet
), HashSet
, LinkedHashSet
。List
包括ArrayList
, Vector
, LinkedList
。Queue
包括LinkedList
, PriorityQueue
。Set:
TreeSet
:基于红黑树实现,支持有序性操作,查找的时间复杂度是O(logn)
。HashSet
:基于Hash表实现,支持快速查找,但是不支持有序操作。LinkedHashSet
:具有HashSet
的查找效率,且内部使用双向链表维护元素的插入顺序。List:
ArrayList
:基于动态数组实现,支持随机访问。Vector
:和ArraList
相似,但它是线程安全的。LinkedList
:基于双向链表实现,只能顺序访问,但是可以快速地插入和删除元素。还可以用作栈、队列、双向队列。Queue:
LinkedList
:可以用它来实现双向队列。PriorityQueue
:基于堆结构实现,可以用它来实现优先队列。Map包括SortedMap
(TreeMap
), HashTable
, HashMap
, LinkedHashMap
。
TreeMap
:基于红黑树实现。HashMap
:基于哈希表实现。HashTable
:和HashMap
相似,但线程安全。LinkedHashMap
:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少(LRU)顺序。ArrayList
基于动态数组实现,支持快速随机访问。RandomAccess
接口标识着该类支持快速随机访问。
数组默认大小为10。
添加元素时使用ensureCapacity()
方法来保证容量是否足够,如果不够时,需要使用grow()
方法进行扩容,扩容大小为旧容量1.5倍。
扩容操作需要调用Arrays.copyOf()
把原数组整个复制到新数组中,代价很高。
需要调用System.arraycopy()
将index+1
后面的元素都复制到index位置上,该操作时间复杂度为O(n)
,删除代价很高。
modeCount
用来记录ArrayList
结构发生变化的次数。
在程序进行序列化或者迭代等操作时,需要比较操作前后modeCount
是否改变,如果改变需要抛出ConcurrentModificationException
。
保存元素的数组elementData
使用transient
修饰,该关键字声明数组默认不会被序列化。
ArrayList
实现了writeObject()
和readObject()
来控制只序列化数组中有元素填充那部分内容。
与ArrayList
类似,但是使用了synchronized
进行同步。
每次扩容请求其大小的2倍空间,而ArrayList
是1.5倍。
可以使用Collections.synchronizedList()
得到一个线程安全的ArrayList
。也可以使用concurrent
并发包下的CopyOnWriteArrayList
类。
读写分离:写操作在一个复制的数组上进行,读操作还是在原数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。写操作结束之后需要把原数组指向新的复制数组。
适用场景:在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。不适合内存敏感以及对实时性要求很高的场景。
缺陷:
基于双向链表实现,只支持顺序查找。
ArrayList
基于动态数组实现,支持随机访问,但是增加和删除元素的效率低;LinkedList
基于双向链表实现,不支持随机访问,增加和删除元素的效率高。List
允许重复元素;Set
不允许重复元素;List
允许多个null
元素;Set
底层是Map
,HashSet
允许一个null
元素,TreeSet
不允许null
元素;List
是一个有序的容器,保持了每个元素的插入元素;Set
是一个无序的容器,TreeSet
通过Comparator
或者Comparable
维护了一个排序顺序,LinkedHashSet
通过双向链表维护了插入顺序。数组+链表+红黑树
内部包含了一个Entry[]
数组,数组的每一个位置被当成一个桶,一个桶中放一个链表,节点组成为int hashCode, K key, V value, Entry
。
HashMap
一般用拉链法解决地址冲突,JDK7之前是头插法,容易导致链表环化,JDK8之后是尾插法。另一个改变是当一个链表长度超过8后,会转换为红黑树,提高查找效率。
key
的hash值(与Key.hashCode的高16位做异或运算);resize()
初始化散列表;key
地址相同或者equals
后内容相同,则替换旧值;resize()
进行扩容。调用场景:
++size > loadfactor* capacity
时,也是在put
函数中。实现过程:
通过判断旧数组的容量是否大于0,来判断数组是否初始化过
否:进行初始化
判断是否调用无参构造器
是:使用默认的大小和阈值;
否:使用构造函数中初始化的容量,这个容量是经过计算后的2的次幂数
是:进行扩容,扩容成两倍(小于最大值的情况下),之后再将元素重新进行运算复制到新的散列表中。
对key
的hashCode
做hash
操作,与高16位做异或运算。
还有平方取中法,除留余数法,伪随机法。
因为数组位置的确定用的是与运算,仅仅最后四位有效,设计者将key
的哈希值与高16位做异或运算,使得在做与运算确定数组的插入位置时,此时的低位实际是高位与低位的结合,增加了随机性,减少了哈希碰撞的次数。
HashTable
使用synchronized
来进行同步;HashMap
可以插入key
为null
的Entry
;HashMap
的迭代器是fail-fast
迭代器;HashMap
不能保证随着时间的推移,Map
中的元素次序是不变的;HashMap
继承自AbstractMap
类,HashTable
继承自Dictionary
。选择Integer, String这种不可变的类型,已经规范覆写了hashCode()
和equals()
,天生线程安全。
ConcurrentHashMap
和 HashMap
实现上类似,主要的差别是 ConcurrentHashMap
采用了分段锁 (Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。
Segment
继承自 ReentrantLock
。
默认的并发级别为 16,也就是说默认创建 16 个 Segment
。Segment
的数量一旦初始化就无法更改.每个Segment
上只有一个线程可以操作。
由于ConcurrentHashMap
本质上还是数组链表的结构,因此我们在链表上遍历数据整体的效率还是偏低。所以JDK8底层改用了数组+链表/红黑树的形式,且换成了Synchronized
锁加 CAS 的机制。Synchronized
只会锁住当前链表或者红黑树的首结点,只要hash
不冲突就不会产生并发,可以大大提升效率。
ConcurrentHashMap
的get
操作并不直接加锁。使用了volatile
关键字去修饰了其中的成员变量value
,还有下一个节点next
。正因为使用了该关键字保证了内存可见性,因此ConcurrentHashMap
的get
方法是不需要加锁的,这大大提升了效率。
根据key
计算出哈希值,定位到插入位置如果为空,那么尝试CAS自旋插入。否则使用synchronized
锁写入数据。
每个 Segment
维护了一个 count
变量来统计该 Segment
中的键值对个数。
在执行 size
操作时,需要遍历所有 Segment
然后把 count
累计起来。
ConcurrentHashMap
在执行 size
操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。尝试次数使用 RETRIES_BEFORE_LOCK
定义,该值为 2
,retries
初始值为 -1
,因此尝试次数为 3
。
如果尝试的次数超过 3
次,就需要对每个 Segment
加锁。
java.util.concurrent.BlockingQueue
是一个队列,在进行检索或移除一个元素的时候,它会等待队列变为非空;当在添加一个元素时,它会等待队列中的可用空间。BlockingQueue
接口是Java集合框架的一部分,主要用于实现生产者-消费者模式。我们不需要担心等待生产者有可用的空间,或消费者有可用的对象,因为它都在BlockingQueue
的实现类中被处理了。Java提供了集中BlockingQueue
的实现,比如ArrayBlockingQueue
、LinkedBlockingQueue
、PriorityBlockingQueue
,、SynchronousQueue
等。