1. Iterator 接口
Iterator 接口,这是一个用于遍历集合中元素的接口,主要包含hashNext(), next(), remove() 三种方法。它的一个子接口 LinkedIterator在它的基础上又添加了三种方法,分别是add(),previous(),hasPrevious()。也就是说如果是先 Iterator 接口,那么在遍历集合中元素的时候,只能往后遍历,被遍历后的元素不会再遍历到,通常无序集合实现的都是这个接口,比如 HashSet,HashMap;而那些元素有序的集合,实现的一般都是 LinkedIterator 接口,实现这个接口的集合可以双向遍历,既可以通过 next() 访问下一个元素,又可以通过 previous() 访问前一个元素,比如ArrayList。
2. List
List是元素有序并且可以重复的集合。
List的主要实现:ArrayList, LinkedList, Vector。
2.1 ArrayList
总结
- 底层是Object数组存储数据
- 扩容机制: 默认大小是10,扩容是扩容到之前的1.5倍的大小,每次扩容都是将原数组的数据复制进新数组中.。
- 如果是添加到数组的指定位置,那么可能会挪动大量的数组元素,并且可能会触发扩容机制;如果是添加到末尾的话,那么只可能触发扩容机制.
- 查找快,增删慢
重要属性
// 默认初始化容量
private static final int DEFAULT_CAPACITY = 10;
// 空数组
private static final Object[] EMPTY_ELEMENTDATA = {};
// 空数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 存储数据元素的数组
transient Object[] elementData; // non-private to simplify nested class access
// 当前arraylist集合的大小,也就是elementData数组中数据元素的个数
private int size;
EMPTY_ELEMENTDATA 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 这两个分别对应的构造方法是 new ArrayList(0) 和 new ArrayList(),为什么要这么做,其实也是要在第一次add 的时候以示区别,前者扩充后的容量为 1,后者扩充后的容量是 10。
构造方法
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
- 无参构造器:造方法中直接将 elementData 指向 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 空数组,这个时候该ArrayList的size为初始值 0。
- 带一个 int 类型的参数的构造器:指定的初始容量小于0时无意义,直接抛出非法参数异常。当initialCapacity 大于 0 时,会直接创建一个 Object 类型的数组,数组的初始大小就是initialCapacity 的值。当 initialCapacity 等于 0 时,会直接将 elementData 指向 EMPTY_ELEMENTDATA 空数组。
- 带一个 Collection 类型的参数的构造器:首先将 Collection 参数通过 toArray 方法转换成数组,并赋值给 elementData,然后对 arraylist 中的 size 进行赋值并判断 size 是否等于 0。当size 为 0 时,直接将 elementData 指向 EMPTY_ELEMENTDATA 空数组。当 size 不为 0 时执行 copyOf 方法。
关于new ArrayList() 的初始容量,在jdk1.6中的确是为10,然而在1.8中,如果只是new ArrayList() ,容量其实是0,当第一次通过add(E e)时,才扩充为10。
add() 方法
/**
* 方式一:直接添加数据元素到 arraylist 的尾部
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
/**
* 方式二:插入数据元素到特定的位置
*/
public void add(int index, E element) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
第一种方式:直接添加数据元素到 arraylist 的尾部。首先会处理 elementData 数组是否需要扩容操作,接着对新添加的数据元素进行赋值操作,size 自增 1,最后 return true 表示数据添加成功。
第二种方式:插入数据元素到特定的位置,首先对下标 index 进行越界判断,然后会对elementData 数组是否需要扩容操作进行处理,接着调用 System.arraycopy 方法将指定角标后的元素后移一位,最后对指定角标位置进行赋值操作并将size自增 1。
扩容
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
}
首先将 elementData 和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA 进行对比,如果elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA,则执行 if 语句体的操作,否则直接调用 ensureExplicitCapacity 方法,将最小容量 minCapacity 作为参数传入。通过ArrayList 的无参构造方法创建 ArrayList 对象后,再调用 add 方法的时候会执行 if 语句体操作,将 minCapacity 重新赋值为 DEFAULT_CAPACITY 和 minCapacity 中的最大值。
接着判断 minCapacity - elementData.length 是否大于 0,当大于0的时候说明当前elementData 数组大小不够用,需要扩容,grow方法就是具体的扩容操作。
private void grow(int minCapacity) {
// 1.首先获取到elementData数组的长度,作为原容量
int oldCapacity = elementData.length;
// 2.新容量 = 原容量 + 原容量/2; 1.5倍扩容
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
// 3.若1.5倍扩容后还不够,则将最小容量作为新容量
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
// 4.限制最大容量
newCapacity = hugeCapacity(minCapacity);
// 5.进行原有数据元素copy处理
elementData = Arrays.copyOf(elementData, newCapacity);
}
add 方法的第二种方式和第一种方式类似,插入数据元素到特定的角标位置,方法中首先会对下角标 index 进行越界判断,然后会对 elementData 数组是否需要扩容操作进行处理,接着调用 System.arraycopy 方法将指定角标后的元素后移一位,最后对指定角标位置进行赋值操作并将 size 自增1。
remove() 方法
public E remove(int index) {
rangeCheck(index);
modCount++;
// 获取到指定下角标位置的数据
E oldValue = elementData(index);
// 计算需要移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
// 返回删除的元素
return oldValue;
}
/*
* 方式二:根据数据元素进行remove操作
*/
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void fastRemove(int index) {
// 1.modCount的值自增1
modCount++;
// 2.计算需要移动的元素个数
int numMoved = size - index - 1;
if (numMoved > 0)
// 3. 指定角标位置后的元素前移一位
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
// 4.将size自减1,并将数组末尾置为null,便于垃圾回收
elementData[--size] = null; // clear to let GC do its work
}
set() 方法
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
第一个参数 index 为下角标,第二个参数 element 为数据元素,表示将下角标 index 处的数据元素赋值为element。在方法中,首先对index下角标进行越界判断,然后获取到角标index处的原数据元素 oldValue,接着将角标 index 处的数据元素赋值为 element,最后返回原有数据元素 oldValue 。
get() 方法
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
clear
public void clear() {
modCount++;
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
并发问题的解决
ArrayList 是线程不安全的,在多线程环境下,可以使用 Vector、Collections.SynchronizedList、CopyOnWriteArrayList
1. Vector
public synchronized void addElement(E obj) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = obj;
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
ArrayList 是线程不安全的,Vector 是线程安全的。
和 ArrayList 差不多,只是在方法上加了 synchronized 锁,扩容为原来的两倍或原容量加扩容因子(构造时指定)。
2. Collections.SynchronizedList
虽然 ArrayList 是线程不安全的,但是通过 Collections.synchronizedList() 方法可以将线程不安全的List转成线程安全的List。
final List<E> list;
SynchronizedList(List<E> list) {
super(list);
this.list = list;
}
SynchronizedList(List<E> list, Object mutex) {
super(list, mutex);
this.list = list;
}
public E get(int index) {
synchronized (mutex) {return list.get(index);}
}
public E set(int index, E element) {
synchronized (mutex) {return list.set(index, element);}
}
public void add(int index, E element) {
synchronized (mutex) {list.add(index, element);}
}
SynchronizedList 类使用了委托 (delegation),实质上存储还是使用了构造时传进来的list,只是将list作为底层存储,对它做了一层包装。
SynchronizedList 的同步,使用的是 synchronized 代码块对 mutex 对象加锁,这个 mutex 对象还能够通过构造函数传进来,也就是说我们可以指定锁定的对象。而 Vector 则使用了 synchronized 方法,同步方法的作用范围是整个方法,所以没办法对同步进行细粒度的控制。而且同步方法加锁的是this对象,没办法控制锁定的对象。这也是 vector 和 SynchronizedList 的一个区别。
3. CopyOnWriteArrayList
属性
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
array 缓存数组只可以通过 getArray(),setArray(Object[] a) 来访问,而且是volatile 修饰的,保证线程可见性,与 ArrayList 不同。
构造器
无参构造方法,ArrayList的无参构造器内是将一个空数组赋给缓冲数组,使用的是延迟初始化,只有在第一次调用add方法时才会去创建一个默认长度为10的数组,并把引用赋给缓冲数组;而CopyOnWriteArrayList是直接在构造器内创建一个长度为0的Object对象数组,并没有采取延迟初始化。
public CopyOnWriteArrayList() {
setArray(new Object[0]);
}
public CopyOnWriteArrayList(Collection<? extends E> c) {
Object[] elements;
if (c.getClass() == CopyOnWriteArrayList.class)
elements = ((CopyOnWriteArrayList<?>)c).getArray();
else {
elements = c.toArray();
if (elements.getClass() != Object[].class)
elements = Arrays.copyOf(elements, elements.length, Object[].class);
}
setArray(elements);
}
// 外部缓存数组构造器,将构造器内的参数数组作为CopyOnWriteArrayList内部的缓冲数组
public CopyOnWriteArrayList(E[] toCopyIn) {
setArray(Arrays.copyOf(toCopyIn, toCopyIn.length, Object[].class));
}
add() 方法
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();
}
}
可见在每一次add方法都将复制一遍数组,并创建一个长度+1的新数组,然后将新添加的元素放入到数组的最后,然后更新缓存数组的引用。
public void add(int index, E element) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
if (index > len || index < 0)
throw new IndexOutOfBoundsException("Index: "+index+
", Size: "+len);
Object[] newElements;
int numMoved = len - index;
if (numMoved == 0)
newElements = Arrays.copyOf(elements, len + 1);
else {
newElements = new Object[len + 1];
System.arraycopy(elements, 0, newElements, 0, index);
System.arraycopy(elements, index, newElements, index + 1,
numMoved);
}
newElements[index] = element;
setArray(newElements);
} finally {
lock.unlock();
}
}
当添加的位置在数组的尾部在只需复制一次数组,当位于中间位置则需要复制两次。
remove() 方法
删除第一个与指定元素相等的集合内元素,我们发现对 remove 方法的优化,即利用 array 数组的线程可见性,无锁获取一个快照,如果在这个快照中没有指定元素则返回false,如果含有指定元素,需要调用remove(Object o, Object[] snapshot, int index)方法加锁进行遍历并删除。即避免了在不含有指定元素的情况下加锁遍历的过程。
public boolean remove(Object o) {
Object[] snapshot = getArray();
int index = indexOf(o, snapshot, 0, snapshot.length);
return (index < 0) ? false : remove(o, snapshot, index);
}
get() 方法
无锁获取元素,性能较高;但是array的元素的变化还没有刷新到主内存上(set方法,通过锁来保证元素的可见性)、或者复制数组的过程中,还没有更新数组引用(add方法);即锁还没有释放,这时另外一个线程去读,所以会出现脏读。
private E get(Object[] a, int index) {
return (E) a[index];
}
public E get(int index) {
return get(getArray(), index);
}
2.2 LinkedList
概述
① LinkedList 是一种双向链表。jdk1.6是双向循环链表,jdk1.7之后就变成了双向链表,去掉了head
② 根据双向链表的特点,会有头节点和尾节点,并且节点之间是通过前驱指针和后继指针来维护关系的,而不是像数组那样通过位置下标来维护节点间关系的。所以既可以从头到尾遍历,又可以从尾到头遍历。
- LinkedList方法内部实现是链表,且内部有fist与last指针控制数据的增加与删除等操作
- LinkedList内部元素是可以重复,且有序的。因为是按照链表进行存储元素的。
- LinkedList线程不安全的,因为其内部添加、删除、等操作,没有进行同步操作。
- LinkedList增删元素速度较快
属性
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
节点:
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
构造方法
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
add() 方法
public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
// 判断是不是第一个节点
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
如果是第一次执行,该方法,first 与 Last 都会指向该节点。如果不是第一次执行。上个节点的 next 会指向新添加的节点,且 last 指向新添加的节点。
addFirst()
public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
linkFirst 方法内部创建了一个新的节点。如果是第一次添加。新节点上个节点为null。如果不是,则新的节点的上个的节点为 first 原来指向的节点,first 指向新添加的节点。
addLast(e)方法原理与addFirst(e)原理差不多。
get(int index)
public E get(int index) {
// 判断位置是否合法
checkElementIndex(index);
return node(index).item;
}
Node<E> node(int index) {
// assert isElementIndex(index);
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
该方法内部先判断 index 的位置是否小于总长度的一半,如果是,则从链表前方遍历,如果不是,则从链表最末尾进行遍历。
remove() 方法
public E remove() {
return removeFirst();
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
unLinkFirst方法将器node中数据置为null,且将frist节点,指向f的下一个节点。并将f的下一个节点的上个节点(也就是f)至为null。
removeLast()方法类似。
并发问题解决办法
方法一: List list = Collections.synchronizedList(new LinkedList());
方法二: 将 LinkedList 全部换成 ConcurrentLinkedQueue
ConcurrentLinkedQueue 采用 CAS 乐观锁的机制实现非阻塞队列
(BlockingQueue 阻塞队列)
3. Map
概述
HashMap 和 LinkedHashMap 的区别
- LinkedHashMap 拥有与 HashMap 相同的底层哈希表结构,即数组 + 单链表 + 红黑树,也拥有相同的扩容机制。
- LinkedHashMap 相比 HashMap 的拉链式存储结构,内部额外通过 Entry 维护了一个双向链表。
- HashMap 元素的遍历顺序不一定与元素的插入顺序相同,而 LinkedHashMap 则通过遍历双向链表来获取元素,所以遍历顺序在一定条件下等于插入顺序。
- LinkedHashMap 可以通过构造参数 accessOrder 来指定双向链表是否在元素被访问后改变其在双向链表中的位置。
HashMap 和 TreeMap 的区别
- HashMap实现了 Map 接口,不保障元素顺序。
- TreeMap实现了SortedMap接口,是一个有序的Map。内部采用红黑树实现,红黑树是一种维护有序数据的高效数据结构
ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
- 底层数据结构: JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现,JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
- Hashtable 和 JDK1.8 之前的 HashMap 的底层数据结构类似都是采用 数组+链表 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- 实现线程安全的方式(重要): ① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
HashMap
HashMap 是非线程安全的,在多线程环境下,可以使用 Hashtable 或 ConcurrentHashMap
Hashtable
属性
public class Hashtable<K,V> extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable {
// Hashtable保存数据的数组
private transient Entry<?,?>[] table;
// hashtable的容量
private transient int count;
// 阈值
private int threshold;
// 负载因子
private float loadFactor;
// 结构性修改
private transient int modCount = 0;
}
- Hashtable 底层是通过数组加链表来实现的。
- Hashtable 并没有太多的常量,比如默认容量大小都是直接写在代码中,而没使用常量。
- 从它的构造函数我们可以知道,Hashtable默认capacity是11,默认负载因子是0.75.。
put方法
// put是synchronized方法
public synchronized V put(K key, V value) {
// 首先就是确保value不能为空
if (value == null) {
throw new NullPointerException();
}
// Makes sure the key is not already in the hashtable.
Entry<?,?> tab[] = table;
int hash = key.hashCode();
// 计算数组的index
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
// 如果index处已经有值,并且通过比较hash和equals方法之后,如果有相同key的替换,返回旧值
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
// 添加数组
addEntry(hash, key, value, index);
return null;
}
private void addEntry(int hash, K key, V value, int index) {
modCount++;
Entry<?,?> tab[] = table;
// 如果容量大于了阈值,扩容
if (count >= threshold) {
// Rehash the table if the threshold is exceeded
rehash();
tab = table;
hash = key.hashCode();
index = (hash & 0x7FFFFFFF) % tab.length;
}
// Creates the new entry.
@SuppressWarnings("unchecked")
Entry<K,V> e = (Entry<K,V>) tab[index];
// 在数组索引index位置保存
tab[index] = new Entry<>(hash, key, value, e);
count++;
}
通过tab[index] = new Entry<>(hash, key, value, e);这一行代码,并且根据Entry的构造方法,我们可以知道,Hashtable是在链表的头部添加元素的,而 HashMap 是尾部添加的,这点可以注意下。
Hashtable计算数组index的方式和HashMap有点不同,int index = (hash & 0x7FFFFFFF) % tab.length;0x7FFFFFFF也就是Integer.MAX_VALUE,也就是2的32次方-1,二进制的话也就是11111111…,那么(hash&0x7FFFFFFF)的含义看来看去好像只有对符号位有效了,就是负数的时候,应该是为了过滤负数,而后面的取模就很简单了,把index的取值限制在数组的长度之内。
protected void rehash() {
int oldCapacity = table.length;
Entry<?,?>[] oldMap = table;
// 扩容为原来的2倍加1
int newCapacity = (oldCapacity << 1) + 1;
// 扩容后的数量校验
if (newCapacity - MAX_ARRAY_SIZE > 0) {
if (oldCapacity == MAX_ARRAY_SIZE)
// Keep running with MAX_ARRAY_SIZE buckets
return;
newCapacity = MAX_ARRAY_SIZE;
}
// 新数组
Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];
modCount++;
// 阈值计算
threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
table = newMap;
// 双层循环,将原数组中数据复制到新数组中
for (int i = oldCapacity ; i-- > 0 ;) {
for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
Entry<K,V> e = old;
old = old.next;
// 重新根据hash计算index
int index = (e.hash & 0x7FFFFFFF) % newCapacity;
e.next = (Entry<K,V>)newMap[index];
newMap[index] = e;
}
}
}
get 方法
public synchronized V get(Object key) {
Entry<?,?> tab[] = table;
int hash = key.hashCode();
int index = (hash & 0x7FFFFFFF) % tab.length;
for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) {
if ((e.hash == hash) && e.key.equals(key)) {
return (V)e.value;
}
}
return null;
}
ConcurrentHashMap
ConcurrentHashMap 取消了 segment 分段锁,而采用 CAS 和 synchronized 来保证并发安全。数据结构跟 HashMap1.8 的结构一样,数组+链表/红黑二叉树。
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
键值均不允许为 null
属性
/**
hash表初始化或扩容时的一个控制位标识量。
负数代表正在进行初始化或扩容操作
-1代表正在初始化
-N 表示有N-1个线程正在进行扩容操作
正数或0代表hash表还没有被初始化,这个数值表示初始化或下一次进行扩容的大小
*/
private transient volatile int sizeCtl;
// 以下两个是用来控制扩容的时候 单线程进入的变量
/**
* The number of bits used for generation stamp in sizeCtl.
* Must be at least 6 for 32bit arrays.
*/
private static int RESIZE_STAMP_BITS = 16;
/**
* The bit shift for recording size stamp in sizeCtl.
*/
private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
/*
* Encodings for Node hash fields. See above for explanation.
*/
static final int MOVED = -1; // hash值是-1,表示这是一个forwardNode节点
static final int TREEBIN = -2; // hash值是-2 表示这时一个TreeBin节点
源码分析
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//ConcurrentHashMap 不允许插入null键,HashMap允许插入一个null键
if (key == null || value == null) throw new NullPointerException();
//计算key的hash值
int hash = spread(key.hashCode());
/**
* static final int spread(int h) {
* return (h ^ (h >>> 16)) & HASH_BITS;
* }
*/
int binCount = 0;
//for循环的作用:因为更新元素是使用CAS机制更新,需要不断的失败重试,直到成功为止。
for (Node<K,V>[] tab = table;;) {
// f:链表或红黑二叉树头结点,向链表中添加元素时,需要synchronized获取f的锁。
Node<K,V> f; int n, i, fh;
//判断Node[]数组是否初始化,没有则进行初始化操作
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//通过hash定位Node[]数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头结点),添加失败则进入下次循环。
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//检查到内部正在移动元素(Node[] 数组扩容)
else if ((fh = f.hash) == MOVED)
//帮助它扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
//锁住链表或红黑二叉树的头结点
synchronized (f) {
//判断f是否是链表的头结点
if (tabAt(tab, i) == f) {
//如果fh>=0 是链表节点
if (fh >= 0) {
binCount = 1;
//遍历链表所有节点
for (Node<K,V> e = f;; ++binCount) {
K ek;
//如果节点存在,则更新value
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
//不存在则在链表尾部添加新节点。
Node<K,V> pred = e;
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
//TreeBin是红黑二叉树节点
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
//添加树节点
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
//如果链表长度已经达到临界值8 就需要把链表转换为树结构
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//将当前ConcurrentHashMap的size数量+1
addCount(1L, binCount);
return null;
}
总结
- 判断 Node[] 数组是否初始化,没有则进行初始化操作
- 通过 hash 定位 Node[] 数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头结点),添加失败则进入下次循环。
- 检查到内部正在扩容,如果正在扩容,就帮助它一块扩容。
- 如果 f!=null,则使用synchronized锁住f元素(链表/红黑二叉树的头元素)
- 如果是Node(链表结构)则执行链表的添加操作。
- 如果是TreeNode(树型结果)则执行树添加操作。
判断链表长度已经达到临界值8 就需要把链表转换为树结构。
总结:
JDK8 中的实现也是锁分离的思想,它把锁分的比 segment(JDK1.5)更细一些,只要 hash 不冲突,就不会出现并发获得锁的情况。它首先使用无锁操作CAS插入头结点,如果插入失败,说明已经有别的线程插入头结点了,再次循环进行操作。如果头结点已经存在,则通过synchronized获得头结点锁,进行后续的操作。性能比segment分段锁又再次提升。
TreeMap
重要属性
public class TreeMap<K,V> extends AbstractMap<K,V>
implements NavigableMap<K,V>, Cloneable, java.io.Serializable
{
// 用于接收外部比较器,插入时用于比对元素的大小
private final Comparator<? super K> comparator;
// 红黑树的根节点
private transient Entry<K,V> root;
// 树中元素个数
private transient int size = 0;
// 其他省略
}
数据结构
TreeMap 采用红黑树的数据结构来实现。树节点 Entry 实现了 Map.Entry,采用内部类的方式实现:
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;
// ...
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return valEquals(key,e.getKey()) && valEquals(value,e.getValue());
}
public int hashCode() {
int keyHash = (key==null ? 0 : key.hashCode());
int valueHash = (value==null ? 0 : value.hashCode());
return keyHash ^ valueHash;
}
public String toString() {
return key + "=" + value;
}
}
因红黑树在构造过程中需要比对元素的大小来决定插入左边还是右边,因此TreeMap里面有一个比较器,可以用默认的,也可以自定义比较器。而ConcurrentHashMap采用key的hash值来比较大小,红黑树的示意图如下图:
初始化与比较器
public TreeMap() {
comparator = null;
}
- 默认构造方法会创建一颗空树。
- 默认使用key的自然顺序来构建有序树,所谓自然顺序,意思是key的类型是什么,就采用该类型的compareTo方法来比较大小,决定顺序。例如key为String类型,就会用String类的compareTo方法比对大小,如果是Integer类型,就用Integer的compareTo方法比对。Java自带的基本数据类型及其装箱类型都实现了Comparable接口的compareTo方法。
- key的类型,必须实现Comparable接口,如果不实现,就没办法完成元素大小的比较来实现有序性的。比如自定义了一个类User来作为key,忘记实现了Comparable接口,就没有一个规则比较User的大小,无法实现TreeMap最重要的有序性。
put() 方法
public V put(K key, V value) {
Entry<K,V> t = root;
if (t == null) {
compare(key, key); // type (and possibly null) check
root = new Entry<>(key, value, null);
size = 1;
modCount++;
return null;
}
int cmp;
Entry<K,V> parent;
// split comparator and comparable paths
Comparator<? super K> cpr = comparator;
if (cpr != null) { // 外部比较器
do {
parent = t;
cmp = cpr.compare(key, t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
else {
if (key == null)
throw new NullPointerException();
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
do {
parent = t;
cmp = k.compareTo(t.key);
if (cmp < 0)
t = t.left;
else if (cmp > 0)
t = t.right;
else
return t.setValue(value);
} while (t != null);
}
Entry<K,V> e = new Entry<>(key, value, parent);
if (cmp < 0)
parent.left = e;
else
parent.right = e;
fixAfterInsertion(e);
size++;
modCount++;
return null;
}
其中方法fixAfterInsertion(e)为红黑树的性质恢复操作,因为插入节点后,可能会破坏红黑树的性质。
get() 方法
public V get(Object key) {
Entry<K,V> p = getEntry(key);
return (p==null ? null : p.value);
}
final Entry<K,V> getEntry(Object key) {
// Offload comparator-based version for sake of performance
if (comparator != null)
// 如果外部比较器,就采用外部比较器比对查找元素
return getEntryUsingComparator(key);
if (key == null)
throw new NullPointerException();
// 采用key的默认比较器
@SuppressWarnings("unchecked")
Comparable<? super K> k = (Comparable<? super K>) key;
Entry<K,V> p = root;
while (p != null) {
int cmp = k.compareTo(p.key);
if (cmp < 0)
p = p.left;
else if (cmp > 0)
p = p.right;
else
return p;
}
return null;
}
remove() 方法
public V remove(Object key) {
Entry<K,V> p = getEntry(key);
if (p == null)
return null;
V oldValue = p.value;
deleteEntry(p);
return oldValue;
}
private void deleteEntry(Entry<K,V> p) {
modCount++;
size--;
// If strictly internal, copy successor's element to p and then make p
// point to successor.
// 情况一:待删除的节点有两个孩子
if (p.left != null && p.right != null) {
// 返回指定Entry的后继者,如果不是,则返回null。
Entry<K,V> s = successor(p);
p.key = s.key;
p.value = s.value;
p = s;
} // p has 2 children
// Start fixup at replacement node, if it exists.
Entry<K,V> replacement = (p.left != null ? p.left : p.right);
// 情况二:待删除节点只有一个孩子
if (replacement != null) {
// Link replacement to parent
replacement.parent = p.parent;
if (p.parent == null)
root = replacement;
else if (p == p.parent.left)
p.parent.left = replacement;
else
p.parent.right = replacement;
// Null out links so they are OK to use by fixAfterDeletion.
p.left = p.right = p.parent = null;
// Fix replacement
if (p.color == BLACK)
fixAfterDeletion(replacement);
} else if (p.parent == null) { // 情况三:根节点
root = null;
} else { //情况四:无任何孩子节点
if (p.color == BLACK)
fixAfterDeletion(p);
if (p.parent != null) {
if (p == p.parent.left)
p.parent.left = null;
else if (p == p.parent.right)
p.parent.right = null;
p.parent = null;
}
}
}
static <K,V> TreeMap.Entry<K,V> successor(Entry<K,V> t) {
if (t == null)
return null;
else if (t.right != null) {// 找右子树中最小元素节点
Entry<K,V> p = t.right;
while (p.left != null)
p = p.left;
return p;
} else {// 下面这段代码根本走不到,因为deleteEntry在调用此方法时传过来的t非null
Entry<K,V> p = t.parent;
Entry<K,V> ch = t;
while (p != null && ch == p.right) {
ch = p;
p = p.parent;
}
return p;
}
}
红黑树的删除分两步:
- 以二叉查找树的方式删除节点。
- 恢复红黑树的性质。
删除分为四种情况:
- 树只有根节点:直接删除即可。
- 待删除节点无孩子:直接删除即可。
- 待删除节点只有一个孩子节点:删除后,用孩子节点替换自己即可。
- 待删除节点有两个孩子:如下:
删除节点 3 的过程:
删除过程:找到节点3右子树中最小的节点2,将3和2节点进行交换,然后删除3节点,3删除后,将原来的4节点变为5节点的子节点。
如果3节点和2节点被替换后,3节点下仍有两个孩子节点,重复利用上述规则删除即可。这种方式的巧妙之处在于,总是将删除的当前节点向叶子节点方向移动,保证最后没有两个孩子节点时就可以执行真正的删除了,而利用右子树的最小节点与自身交换的动作并不会破坏二叉查找树的任何特性。
4. Set
Set 集合中的对象不按特定的方式排序(存入和取出的顺序不一定一致),并且没有重复对象。
Set 的主要实现类:HashSet, TreeSet。
HashSet
- HashSet 底层是使用 HashMap 来保存元素的
- 它不保证集合中存放元素的顺序,即是无序的,且这种顺序可能会随着时间的推移还会改变
- 允许 null 值,且只有一个
- HashSet 不是线程安全的,底层的 HashMap 不是线程安全的,它自然就不是啦,可以使用 Collections.synchronizedSet(new HashSet()) 来创建线程安全的 HashSet
- 集合中的元素不会重复
核心成员变量
//HashSet 底层是基于 HashMap 存储数据,该 map 的 key 就是 HashSet 要存放的数据
private transient HashMap<E,Object> map;
//该变量用来填充 map 的 value 字段,因为 HashSet 关注的是 map 的 Key
private static final Object PRESENT = new Object();
构造器
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
add(E e) 方法
//添加一个元素,如果该元素已经存在,则返回true,如果不存在,则返回false
public boolean add(E e) {
//往map中添加元素,返回null,说明是第一个往map中添加该key
return map.put(e, PRESENT)==null;
}
add 方法调用的是 HashMap 的 put 方法向 HashSet 添加元素,如果要添加元素的 hashcode 已存在,且 equals 相等,则会替换掉旧的值。
- 往hashSet中添加元素,实际是往map成员变量里面添加对应的key和value;
- map 中的key实际就是要添加的元素,value是一个固定的对象;
- 由于 hashMap 中的 key 不能重复,所以 hashSet 不能存储重复元素;
remove(Object o) 方法
//删除指定的元素,删除成功返回true
public boolean remove(Object o) {
//实际是删除map中的一个对象
return map.remove(o)==PRESENT;
}
- 当 hashSet 删除一个元素时,实际是操作 map 删除对应的元素;
- 当删除 map 中一个不存在的对象是,会返回 null,所以这里当返回 PERSENT时,说明之前 hashSet 往 map 中添加过对应的元素,因此,当 remove(o) 返回 true时,说明之前已经存在该元素,并且成功删除;当返回false时,说明之前并没有添加过该对象;
iterator()方法
//获取hashSet的迭代器
public Iterator<E> iterator() {
//调用map获取keySet
return map.keySet().iterator();
}
- hashset 获取迭代器实际是获取 map 的 keySet 的 iterator;
并发问题
在多线程情况下,使用
Set set = new HashSet<>();
会产生 java.util.ConcurrentModificationException异常
解决方式:
Set < Object > set = Collections.synchronizedSet(new HashSet< >());
Set< Object > set = new CopyOnWriteArraySet< >();
CopyOnWriteArraySet
- 底层实现是利用数组,它的上层实现是CopyOnWriteArrayList。
- CopyOnWriteArraySet 是一个集合,所以它是不可以放置重复的元素的,它的取重逻辑是在add中体现的。
- CopyOnWriteArraySet 是利用 CopyOnWriteArrayList 来实现的,因为CopyOnWriteArrayList 是线程安全的,所以 CopyOnWriteArraySet 操作也是线程安全的。
add(E e) 方法
public boolean add(E e) {
/**
* al就是 CopyOnWriteArrayList
* 也就是说CopyOnWriteArraySet内部是用
* CopyOnWriteArrayList来实现的
*/
return al.addIfAbsent(e);
}
/**
* 首先检查原来的数组里面有没有要添加的元素,
* 如果有的话就不要再添加了,
* 如果没有的话,创建一个新的数组,复制之前数组元素并且添加新的元素
*/
public boolean addIfAbsent(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// Copy while checking if already present.
// This wins in the most common case where it is not present
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = new Object[len + 1];
for (int i = 0; i < len; ++i) {
if (eq(e, elements[i]))
return false; // exit, throwing away copy
else
newElements[i] = elements[i];
}
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
remove(Object o)
public boolean remove(Object o) {
Object[] snapshot = getArray();
int index = indexOf(o, snapshot, 0, snapshot.length);
return (index < 0) ? false : remove(o, snapshot, index);
}
TreeSet
重要属性
private transient NavigableMap<E,Object> m; //存放元素的集合
private static final Object PRESENT = new Object(); //m中key 对应的value
构造器
//相同包下可以访问的构造方法,将指定的m赋值为m
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
//无参构造方法,创建一个空的TreeMap对象,并调用上面的构造方法
public TreeSet() {
this(new TreeMap<E,Object>());
}
//指定比较器,并用指定的比较器创建TreeMap对象
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
//将指定的集合C转化为TreeSet
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
//将SortedMap中的元素转化为TreeMap对象
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
TreeSet 的底层是用 TreeMap 实现的。在构造方法中会创建一个 TreeMap 实例,用于存放元素,另外 TreeSet 是有序的,也提供了制定比较器的构造函数,如果没有提供比较器,则采用 key 的自然顺序进行比较大小,如果指定的比较器,则采用指定的比较器,进行key值大小的比较。
主要方法
add() 方法和 remove() 方法都是调用 TreeMap 的方法进行实现
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}