Java容器--笔记

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下扩容出现死循环及1.8的改进

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);

面试题:ConcurrentHashMap几个问题

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,竞争会越来越激烈效率越低。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值