Collection接口
ArrayList
ArrayList默认大小为10。add之前都会确保容量足够,不够的话扩容,扩容默认增加到原先的1.5倍大小,然后拷贝一个新数组出来。修改和查询的时候都会做越界检查,任何修改结构时都会修改modCount。ArrayList中的操作不是线程安全的。
LinkedList
jdk1.8里是由Node串联的链表,有序。存储元素的数据结构是双向链表结构,由存储元素的结点连接而成,每一个节点都包含前一个节点的引用,后一个节点的引用和节点存储的值。当一个新节点插入时,只需要修改其中保持先后关系的节点的引用即可。
transient Node<E> first;
transient Node<E> last;
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
}
CopyOnWriteArrayList
CopyOnWriteArrayList,是一个写入时复制的容器。平时查询的时候,都不需要加锁,随便访问,只有在写入/删除的时候,才会从原来的数据复制一个副本出来,然后修改这个副本,最后把原数据替换成当前的副本。修改操作的同时,读操作不会被阻塞,而是继续读取旧的数据。这点要跟读写锁区分一下。
Java中JDK的源码实现,其实也是非常的简单,在add操作的时候,先使用synchronized进行加锁,保证同时只有1个线程进行变更,在变更的时候,先拷贝出来一个副本,先操作这个副本,操作完成后,再把现有的数据替换成这个副本。
-
优点
- 对于一些读多写少的数据,这种做法的确很不错,例如配置、黑名单、物流地址等变化非常少的数据,这是一种无锁的实现。可以帮我们实现程序更高的并发。
-
缺点
- 这种实现只是保证数据的最终一致性,在添加到拷贝数据而还没进行替换的时候,读到的仍然是旧数据。如果对象比较大,频繁地进行替换会消耗内存,从而引发Java的GC问题,这个时候,我们应该考虑其他的容器,例如ConcurrentHashMap。
Set
Set继承于Collection接口,是一个不允许出现重复元素,并且无序的集合,主要有HashSet和TreeSet两大实现类。
HashSet
private transient HashMap<E,Object> map;
不保证元素的顺序,而且HashSet允许使用 null 元素。
默认构造函数创建一个大小为16的容器,加载因子为0.75(容器的大小始终是2的冥,默认为16,为这个容器的临界值,当存储的元素到了这个临界值,那么容器就会自动扩容。)此实现不同步,为线程不安全的实现,如果有多个线程同时访问这个容器(HashSet),并对里面的元素进行了修改,则需要在外部同步。保证数据的冥等性(幂等是数据中得一个概念,表示N次变换和1次变换的结果相同。)
通过调用元素内部的hashCode和equals方法实现去重,首先调用hashCode方法,比较两个元素的哈希值,如果哈希值不同,直接认为是两个对象,停止比较。如果哈希值相同,再去调用equals方法,返回true,认为是一个对象。返回false,认为是两个对象。
要想按照自己制定的比较规则进行去重,必须重写hashCode和equals方法
TreeSet
private transient NavigableMap<E,Object> m;
TreeSet是一个包含有序的且没有重复元素的集合,通过TreeMap实现。非线程安全。这些元素是使用它们的可比较自然顺序排序的,是由在设置创建时提供的Comparator排序的。底层由treeMap实现。
LinkedHashSet
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
final boolean accessOrder;
LinkedHashSet存储结构是一个双向链表,因此它存储的元素是有序的,此处有序指按添加顺序的顺序。
LinkedHashSet继承自HashSet,初始化为一个容器为16大小,加载因子为0.75的Map容器。源码更少、更简单,唯一的区别是LinkedHashSet内部使用的是LinkHashMap。这样做的意义或者好处就是LinkedHashSet中的元素顺序是可以保证的,也就是说遍历序和插入序是一致的。此链接列表定义了迭代顺序,该迭代顺序可为插入顺序或是访问顺序。
Queue
队列是一种特殊的线性表,它只允许在表的前端进行删除操作,而在表的后端进行插入操作。
Map接口
HashMap
- HashMap存储键值对实现快速存取,允许为null。key值不可重复,若key值重复则覆盖。非同步,线程不安全。底层是hash表,不保证有序(比如插入的顺序)
- 基于hashing的原理,jdk8后采用数组+链表+红黑树的数据结构。我们通过put和get存储和获取对象。当我们给put()方法传递键和值时,先对键做一个hashCode()的计算来得到它在bucket数组中的位置来存储Entry对象。当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象。
- hashMap中put的实现方法
- 计算关于key的hashcode值(与Key.hashCode的高16位做异或运算)
- 如果散列表为空时,调用resize()初始化散列表
- 如果没有发生碰撞,直接添加元素到散列表中去
- 如果发生了碰撞(hashCode值相同),进行三种判断
- 若key地址相同或者equals后内容相同,则替换旧值
- 如果是红黑树结构,就调用树的插入方法
- 链表结构,循环遍历直到链表中某个节点为空,尾插法进行插入,插入之后判断链表个数是否到达变成红黑树的阙值8;也可以遍历到有节点与插入元素的哈希值和内容相同,进行覆盖。
- 如果桶满了大于阀值,则resize进行扩容
- 扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。
- hash是对key的hashCode做hash操作,与高16位做异或运算。因为数组位置的确定用的是与运算,仅仅最后四位有效,设计者将key的哈希值与高16为做异或运算使得在做&运算确定数组的插入位置时,此时的低位实际是高位与低位的结合,增加了随机性,减少了哈希碰撞的次数。
- HashMap默认初始化长度为16,并且每次自动扩展或者是手动初始化容量时,必须是2的幂。
- 为了数据的均匀分布,减少哈希碰撞。因为确定数组位置是用的位运算,若数据不是2的次幂则会增加哈希碰撞的次数和浪费数组空间。(PS:其实若不考虑效率,求余也可以就不用位运算了也不用长度必需为2的幂次)
- 输入数据若不是2的幂,HashMap通过一通位移运算和或运算得到的肯定是2的幂次数,并且是离那个数最近的数字
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
- 当两个对象的hashCode相等时会产生哈希碰撞,若key值相同则替换旧值,不然链接到链表后面,链表长度超过阙值8就转为红黑树存储
- HashMap和HashTable
- 相同点:都是存储key-value键值对的
- 不同点:
- HashMap允许Key-value为null,hashTable不允许;
- hashMap没有考虑同步,是线程不安全的。hashTable是线程安全的,给api套上了一层synchronized修饰;
- HashMap继承于AbstractMap类,hashTable继承与Dictionary类。
- HashMap的迭代器(Iterator)是fail-fast迭代器,而Hashtable的enumerator迭代器不是fail-fast的。所以当有其它线程改变了HashMap的结构(增加或者移除元素),会抛ConcurrentModificationException。
- 容量的初始值和增加方式都不一样
- HashMap默认的容量大小是16;增加容量时,每次将容量变为"原始容量x2"。
- Hashtable默认的容量大小是11;增加容量时,每次将容量变为"原始容量x2 + 1";
- 添加key-value时的hash值算法不同
- HashMap添加元素时,是使用自定义的哈希算法。
- Hashtable没有自定义哈希算法,而直接采用的key的hashCode()。
- loadFactor表示HashMap的拥挤程度,影响hash操作到同一个数组位置的概率。默认loadFactor等于0.75,当HashMap里面容纳的元素已经达到HashMap数组长度的75%时,表示HashMap太挤了,需要扩容,在HashMap的构造器中可以定制loadFactor。
- 选择Integer,String这种不可变的类型的元素作为Key,像对String的一切操作都是新建一个String对象,对新的对象进行拼接分割等,这些类已经很规范的覆写了hashCode()以及equals()方法。作为不可变类天生是线程安全的,
LinkedHashMap
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
......
LinkedHashMap是有序的,且默认为插入顺序。共有两种:插入顺序和访问顺序。
这里accessOrder设置为false,表示不是访问顺序而是插入顺序存储的,这也是默认值,表示LinkedHashMap中存储的顺序是按照调用put方法插入的顺序进行排序的。
LinkedHashMap就是HashMap+双向链表。
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
final boolean accessOrder;
TreeMap
private transient Entry<K,V> root;
/**
* The number of entries in the tree
*/
private transient int size = 0;
/**
* The number of structural modifications to the tree.
*/
private transient int modCount = 0;
static final class Entry<K,V> implements Map.Entry<K,V> {
K key;
V value;
Entry<K,V> left;
Entry<K,V> right;
Entry<K,V> parent;
boolean color = BLACK;
......
TreeMap可以实现存储元素的自动排序。在TreeMap中,键值对之间按键有序,TreeMap的实现基础是平衡二叉树。
TreeMap源码(转载)
ConcurrentHashMap
- HashMap线程不安全,数组+链表+红黑树
- Hashtable线程安全,锁住整个对象,数组+链表
- ConccurentHashMap线程安全,CAS+同步锁,数组+链表+红黑树
- HashMap的key,value均可为null,其他两个不行。
JDK1.8中的改进
- 不采用segment而采用node,锁住node来实现减小锁粒度。
- 设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。
- 使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。
- sizeCtl的不同值来代表不同含义,起到了控制的作用。
- 采用synchronized而不是ReentrantLock