ArrayList 的底层是动态数组,它的容量能动态增长。在添加大量元素前,应用可以使用ensureCapacity操作增加 ArrayList 实例的容量。ArrayList 继承了 AbstractList ,并实现了 List 接口。
- ArrayList在内存不够时默认是扩展50% + 1个,Vector是默认扩展1倍。
- Vector属于线程安全级别的,但是大多数情况下不使用Vector,因为操作Vector效率比较低。
- ArrayList基于动态数组实现;LinkedList基于链表实现。
- 对于随机index访问的get和set方法,ArrayList的速度要优于LinkedList。因为ArrayList直接通过数组下标直接找到元素;LinkedList要移动指针遍历每个元素直到找到为止。
- 新增和删除元素,LinkedList的速度要优于ArrayList。因为ArrayList在新增和删除元素时,可能扩容和复制数组;LinkedList实例化对象需要时间外,只需要修改指针即可。
以 Map 结尾的类都实现了 Map 接口,其他所有的类都实现了 Collection 接口。
HashMap 使用数组+链表+红黑树(JDK1.8增加了红黑树部分)实现的, 链表长度大于8(TREEIFY_THRESHOLD)时,会把链表转换为红黑树,红黑树节点个数小于6(UNTREEIFY_THRESHOLD)时才转化为链表,防止频繁的转化。
Hash算法:取key的hashCode值、高位运算、取模运算。
h=key.hashCode() //第一步 取hashCode值
h^(h>>>16) //第二步 高位参与运算,减少冲突
return h&(length-1); //第三步 取模运算
在JDK1.8的实现中,优化了高位运算的算法,通过hashCode()的高16位异或低16位实现的:这么做可以在数组比较小的时候,也能保证考虑到高低位都参与到Hash的计算中,可以减少冲突,同时不会有太大的开销。
1.8扩容机制:当元素个数大于threshold时,会进行扩容,使用2倍容量的数组代替原有数组。采用尾插入的方式将原数组元素拷贝到新数组。1.8扩容之后链表元素相对位置没有变化,而1.7扩容之后链表元素会倒置。
1.7链表新节点采用的是头插法,这样在线程一扩容迁移元素时,会将元素顺序改变,导致两个线程中出现元素的相互指向而形成循环链表,1.8采用了尾插法,避免了这种情况的发生。
原数组的元素在重新计算hash之后,因为数组容量n变为2倍,那么n-1的mask范围在高位多1bit。在元素拷贝过程不需要重新计算元素在数组中的位置,只需要看看原来的hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变成“原索引+oldCap”(根据e.hash & (oldCap - 1) == 0判断) 。这样可以省去重新计算hash值的时间,而且由于新增的1bit是0还是1可以认为是随机的,因此resize的过程会均匀的把之前的冲突的节点分散到新的bucket。
put方法流程:
- 如果table没有初始化就先进行初始化过程
- 使用hash算法计算key的索引
- 判断索引处有没有存在元素,没有就直接插入
- 如果索引处存在元素,则遍历插入,有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入
- 链表的数量大于阈值8,就要转换成红黑树的结构
- 添加成功后会检查是否需要扩容
参考链接:
http://www.importnew.com/20386.html
为什么使用红黑树而不使用AVL树?
ConcurrentHashMap 在put的时候会加锁,使用红黑树插入速度更快,可以减少等待锁释放的时间。红黑树是对AVL树的优化,只要求部分平衡,用非严格的平衡来换取增删节点时候旋转次数的降低,提高了插入和删除的性能。
HashMap和Hashtable都实现了Map接口
- HashMap可以接受为null的键值(key)和值(value),key为null的键值对放在下标为0的头结点的链表中,而Hashtable则不行。
- HashMap是非线程安全的,HashTable是线程安全的。Jdk1.5提供了ConcurrentHashMap,它是HashTable的替代。
- Hashtable很多方法是同步方法,在单线程环境下它比HashMap要慢。
- 哈希值的使用不同,HashTable直接使用对象的hashCode。而HashMap重新计算hash值。
HashMap是无序的,迭代HashMap所得到元素的顺序并不是它们最初放到HashMap的顺序,即不能保持它们的插入顺序。
LinkedHashMap继承于HashMap,是HashMap和LinkedList的融合体,具备两者的特性。每次put操作都会将entry插入到双向链表的尾部。
TreeMap是一个能比较元素大小的Map集合,会对传入的key进行了大小排序。可以使用元素的自然顺序,也可以使用集合中自定义的比较器来进行排序。
public class TreeMap<K,V>
extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable {
}TreeMap 的继承结构:
TreeMap的特点:
- TreeMap是有序的key-value集合,通过红黑树实现。根据键的自然顺序进行排序或根据提供的Comparator进行排序。
- TreeMap继承了AbstractMap,实现了NavigableMap接口,支持一系列的导航方法,给定具体搜索目标,可以返回最接近的匹配项。如floorEntry()、ceilingEntry()分别返回小于等于、大于等于给定键关联的Map.Entry()对象,不存在则返回null。lowerKey()、floorKey、ceilingKey、higherKey()只返回关联的key。
HashSet 基于 HashMap 实现。放入HashSet中的元素实际上由HashMap的key来保存,而HashMap的value则存储了一个静态的Object对象。
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable {
static final long serialVersionUID = -5024744406713321676L;
private transient HashMap<E,Object> map; //基于HashMap实现
//...
}ArrayDeque实现了双端队列,内部使用循环数组实现,默认大小为16。
特点:
-
在两端添加、删除元素的效率较高
-
根据元素内容查找和删除的效率比较低。
-
没有索引位置的概念,不能根据索引位置进行操作。
ArrayDeque和LinkedList都实现了Deque接口,如果只需要从两端进行操作,ArrayDeque效率更高一些。如果同时需要根据索引位置进行操作,或者经常需要在中间进行插入和删除(LinkedList有相应的 api,如add(int index, E e)),则应该选LinkedList。
ArrayDeque和LinkedList都是线程不安全的,可以使用Collections工具类中synchronizedXxx()转换成线程同步。
Iterator模式用同一种逻辑来遍历集合。它可以把访问逻辑从不同类型的集合类中抽象出来,不需要了解集合内部实现便可以遍历集合元素,集合是数组还是链表实现的无所谓,统一使用 Iterator 提供的接口去遍历。
主要有三个方法:hasNext()、next()和remove()。
for(Iterator it = c.iterater(); it.hasNext(); ) { ... }Iterator的增强版,增加了以下功能:
-
ListIterator有add()方法,可以向List中添加对象。
-
hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。
-
nextIndex()和previousIndex()定位索引位置。
-
set() 实现对象的修改。
public interface ListIterator<E> extends Iterator<E> {
boolean hasNext();
E next();
boolean hasPrevious();
E previous();
int nextIndex();
int previousIndex();
void remove();
void set(E var1);
void add(E var1);
}JDK 提供的这些容器大部分在 java.util.concurrent 包中。
- ConcurrentHashMap: 线程安全的 HashMap
- CopyOnWriteArrayList: 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector.
- ConcurrentLinkedQueue: 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。
- BlockingQueue: 阻塞队列接口,JDK 内部通过链表、数组等方式实现了这个接口。非常适合用于作为数据共享的通道。
- ConcurrentSkipListMap: 跳表的实现。使用跳表的数据结构进行快速查找。
多线程环境下,使用Hashmap进行put操作会引起死循环,应该使用支持多线程的 ConcurrentHashMap。
private static final int MAXIMUM_CAPACITY = 1 << 30;
private static final int DEFAULT_CAPACITY = 16;
static final int TREEIFY_THRESHOLD = 8;
static final int UNTREEIFY_THRESHOLD = 6;
static final int MIN_TREEIFY_CAPACITY = 64;
static final int MOVED = -1; // 表示正在转移
static final int TREEBIN = -2; // 表示已经转换成树
static final int RESERVED = -3; // hash for transient reservations
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
transient volatile Node<K,V>[] table;//默认没初始化的数组,用来保存元素
private transient volatile Node<K,V>[] nextTable;//转移的时候用的数组
/**
* 用来控制表初始化和扩容的,默认值为0,当在初始化的时候指定了大小,这会将这个大小保存在sizeCtl中,大小为数组的0.75
* 当为负的时候,说明表正在初始化或扩张,
* -1表示初始化
* -(1+n) n:表示活动的扩张线程
*/
private transient volatile int sizeCtl;
```JDK1.7中的ConcurrentHashmap主要使用Segment来实现减小锁粒度。Segment继承了ReentrantLock,所以它就是一种可重入锁。默认分配16个segment,允许16个线程并发执行。Segment维护了一个HashEntry**数组**,对于同一个Segment的操作才需考虑线程同步,不同的Segment则无需考虑。当要统计size时,比较统计前后modCount是否发生变化。如果没有变化,则直接返回size。否则,需要依次锁住所有的Segment来计算。当长度过长碰撞会很频繁,链表的增改删查操作都会消耗很长的时间,影响性能。JDK1.8 ConcurrentHashMap取消了segment分段锁,而采用CAS和synchronized来保证并发安全。数据结构采用数组+链表/红黑二叉树。synchronized只锁定当前链表或红黑二叉树的首节点,相比1.7锁定HashEntry数组,锁粒度更小,支持更高的并发量。当链表长度过长时,Node会转换成TreeNode,提高查找速度。
在put的时候需要锁住Segment,保证并发安全。调用get的时候不加锁,因为node数组成员val和指针next是用volatile修饰的,更改后的值会立刻刷新到主存中,保证了可见性,node数组table也用volatile修饰,保证在运行过程对其他线程具有可见性。
transient volatile Node<K,V>[] table;
static class Node<K,V> implements Map.Entry<K,V> {
volatile V val;
volatile Node<K,V> next;
}put 操作流程:
- 如果table没有初始化就先进行初始化过程
- 使用hash算法计算key的位置
- 如果这个位置为空则直接CAS插入,如果不为空的话,则取出这个节点来
- 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制
- 如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作,这里有两种情况,一种是链表形式就直接遍历到尾端插入或者覆盖掉相同的key,一种是红黑树就按照红黑树结构插入
- 链表的数量大于阈值8,就会转换成红黑树的结构或者进行扩容(table长度小于64)
- 添加成功后会检查是否需要扩容
数组扩容transfer方法中会设置一个步长,表示一个线程处理的数组长度,最小值是16。在一个步长范围内只有一个线程会对其进行复制移动操作。
区别:
- Hashtable通过使用synchronized修饰方法的方式来实现多线程同步,因此,Hashtable的同步会锁住整个数组。在高并发的情况下,性能会非常差。ConcurrentHashMap采用了更细粒度的锁来提高在并发情况下的效率。注:Synchronized容器(同步容器)也是通过synchronized关键字来实现线程安全,在使用的时候会对所有的数据加锁。
- Hashtable默认的大小为11,当达到阈值后,每次按照下面的公式对容量进行扩充:newCapacity = oldCapacity * 2 + 1。ConcurrentHashMap默认大小是16,扩容时容量扩大为原来的2倍。
ConcurrentHashMap 和 Hashtable 的key和value不能为null。
HashMap.java 部分源码:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);//key为null时,hash值为0
}ConcurrentHashMap.java 部分源码:
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
......
}ConcurrentHashmap和Hashtable都支持并发,当你通过get(k)获取对应的value时,如果获取到的是null时,无法判断是key 对应的 value 为 null,还是这个 key 从来没有做过映射,在多线程里面是模糊不清的,所以不让put null。HashMap用于非并发场景,可以通过contains(key)来判断是否存在key。而支持并发的Map在调用m.get(key)之后,再调用m.contains(key),两个调用之间可能有其他线程删除了key,得到的结果不准确,产生多线程安全问题。因此ConcurrentHashMap 和 Hashtable 的key和value不能为null。
写时复制。当我们往容器添加元素时,不直接往容器添加,而是先将当前容器进行复制,复制出一个新的容器,然后往新的容器添加元素,添加完元素之后,再将原容器的引用指向新容器。这样做的好处就是可以对CopyOnWrite容器进行并发的读而不需要加锁,因为当前容器不会被修改。
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock(); //add方法需要加锁
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();
}
}从JDK1.5开始Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器,它们是CopyOnWriteArrayList和CopyOnWriteArraySet。
CopyOnWriteArrayList中add方法添加的时候是需要加锁的,保证同步,避免了多线程写的时候复制出多个副本。读的时候不需要加锁,如果读的时候有其他线程正在向CopyOnWriteArrayList添加数据,还是可以读到旧的数据。
缺点:
- 内存占用问题。由于CopyOnWrite的写时复制机制,在进行写操作的时候,内存里会同时驻扎两个对象的内存。
- CopyOnWrite容器不能保证数据的实时一致性,可能读取到旧数据。
非阻塞队列。高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,通过 CAS 操作实现。
如果对队列加锁的成本较高则适合使用无锁的 ConcurrentLinkedQueue 来替代。适合在对性能要求相对较高,同时有多个线程对队列进行读写的场景。
非阻塞队列中的几种主要方法: add(E e) : 将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则会抛出异常; remove() :移除队首元素,若移除成功,则返回true;如果移除失败(队列为空),则会抛出异常; offer(E e) :将元素e插入到队列末尾,如果插入成功,则返回true;如果插入失败(即队列已满),则返回false; poll() :移除并获取队首元素,若成功,则返回队首元素;否则返回null; peek() :获取队首元素,若成功,则返回队首元素;否则返回null
对于非阻塞队列,一般情况下建议使用offer、poll和peek三个方法,不建议使用add和remove方法。因为使用offer、poll和peek三个方法可以通过返回值判断操作成功与否,而使用add和remove方法却不能达到这样的效果。
阻塞队列是java.util.concurrent包下重要的数据结构,BlockingQueue提供了线程安全的队列访问方式:当阻塞队列进行插入数据时,如果队列已满,线程将会阻塞等待直到队列非满;从阻塞队列取数据时,如果队列已空,线程将会阻塞等待直到队列非空。并发包下很多高级同步类的实现都是基于BlockingQueue实现的。BlockingQueue 适合用于作为数据共享的通道。
使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入队和出队用不同的锁)等方式来实现。
阻塞队列和一般的队列的区别就在于:
- 多线程支持,多个线程可以安全的访问队列
- 阻塞操作,当队列为空的时候,消费线程会阻塞等待队列不为空;当队列满了的时候,生产线程就会阻塞直到队列不满。
| 方法\处理方式 | 抛出异常 | 返回特殊值 | 一直阻塞 | 超时退出 |
|---|---|---|---|---|
| 插入方法 | add(e) | offer(e) | put(e) | offer(e,time,unit) |
| 移除方法 | remove() | poll() | take() | poll(time,unit) |
| 检查方法 | element() | peek() | 不可用 | 不可用 |
JDK 7 提供了7个阻塞队列,如下
1、ArrayBlockingQueue
有界阻塞队列,底层采用数组实现。ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不能保证线程访问队列的公平性,参数fair可用于设置线程是否公平访问队列。为了保证公平性,通常会降低吞吐量。
private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);//fair2、LinkedBlockingQueue
LinkedBlockingQueue是一个用单向链表实现的有界阻塞队列,可以当做无界队列也可以当做有界队列来使用。通常在创建 LinkedBlockingQueue 对象时,会指定队列最大的容量。此队列的默认和最大长度为Integer.MAX_VALUE。此队列按照先进先出的原则对元素进行排序。与 ArrayBlockingQueue 相比起来具有更高的吞吐量。
3、PriorityBlockingQueue
支持优先级的无界阻塞队列。默认情况下元素采取自然顺序升序排列。也可以自定义类实现compareTo()方法来指定元素排序规则,或者初始化PriorityBlockingQueue时,指定构造参数Comparator来进行排序。
PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容。
PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的(comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。
4、DelayQueue
支持延时获取元素的无界阻塞队列。队列使用PriorityBlockingQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。
5、SynchronousQueue
不存储元素的阻塞队列,每一个put必须等待一个take操作,否则不能继续添加元素。支持公平访问队列。
SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。队列本身不存储任何元素,非常适合传递性场景。SynchronousQueue的吞吐量高于LinkedBlockingQueue和ArrayBlockingQueue。
6、LinkedTransferQueue
由链表结构组成的无界阻塞TransferQueue队列。相对于其他阻塞队列,多了tryTransfer和transfer方法。
transfer方法:如果当前有消费者正在等待接收元素(take或者待时间限制的poll方法),transfer可以把生产者传入的元素立刻传给消费者。如果没有消费者等待接收元素,则将元素放在队列的tail节点,并等到该元素被消费者消费了才返回。
tryTransfer方法:用来试探生产者传入的元素能否直接传给消费者。如果没有消费者在等待,则返回false。和上述方法的区别是该方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。
JDK使用通知模式实现阻塞队列。所谓通知模式,就是当生产者往满的队列里添加元素时会阻塞生产者,当消费者消费了一个队列中的元素后,会通知生产者当前队列可用。
ArrayBlockingQueue使用Condition来实现:
private final Condition notEmpty;
private final Condition notFull;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0) // 队列为空时,阻塞当前消费者
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal(); // 队列不为空时,通知消费者获取元素
}



