Java容器
1、概述

Collection:存储对象的集合

1、Set
1、TreeSet:基于红黑树实现,支持有序性操作。查找效率不如HashSet。查找的时间复杂度为O(logN)
2、HashSet:基于哈希表HashMap实现,支持快速查找但不有序。失去了元素插入顺序信息。查找的时间复杂度为O(1)
3、LinkedHashSet:内部使用双向链表维护元素的插入顺序,有HashSet的查找效率。
2、List
1、ArrayList:基于动态数组实现,支持随机访问。
2、Vector:类似ArrayList,但是线程安全。性能不如ArrayList,考虑线程并发访问的情况下使用。
3、LinkedList:基于双向链表实现,只能顺序访问,但是可以快速插入和删除元素。可以实现栈、队列、双向队列。
3、Queue
1、LinkedList:可以用来实现双向队列
2、PriorityQueue:基于堆结构实现,可以用来实现优先队列

Map存储键值对的映射表(两个对象)

1、TreeMap:基于红黑树实现。
2、HashMap:基于哈希表实现。
3、LinkedHashMap:使用双向链表维护元素的插入顺序或者最近最少使用顺序(LRU)
4、HashTable:类似HashMap,线程安全的,同一时刻多个线程同时写入 HashTable 不会导致数据不一致。
--像Vector中stack是遗留类,不应使用它。
--使用 ConcurrentHashMap 来支持线程安全,效率会更高,因为 ConcurrentHashMap 引入了分段锁。--要单独看
2、迭代器模式
一般来说,类集框架中对集合的输出提供了四种方式:Iterator、ListIterator、Enumeration、foreach
分别是迭代输出、双向迭代输出、枚举输出及foreach输出

Collection 继承了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
从 JDK 1.5 之后可以使用 foreach 方法来遍历实现了 Iterable 接口的聚合对象。
Collection接口中有toArray()方法可以将集合保存的数据转为对象数组返回。
3、适配器模式
java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
@SafeVarargs
public static <T> List<T> asList(T... a)
参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。–Integer
#1
Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr);
#2
List list = Arrays.asList(1, 2, 3);
源码
1、ArrayList
1、概述
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
private static final long serialVersionUID = 8683452581122892189L;
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--游离态,默认不序列化
private int size;
}
ArrayList基于数组实现,支持快速随机访问。RandomAccess接口标识该类支持快速随机访问。
数组默认大小为10
2、扩容
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
/**
其中modCount定义在抽象类AbstractList中,定义为游离态的变量。---标记修改次数
该字段被Iterator以及ListIterator的实现类所使用,如果该值被意外更改,Iterator或者ListIterator 将抛出ConcurrentModificationException异常,
这是jdk在面对迭代遍历的时候为了避免不确定性而采取的快速失败原则。
*/
protected transient int modCount = 0;
代码的逻辑是:如果需要往数组里面add元素,通过ensureCapacityInternal()方法来保证容量足够,如果容量不够,就调用grow方法进行扩容。newCapacity = oldCapacity + (oldCapacity >> 1);新容量为旧容量约1.5倍。
elementData = Arrays.copyOf(elementData, newCapacity);
扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
3、删除元素
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;
}
System.arraycopy(elementData, index+1, elementData, index, numMoved);
通过arraycopy方法将idx+1后面的元素都复制到idx位置上,时间复杂度为O(N),删除元素代价高。
4、序列化
ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。
保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
transient Object[] elementData; // non-private to simplify nested class access
ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。
反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
5、 Fail-Fast
/**
其中modCount定义在抽象类AbstractList中,定义为游离态的变量。---标记修改次数
该字段被Iterator以及ListIterator的实现类所使用,如果该值被意外更改,Iterator或者ListIterator 将抛出ConcurrentModificationException异常,
这是jdk在面对迭代遍历的时候为了避免不确定性而采取的快速失败原则。
*/
protected transient int modCount = 0;
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException。
2、Vector
1、同步
实现与 ArrayList 类似,但是使用了 synchronized 进行同步,线程安全。
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
2、扩容
Vector的构造函数:
与ArrayList不同,传入了capacityIncrement参数
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
其他函数类似ArrayList,grow方法是扩容时使容量 capacity 增长 capacityIncrement。
如果这个参数的值小于等于 0,扩容时每次都令 capacity 为原来的两倍。
调用没有 capacityIncrement 的构造函数时,capacityIncrement 值被设置为 0,
也就是说默认情况下 Vector 每次扩容时容量都会翻倍。
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);
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector() {
this(10);
}
3、与ArrayList作比较
1、Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制;
2、Vector 每次扩容请求其大小的 2 倍(也可以通过构造函数设置增长的容量),而 ArrayList 是 1.5 倍。
3、Vector替代
//1、可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList。
List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list);
//2、也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
List<String> list = new CopyOnWriteArrayList<>();
3、CopyOnWriteArrayList
在并发编程开发包java.util.concurrent,JUC中。–线程安全基于读写分离
1、读写分离
1、每次执行写操作add都是在一个复制的数组上进行,读操作get则是在原始数组中进行,读写分离,互不影响
2、每次写操作add需要加锁,这里是重入锁ReentrantLock ,防止并发写入时导致写入数据丢失。
3、写操作结束后,通过setArray方法将原始数组指向新的复制数组。
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();
}
}
final void setArray(Object[] a) {
array = a;
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
2、适用场景
写操作的同时允许读操作,大大提高了读操作的性能,适合读多写少的应用场景。
缺陷:
1、内存占用高:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右;
2、数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。
因此,不适合内存敏感及对实时性要求高的场景。
4、LinkedList
1、概述
public class LinkedList<E> extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
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;
}
}

LinkedList基于双向链表Deque实现,使用Node–存放每个节点的prev和next节点–存储链表节点信息。
每个LinkedList存储first和last指针。
为追求效率LinkedList没有实现同步(synchronized),如果需要多个线程并发访问,可以先采用Collections.synchronizedList()方法对其进行包装。
2、与ArrayList比较:
ArrayList基于动态数组实现,LinkedList基于双向链表实现。
数组支持随机访问,但是插入删除的代价高,需要移动大量元素。
链表不支持随机访问,但是插入删除操作只需要改变指针。
5、HashMap & HashSet
HashSet 和 HashMap在Java中有相同的实现,HashSet基于HashMap进行实现,进行包装。
HashSet里面有一个HashMap(适配器模式)。
HashMap实现了Map接口,即允许放入key为null的元素,也允许插入value为null的元素;除该类未实现同步外,其余跟Hashtable大致相同;跟TreeMap不同,该容器不保证元素顺序,根据需要该容器可能会对元素重新哈希,元素的顺序也会被重新打散,因此不同时间迭代同一个HashMap的顺序可能会不同。
根据对冲突的处理方式不同,哈希表有两种实现方式,一种开放地址方式(Open addressing),另一种是冲突链表方式(Separate chaining with linked lists)。Java HashMap采用的是冲突链表方式。
JDK1.7:
1、存储结构
transient Entry[] table;
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;
V value;
Entry<K,V> next;
int hash;
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
}
内部包含了一个Entry类型的数组,用于存储键值对。
Entry是一个链表结构。包含四个字段:next、key、value、hashcode。
table数组中每个位置被当成一个桶,每个桶存放一个链表。
HashMap使用拉链法解决冲突,同一个链表中存放哈希值和散列桶取模运算结果相同的Entry。
存储结构为数组+链表形式

从上图容易看出,如果选择合适的哈希函数,put()和get()方法可以在常数时间内完成。但在对HashMap进行迭代时,需要遍历整个table以及后面跟的冲突链表。因此对于迭代比较频繁的场景,不宜将HashMap的初始大小设的过大。
有两个参数可以影响HashMap的性能: 初始容量(inital capacity)和负载系数(load factor)。
初始容量指定了初始table的大小,负载系数用来指定自动扩容的临界值。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。
将对象放入到HashMap或HashSet中时,有两个方法需要特别关心: hashCode()和equals()。
hashCode()方法决定了对象会被放到哪个bucket里,当多个对象的哈希值冲突时,
equals()方法决定了这些对象是否是“同一个对象”。
如果要将自定义的对象放入到HashMap或HashSet中,需要*@Override*hashCode()和equals()方法。
2、拉链法工作原理
举例:new一个HashMap结构,往中添加三个键值对。
新建一个 HashMap,默认大小为 16;
插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。
插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。
插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。
----其中链表的插入,是以头插法方式进行的。 <K3,V3> 不是插在 <K2,V2> 后面,而是插入在链表头部。

完成查找操作:
1、计算键值对所在的桶
2、在链表上顺序查找,时间复杂度和链表的长度成正比。
3、put操作
public V put(K key, V value) {
if (table == EMPTY_TABLE) {
inflateTable(threshold);
}
// 键为 null 单独处理
if (key == null)
return putForNullKey(value);
int hash = hash(key);
// 确定桶下标
int i = indexFor(hash, table.length);
// 先找出是否已经存在键为 key 的键值对,如果存在的话就更新这个键值对的值为 value
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
Object k;
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
// 插入新键值对
addEntry(hash, key, value, i);
return null;
}
HashMap允许插入Key为null的键值对。但是无法调用null的hashcode方法,因此无法确定键值对的桶下标,只能强制指定一个桶下标进行存放。HashMap使用第0个桶存放键为null的键值对。
private V putForNullKey(V value) {
for (Entry<K,V> e = table[0]; e != null; e = e.next) {
if (e.key == null) {
V oldValue = e.value;
e.value = value;
e.recordAccess(this);
return oldValue;
}
}
modCount++;
addEntry(0, null, value, 0);
return null;
}
同时,确定桶下标之后。使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。
void addEntry(int hash, K key, V value, int bucketIndex) {
if ((size >= threshold) && (null != table[bucketIndex])) {
resize(2 * table.length);
hash = (null != key) ? hash(key) : 0;
bucketIndex = indexFor(hash, table.length);
}
createEntry(hash, key, value, bucketIndex);
}
void createEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
// 头插法,链表头部指向新的键值对
table[bucketIndex] = new Entry<>(hash, key, value, e);
size++;
}
4、确定桶下标
确定一个键值对所在的桶下标。—通过位运算完成取模
—使用hash%capacity确定桶下标。如果capacity为2的n次方,则能大大降低计算桶下标的复杂度。
hash(k)&(table.length-1)等价于hash(k)%table.length,
原因是HashMap要求table.length必须是2的指数,因此table.length-1就是二进制低位全是1,
跟hash(k)相与会将哈希值的高位全抹掉,剩下的就是余数了。
int hash = hash(key);
int i = indexFor(hash, table.length);
//计算hash值
final int hash(Object k) {
int h = hashSeed;
if (0 != h && k instanceof String) {
return sun.misc.Hashing.stringHash32((String) k);
}
h ^= k.hashCode();
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
//确定桶下标
static int indexFor(int h, int length) {
return h & (length-1);
}
5、扩容
设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此查找的复杂度为 O(N/M)。
为了让查找的成本降低,应该使 N/M 尽可能小,因此需要保证 M 尽可能大,也就是说 table 要尽可能大。
HashMap 采用动态扩容来根据当前的 N 值来调整 M 值,使得空间效率和时间效率都能得到保证。
和扩容相关的参数主要有:capacity、size、threshold 和 load_factor。
capacity:代表table的容量大小,默认为16。必须为2的n次方
size:代表键值对的数量。
threshold:代表size的临界值,当s键值对数量大于等于临界值就必须进行扩容。
loadFactor:负载因子,table能够使用的比例,默认值为0.75。threshold = (int)(capacity* loadFactor)。
static final int DEFAULT_INITIAL_CAPACITY = 16;
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
transient Entry[] table;
transient int size;
int threshold;
final float loadFactor;
transient int modCount;
添加元素:
void addEntry(int hash, K key, V value, int bucketIndex) {
Entry<K,V> e = table[bucketIndex];
table[bucketIndex] = new Entry<>(hash, key, value, e);
if (size++ >= threshold)
resize(2 * table.length);
}
需要扩容时,使用resize方法令capacity变为原来的两倍。
jdk1.7 HashMap在进行resize扩容时使用的也是链表头插法
扩容后,链表中键值对的顺序与原来的链表是反过来的
void resize(int newCapacity) {
Entry[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity == MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return;
}
Entry[] newTable = new Entry[newCapacity];
transfer(newTable);
table = newTable;
threshold = (int)(newCapacity * loadFactor);
}
void transfer(Entry[] newTable) {
Entry[] src = table;
int newCapacity = newTable.length;
for (int j = 0; j < src.length; j++) {
Entry<K,V> e = src[j];
if (e != null) {
src[j] = null;
do {
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e;
e = next;
} while (e != null);
}
}
}

扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 中,因此这一步是很费时的。
int i = indexFor(e.hash, newCapacity);
扩容时,需要重新计算桶下标,从而把键值对放到对应的桶上。
HashMap 使用 hash%capacity 来确定桶下标。HashMap capacity 为 2 的 n 次方这一特点能够极大降低重新计算桶下标操作的复杂度。
假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32:
capacity : 00010000
new capacity : 00100000
对于一个 Key,它的哈希值 hash 在第 5 位:
为 0,那么 hash%00010000 = hash%00100000,桶位置和原来一致;
为 1,hash%00010000 = hash%00100000 + 16,桶位置是原位置 + 16。
多线程下扩容可能出现的问题:
jdk1.7 多线程扩容出现死循环:
假如有2个线程:线程1和线程2,
线程1在执行扩容时如上图所示,在newTable[7]处依次插入了a、b、c三个节点,
这时切换到线程2执行扩容插入节点操作。若线程2此时只进行到了插入b节点的操作,
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i];
newTable[i] = e; <—— 假设线程2刚执行完这行代码,当前节点e是b节点
e = next;

若在单线程下,线程2接着应该插入c节点,但是因为线程1的缘故,b的下一个节点不是c节点,反而是a节点
Entry<K,V> next = e.next;
int i = indexFor(e.hash, newCapacity);
e.next = newTable[i]; <—— 线程2执行这行代码,当前节点是a, newTable[i] 指向节点b,
那就是把a的next指向了节点b,这样a和b就相互引用了,形成了一个环;
newTable[i] = e; <—— 线程2执行这行代码, newTable[i] 指向节点a
e = next;

jdk1.8中进行的改进:
扩容时采用链表尾插法,因此不会出现上图的扩容死循环问题。
—但是面试题中,jdk1.8中并发出现数据丢失问题,同样也会出现循环链表死循环问题:
—jdk1.8是在链表转换树或者对树进行操作的时候会出现线程安全的问题。
6、计算数组容量capacity
HashMap 构造函数允许用户传入的容量不是 2 的 n 次方,因为它可以自动地将传入的容量转换为 2 的 n 次方。
–利用原理:mask+1 是大于原始数字的最小的 2 的 n 次方。
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;
}
JDK1.8:
当 Hash 冲突严重时,在桶上形成的链表会变的越来越长,这样在查询时的效率就会越来越低;时间复杂度为 O(N)。
因此从JDK1.8开始,一个桶存储的链表长度大于等于8时会将链表转换为红黑树。—O(N)->O(logN)
JDK1.8中:
1、TREEIFY_THRESHOLD 用于判断是否需要将链表转换为红黑树的阈值。
2、HashEntry 修改为 Node,存放的仍为 key value hashcode next 等数据。

1、put方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)//判断当前桶是否为空,如果桶是空的需要初始化
n = (tab = resize()).length;//----在resize方法中会判断是否进行初始化
if ((p = tab[i = (n - 1) & hash]) == null)//根据hash值定位到具体桶判断是否为空
tab[i] = newNode(hash, key, value, null);//为空说明没有hash冲突,直接在当前位置创建一个新桶
else {//有hash冲突
Node<K,V> e; K k;
//比较当前桶中key、key的hash与写入是否相等,相等赋值给e
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
e = p;
else if (p instanceof TreeNode)//如果当前桶是红黑树,写入红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
for (int binCount = 0; ; ++binCount) {//如果是个链表,就封装一个新节点,写入链表
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);//像是放在后面,1.7是头插法
if (binCount >= TREEIFY_THRESHOLD - 1) // 如果链表大小>=预设值,调用treeifyBin函数
treeifyBin(tab, hash);
break;
}
//在遍历过程中找到key相同(即key值相等、hashcode也相等)时直接退出遍历
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // 如果e不为空,说明存在相同的key,将值覆盖
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)//判断是否进行扩容
resize();
afterNodeInsertion(evict);
return null;
}
—这里将旧值return
—HashSet的add实现:
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
树化:
将链表转换为红黑树之前,会进行判断,如果当前数组的长度小于64,那么会选择先进行数组扩容,而不是直接转成红黑树。
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
2、get方法
public V get(Object key) {
//首先将 key hash 之后取得所定位的桶。如果桶为空则直接返回 null 。
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
//判断桶的第一个位置(有可能是链表、红黑树)的 key 是否为查询的 key,是就直接返回 value。
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
//判断它的下一个是红黑树还是链表。
if (first instanceof TreeNode)//红黑树就按照树的查找方式返回值。
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {//不然就按照链表的方式遍历匹配返回值。
if (e.hash == hash &&((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
对比Hashtable:
1、HashTable使用synchronized进行同步。
2、HashMap可以插入键为null的Entry。
3、HashMap的迭代器是fail-fast迭代器。
4、HashMap不能保证随时间推移Map中的元素次序不会发生变化。
面试题:fail-fast和fail-safe的区别是什么?
1、fail-fast快速失败机制
并发修改:一个或多个线程正遍历一个集合,另一个线程修改了这个集合的内容。
在使用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增删改),则会抛出Concurrent Modification Exception异常。
原理:
迭代器在遍历时直接访问集合中的内容,使用modCount变量计算集合变化的次数。
迭代器每次next()遍历下一个元素之前,都会检测modCount对象是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。–单线程环境下remove方法会使这两个值相等,不抛异常。
注意异常的抛出条件是检测到 modCount != expectedmodCount这个条件。
如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
java.util包下的集合类都是fail-fast的,不能在多线程下发生并发修改(不能在迭代过程中被修改)。
2、fail-safe安全失败机制
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。
原理:
迭代时是对原集合的拷贝进行遍历,在遍历过程中对原集合做的修改不能被迭代器检测到,不会触发Concurrent Modification Exception异常。
缺点:
虽然基于拷贝进行遍历,避免了Concurrent Modification Exception异常,但迭代器无法访问修改后的内容
迭代器遍历的是开始遍历的那一刻的集合的拷贝,遍历期间原集合发生的修改迭代器是不知道的。
(1)需要复制集合,产生大量的无效对象,开销大
(2)无法保证读取的数据是目前原始数据结构中的数据。
java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。
3、二者区别
1、fail-fast迭代器会抛出ConcurrentModification Exception异常,fail-safe迭代器不会。
2、fail-safe迭代器会克隆对象,fail-fast不会。
3、fail-safe迭代器有存储开销。
4、fail-fast迭代器:HashMap、Vector、ArrayList、HashSet
fail-safe迭代器:CopyOnWriteArrayList、ConcurrentHashMap
6、ConcurrentHashMap
无论是 1.7 还是 1.8 其实都能看出 JDK 没有对它做任何的同步操作,所以并发会出问题,甚至出现死循环导致系统不可用。
1、存储结构

由Segment数组、HashEntry组成,数组加链表的形式同HashMap。
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {
//Segment 数组,存放数据时首先需要定位到具体的 Segment 中。
final Segment<K,V>[] segments;
transient Set<K> keySet;
transient Set<Map.Entry<K,V>> entrySet;
}

ConcurrentHashMap的实现与HashMap类似,主要差别是ConcurrentHashMap采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段上的桶,从而使并发度更高(并发度就是分段锁Segment的个数)。
Segment分段锁继承自ReentrantLock重入锁。
HashEntry中 value、next链表都是volatile修饰的,保证了获取时的可见性。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
transient volatile HashEntry<K,V>[] table;
transient int count;
transient int modCount;
transient int threshold;
final float loadFactor;
}
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
}
默认的并发级别为 16,也就是说默认创建 16 个 Segment。
final Segment<K,V>[] segments;
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
ReentrantLock(重入锁):参考博客
ReentrantLock 提供了一种互斥锁(或者说独占锁)机制,在同一个时间点内只允许有一个线程持有该锁,其他线程将进行等待与重新获取操作。同时是一个可重用锁,可以被单个线程重复获取。
ReentrantLock分为FairSync公平锁和NonfairSync非公平锁,区别体现在获取锁的机制上是否公平。
ReentrantLock通过一个FIFO的队列来管理所有等待线程-即没有获取到锁的线程。
公平锁和非公平锁:
通过CLH这个非阻塞的FIFO队列管理所有等待的线程。
----往里面插入或移除一个节点的时候,在并发条件下不会产生阻塞,而通过自旋锁和CAS保证节点插入与移除的原子性
--公平锁:只有在当前线程是CLH等待队列的表头时,才获取锁。
--非公平锁:当前锁处于空闲状态,则直接获取锁,而不管CLH等待队列中的顺序。
volatile关键字: 参考博客
多线程编程中,不希望对拷贝的副本进行操作,希望直接进行原始变量的操作(节约复制变量副本与同步的时间),
就可以在变量声明时使用volatile关键字。
使用volatile定义的变量在进行操作时直接进行原始变量内容的处理。
被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。
volatile与synchronized的区别:
volatile无法描述同步的处理,只是一种直接内存的处理,避免了副本的操作,synchronized是实现同步操作的关键字。
volatile主要在属性上使用,synchronized则是在代码块与方法上使用。
2、size操作
每个Segment分段锁维护了一个count变量统计该Segment中的键值对的个数。
transient int count;
执行size操作时,需要遍历所有的Segment,然后把count累计起来。
ConcurrentHashMap执行size操作时先尝试不加锁,若连续两次不加锁操作得到的结果一致,可以认为这个结果正确。
RETRIES_BEFORE_LOCK用于定义尝试次数,值为2,retries初始值为-1,因此尝试次数为3.
因此如果尝试的次数超过3次,就需要对每个Segment加锁。
static final int RETRIES_BEFORE_LOCK = 2;//尝试次数值定义为2
public int size() {
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; //如果大小溢出32位则为true
long sum; //修改次数的和
long last = 0L; //之前的和
int retries = -1; //第一次迭代不需要retry,初始值为-1
try {
for (;;) {
// 超过尝试次数,则对每个 Segment 加锁
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
// 连续两次得到的结果一致,则认为这个结果是正确的
if (sum == last)
break;
last = sum;
}
} finally {
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
ConcurrentHashMap采用了分段锁技术,其中Segment继承于ReentrantLock。
put和get操作都需要做同步处理,支持并发级别CurrencyLevel(Segment数量,默认为16)的线程并发。
每当一个线程占用锁访问一个Segment时,不会影响别的Segment。
3、put方法–JDK1.7
1 public V put(K key, V value) {
2 Segment<K,V> s;
3 if (value == null)
4 throw new NullPointerException();
5 int hash = hash(key);
6 int j = (hash >>> segmentShift) & segmentMask;
7 if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
8 (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
9 s = ensureSegment(j);
10 return s.put(key, hash, value, false);
11 }
首先通过key定位到Segment,在对应的Segment中进行put。
----HashEntry中volatile关键字不能保证并发的原子性
----scanAndLockForPut()–自旋获取锁–重试的次数达到 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
1 final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。
2 HashEntry<K,V> node = tryLock() ? null :
3 scanAndLockForPut(key, hash, value);
4 V oldValue;
5 try {
6 HashEntry<K,V>[] tab = table;
7 int index = (tab.length - 1) & hash;
8 HashEntry<K,V> first = entryAt(tab, index);
//遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
9 for (HashEntry<K,V> e = first;;) {
10 if (e != null) {
11 K k;
12 if ((k = e.key) == key ||
13 (e.hash == hash && key.equals(k))) {
14 oldValue = e.value;
15 if (!onlyIfAbsent) {
16 e.value = value;
17 ++modCount;
18 }
19 break;
20 }
21 e = e.next;
22 }
23 else {//为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容
24 if (node != null)
25 node.setNext(first);
26 else
27 node = new HashEntry<K,V>(hash, key, value, first);
28 int c = count + 1;
29 if (c > threshold && tab.length < MAXIMUM_CAPACITY)
30 rehash(node);
31 else
32 setEntryAt(tab, index, node);
33 ++modCount;
34 count = c;
35 oldValue = null;
36 break;
37 }
38 }
39 } finally {
40 unlock();//解锁
41 }
42 return oldValue;
43 }
get:
通过key的hash定位到Segment,再hash定位到具体的元素。
HashEntry中的value使用volatile修饰,内存可见,每次获取的值都是最新值。
不使用锁,高效。
4、JDK1.8的改动

1、JDK1.7使用分段锁机制实现并发更新操作,核心类为Segment,继承自重入锁ReentrantLock,并发度与Segment数量相等。
2、JDK1.8中使用了CAS操作支持更高的并发度,在CAS操作失败时使用synchronized内置锁。
3、JDK1.8也会在链表过长时转换为红黑树存储。保证查询效率(O(logn))
Segment分段锁 ---->> CAS + synchronized锁
HashEntry ---->> Node —同样使用volatile修饰value和next
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
}
CAS:
JUC中有CAS和AQS两个核心操作,CAS是JUC.atomic包的基础,AQS是同步锁的实现基础。
CAS(Compare And Swap)是一条CPU并发原语。
用于判断内存某个位置的值是否为预期值,如果不是则更改为新的值,属于原子性操作。
主要使用方式:在代码中使用CAS自旋volatile变量的形式实现非阻塞并发。
synchronized是悲观锁,这种线程一旦得到锁,其他需要锁的线程就挂起的情况就是悲观锁。
CAS操作的就是乐观锁,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。
CAS是乐观锁,是一种冲突重试机制,在并发不是很激烈的情况下,操作性好于synchronized悲观锁机制。
CAS缺点:
1、CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。
2、不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。
5、put方法–JDK1.8
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();//如果键值对有空空指针异常
int hash = spread(key.hashCode());//根据key计算hashcode
int binCount = 0;
for (Node<K,V>[] tab = table;;) {//遍历table
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)//判断是否需要进行初始化
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//f为当前key定位到的Node,如果为空表示当前位置可以写入数据
//利用CAS尝试写入数据,失败则自旋 保证成功
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)//如果当前位置的 hashcode == MOVED == -1,则需要进行扩容
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {//如果都不满足,则利用 synchronized 锁写入数据。
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
for (Node<K,V> e = f;; ++binCount) {
K ek;
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;
}
}
}
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) {
if (binCount >= TREEIFY_THRESHOLD)//如果数量大于TREEIFY_THRESHOLD则转换为红黑树。
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
CAS:
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
//unsafe类中
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
1、谈谈你理解的 HashMap,讲讲其中的 get put 过程。
2、1.8 做了什么优化?
3、是线程安全的嘛?
4、不安全会导致哪些问题?
5、如何解决?有没有线程安全的并发容器?
6、ConcurrentHashMap 是如何实现的? 1.7、1.8 实现有何不同?为什么这么做?
7、LinkedHashMap
1、存储结构
继承自HashMap,具有同HashMap一样的快速查找特性。
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>{
//内部维护了一个双向链表,用来维护插入顺序或者LRU顺序。
transient LinkedHashMap.Entry<K,V> head;
transient LinkedHashMap.Entry<K,V> tail;
//accessOrder决定了顺序,默认为false,此时维护插入顺序。
final boolean accessOrder;
//用于维护顺序的函数
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
}
2、afterNodeAccess()方法
当一个节点被访问时,如果accessOrder为true,则会将这个节点移到链表尾部。
也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,
保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
头 ----> 尾 == 最近最久未使用 ----> 最近访问
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
3、afterNodeInsertion()方法
在 put 等操作之后执行,当 removeEldestEntry() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。
evict 只有在构建 Map 的时候才为 false,在这里为 true。
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
4、LRU缓存
使用 LinkedHashMap 实现的一个 LRU 缓存:
1、设定最大缓存空间 MAX_ENTRIES 为 3;
2、使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LRU 顺序;
3、覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private static final int MAX_ENTRIES = 3;
protected boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES;
}
LRUCache() {
super(MAX_ENTRIES, 0.75f, true);
}
}
8、WeakHashMap
1、存储结构
WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。
WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
2、ConcurrentCache
Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
ConcurrentCache 采取的是分代缓存:
1、经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
2、不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。
3、当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。
4、当调用 put() 方法时,如果 eden 的大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
public final class ConcurrentCache<K, V> {
private final int size;
private final Map<K, V> eden;
private final Map<K, V> longterm;
public ConcurrentCache(int size) {
this.size = size;
this.eden = new ConcurrentHashMap<>(size);
this.longterm = new WeakHashMap<>(size);
}
public V get(K k) {
V v = this.eden.get(k);
if (v == null) {
v = this.longterm.get(k);
if (v != null)
this.eden.put(k, v);
}
return v;
}
public void put(K k, V v) {
if (this.eden.size() >= size) {
this.longterm.putAll(this.eden);
this.eden.clear();
}
this.eden.put(k, v);
}
}
面试题:HashMap 和 Hashtable 的区别?
1、线程安全: HashMap 是非线程安全的, HashTable 是线程安全的。因为 HashTable 内部的⽅法基本都经过 synchronized 修饰。
2、效率 HashMap的效率高于HashTable。
3、Null的键值 HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有⼀个, null 作为值可以有多个; HashTable 不允许有 null 键和 null 值,否则会抛出NullPointerException 。
4、初始容量大小和每次扩充容量大小的不同 :
① 创建时如果不指定容量初始值, Hashtable默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。 HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。
② 创建时如果给定了容量初始值,那么Hashtable 会直接使⽤你给定的大小,⽽ HashMap 会将其扩充为 最接近2 的幂次方大小(HashMap 中的 tableSizeFor() ⽅法保证)。也就是说 HashMap 总是使⽤ 2 的幂作为哈希表的大小。
—2的幂次方是因为出现哈希冲突时,查找数组下标:hash%length==hash&(length-1),位运算效率高。
5、底层数据结构: JDK1.8后的HashMap解决哈希冲突引入了红黑树。
面试题: ConcurrentHashMap 和 Hashtable 的区别?
1、底层数据结构:
JDK1.7 的 ConcurrentHashMap 底层采⽤ 分段的数组+链表 实现, JDK1.8采⽤的数据结构跟 HashMap1.8 的结构⼀样,数组+链表/红黑树。
Hashtable 和JDK1.8 之前的 HashMap 的底层数据结构类似都是采⽤ 数组+链表 的形式,数组是
HashMap 的主体,链表则是主要为了解决哈希冲突⽽存在的;
2、实现线程安全的⽅式:
① 在 JDK1.7 的时候, ConcurrentHashMap (分段锁)对整个桶数组进⾏了分割分段( Segment ),每⼀把锁只锁容器其中⼀部分数据,多线程访问容器⾥不同数据段的数据,就不会存在锁竞争,提⾼并发访问率。
到了 JDK1.8 的时候已经摒弃了 Segment 的概念,⽽是直接⽤ Node 数组+链表+红⿊树的数据结构来实现,并发控制使⽤ synchronized 和 CAS 来操作。(JDK1.6 以后 对 synchronized 锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap ,虽然在 JDK1.8 中还能看到Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable (同⼀把锁) :使⽤ synchronized 来保证线程安全,效率⾮常低下。当⼀个线程访问同步⽅法时,其他线程也访问同步⽅法,可能会进⼊阻塞或轮询状态,如使⽤ put 添加元素,另⼀个线程不能使⽤ put 添加元素,也不能使⽤ get,竞争会越来越激烈效率越低。
385

被折叠的 条评论
为什么被折叠?



