本文整理了java.util.Collection包下的集合类(JDK1.8),同时也加入了java.util.concurrent包下的类,因为面试常会被问到
注:本文只是比较简单的将集合类中的关键点做了介绍,更多的是互相之间的比较,前提是在你对这些集合类有一定了解的情况下,便于复习巩固。更加具体的实现以及一些知识点(比如什么是红黑树,什么是CAS),建议大家查看源码或查看其他文章深入理解。
HashMap
- 一个数组存储,数组内是一个Node,存着hash,key,value,next,next指向下一个node,如果相同hashcode的node个数小于8,用链表,大于8,改成红黑树,根据key值排序
- 初始大小16,扩容2的倍数增长
- 非线程安全,并且在多线程环境下,put操作可能会引起死循环(多个put同时触发rehash,使得链表中出现环,之后再get的时候就会进入死循环,导致cpu利用率接近100%)
HashMap<Integer,Integer> hashMap = new HashMap<Integer,Integer>();
LinkedHashMap
- 在HashMap的基础上,节点还增加了before,after指针,指向前后节点,形成双向链表+哈希表的形式,可以按插入顺序遍历或按get顺序遍历,记录的双向链表的head和tail指针
- 初始大小16,扩容2的倍数增长
LinkedHashMap<Integer,Integer> linkedHashMap = new LinkedHashMap<Integer,Integer>();
linkedHashMap.put(1,1);
linkedHashMap.put(3,3);
linkedHashMap.put(2,2);
linkedHashMap.put(4,4);
Iterator it = linkedHashMap.keySet().iterator();
while(it.hasNext()){
System.out.print(it.next()+ " ");// 1 3 2 4 按插入顺序
}
TreeMap
- 红黑树(平衡二叉排序树),一个节点用Entry表示,存着key,value,left,right,parent,color,以key值排序,可以重写Compartor接口的compare方法来选择排序方式,但必须以key值排序,用一个字段size来记录大小
TreeMap<Integer,Integer> treeMap = new TreeMap<Integer,Integer>();
treeMap.put(4,4);
treeMap.put(1,1);
treeMap.put(3,3);
treeMap.put(2,2);
Iterator it2 = treeMap.keySet().iterator();
while(it2.hasNext()){
System.out.print(it2.next()+ " ");// 1 2 3 4 按key值大小顺序排列
}
HashTable
- 与HashMap类似,但是是线程安全的,因为在会冲突的方法前加了synchronized关键词,来保证同步,初始大小11,扩容old*2+1
Hashtable<Integer,Integer> hashTable = new Hashtable<Integer,Integer>();
ConcurrentHashMap
- 在JDK1.8中,启用了一种全新的方式实现,利用CAS算法。它沿用了与它同时期的HashMap版本的思想,底层依然由“数组”+链表+红黑树的方式思想
- CAS算法(compareAndSwap)实现无锁化的修改值的操作,他可以大大降低锁代理的性能消耗。这个算法的基本思想就是不断地去比较当前内存中的
- 变量值与你指定的一个变量值是否相等,如果相等,则接受你指定的修改的值,否则拒绝你的操作。
- get方法不加锁,get方法需要用tabAt方法去读取table[i],来保证取到的内容是最新值(因为java内存模型中每个线程有工作内存,会有缓存,Node里的value和next虽然是volatile保证了可见性,但是next指向的内容不是)
- put会给每个头结点加内置synchronized锁
* Node结构
* class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;//volatile修饰保证并发可见性
volatile Node<K,V> next;
... 省略部分代码
}
- 初始化:第一次put时发生
- sizeCtl默认为0,如果ConcurrentHashMap实例化时有传参数,sizeCtl会是一个2的幂次方的值(容量)。所以执行第一次put操作的线程会执行Unsafe.compareAndSwapInt方法修改sizeCtl为-1,有且只有一个线程能够修改成功,其它线程通过Thread.yield()让出CPU时间片等待table初始化完成。
- 如果链表中节点数binCount >= TREEIFY_THRESHOLD(默认是8),则把链表转化为红黑树结构。
table扩容
1.首先根据运算得到需要遍历的次数i,然后利用tabAt方法获得i位置的元素f,初始化一个forwardNode实例fwd。
2.如果f == null,则在table中的i位置放入fwd,这个过程是采用Unsafe.compareAndSwapObjectf方法实现的,很巧妙的实现了节点的并发移动。
3。如果f是链表的头节点,就构造一个反序链表,把他们分别放在nextTable的i和i+n的位置上,移动完成,采用Unsafe.putObjectVolatile方法给table原位置赋值fwd。
4.如果f是TreeBin节点,也做一个反序处理,并判断是否需要untreeify,把处理的结果分别放在nextTable的i和i+n的位置上,移动完成,同样采用Unsafe.putObjectVolatile方法给table原位置赋值fwd。
5.遍历过所有的节点以后就完成了复制工作,把table指向nextTable,并更新sizeCtl为新数组大小的0.75倍 ,扩容完成。
*
ConcurrentHashMap<Integer,Integer> concurrentHashMap = new ConcurrentHashMap<Integer,Integer>();
HashSet
- 底层就是一个HashMap,数值存在key中,value默认为一个空对象,key值不能重复
HashSet<Integer> hashSet = new HashSet<Integer>();
TreeSet
- 底层就是一个TreeMap,数值存在key中,value默认为一个空对象,key值不能重复
TreeSet<Integer> treeSet = new TreeSet<Integer>();
StringBuffer
- 底层用char数组实现线程安全的,通过在方法前加synchronized关键字来保证同步,默认大小16,
- 扩容方式:原大小乘2加2,如果乘2加2之后还不够用,直接用minCapacity,得到大小后用Arrays.copy()复制到新数组上,下面是扩容的源码
//StringBuffer扩容源码
private int newCapacity(int minCapacity) { // minCapacity是length+新加入的字符串长度和,即append之后的长度
// overflow-conscious code
int newCapacity = (value.length << 1) + 2;//length是原长度
if (newCapacity - minCapacity < 0) {
newCapacity = minCapacity;
}
return (newCapacity <= 0 || MAX_ARRAY_SIZE - newCapacity < 0) //MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
? hugeCapacity(minCapacity)
: newCapacity;
}
StringBuffer sb = new StringBuffer();
StringBuilder
- 底层用char数组实现,与StringBuilder类似,但是不是线程安全的,默认大小16,扩容方式同StringBuffer,单线程下建议用StringBuilder,速度比较快,因为不需要加锁的消耗
StringBuilder sb2 = new StringBuilder();
ArrayList
- 底层用数组实现(Object[]),查询效率高,插入效率低,非线程安全,默认大小10
- 扩容:乘1.5,即oldCapacity + (oldCapacity >> 1),还不够用,直接用minCapacity,得到大小后用Arrays.copy()复制到新数组上
//ArrayList扩容源码
private void grow(int minCapacity) {//minCapacity是length+新加入的数据长度和,即add之后的长度
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);//扩容1.5倍
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)//MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
List<Integer> l1 = new ArrayList<Integer>();
LinkedList
- 底层用双向链表实现,一个节点用Node表示,存储item,prev,next,记录头、尾节点指针,插入数组快,查找时如果index小于size / 2,从头开始找,否则从尾开始找
List<Integer> l2 = new LinkedList<Integer>();
Vector
- Vector和ArrayList一样,底层用数组实现(Object[]),但是是线程安全的,通过synchronized关键词保证同步,默认大小10,扩容和ArrayList一样
Vector<Integer> vector = new Vector<Integer>();
Stack
- Stack继承了Vecator,底层用数组实现(Object[]),但是是线程安全的,通过synchronized关键词保证同步,通过addElement和removeElement来posh和pop
Stack<Integer> stack = new Stack<Integer>();
Queue
- 底层就是靠LinkedList实现的,底层用双向链表实现
Queue<Integer> queue = new LinkedList<Integer>();
PriorityQueue
- 优先级队列,底层用小根堆实现,可以重写Compartor接口的compare方法来选择排序方式
PriorityQueue<Integer> priorityQueue = new PriorityQueue<Integer>();
ArrayBlockingQueue
- 基于数组的阻塞队列实现,在ArrayBlockingQueue内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,
- 除了一个定长数组外,ArrayBlockingQueue内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置
- ArrayBlockingQueue在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue
BlockingQueue<Integer> ArrayBlockingQueue = new ArrayBlockingQueue<Integer>(10);
LinkedBlockingQueue
- 基于链表的阻塞队列,同ArrayListBlockingQueue类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成)
- 采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据
- 如果不指定初始容量默认大小是Integer.MAX_VALUE
BlockingQueue<Integer> LinkedBlockingQueue = new LinkedBlockingQueue<Integer>();
PriorityBlockingQueue
- 基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator对象来决定),但需要注意的是PriorityBlockingQueue并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue时,内部控制线程同步的锁采用的是公平锁。
BlockingQueue<Integer> PriorityBlockingQueue = new PriorityBlockingQueue<Integer>();
SynchronousQueue
- 一种无缓冲的等待队列,类似于无中介的直接交易,相对于有缓冲的BlockingQueue来说,少了一个中间经销商的环节(缓冲区)
- 声明一个SynchronousQueue有两种不同的方式,它们之间有着不太一样的行为。公平模式和非公平模式
* 如果采用公平模式:SynchronousQueue会采用公平锁,并配合一个FIFO队列来阻塞多余的生产者和消费者,从而体系整体的公平策略;
* 但如果是非公平模式(SynchronousQueue默认):SynchronousQueue采用非公平锁,同时配合一个LIFO队列来管理多余的生产者和消费者
BlockingQueue<Integer> SynchronousQueue = new SynchronousQueue<Integer>();
最后附上一张Collection体系框架图: