声明:题目大部分来源于Java后端公众号,有些个人整理,但答案皆为个人整理,仅供参考。
目录
1.Set:集合中的对象不按特定方式排序(针对内存地址来说,即非线性),并且没有重复对象。它的有些实现类能对集合中的对象按特定方式排序。
2.List:集合中的对象线性方式储存,可以有重复对象,允许按照对象在集合中的索引位置检索对象。有序可重复。
1.Map:通过键值对进行取值,key-value一一对应的,其中key不可以重复,而value可以重复
ConcurrentHashMap 的工作原理及代码实现,如何统计所有的元素个数
HashMap 和 ConcurrentHashMap 的区别
什么是CopyOnWriteArrayList,它与ArrayList有何不同?
Java中的集合
Java中的集合主要分为value,key-value(Collection,Map)两种,存储值分为List和Set,存储为key-value得失Map。
Collection接口中主要有这些方法:
boolean add(Object o) :向集合中加入一个对象的引用
void clear():删除集合中所有的对象,即不再持有这些对象的引用
boolean isEmpty() :判断集合是否为空
boolean contains(Object o) : 判断集合中是否持有特定对象的引用
Iterartor iterator() :返回一个Iterator对象,可以用来遍历集合中的元素
boolean remove(Object o) :从集合中删除一个对象的引用
int size() :返回集合中元素的数目
Object[] toArray() : 返回一个数组,该数组中包括集合中的所有元素
boolean equals(Object o):判断值是否相等
int hashCode(): 返回当前集合的hash值,可以作为判断地址是否想相等
Collection接口继承 Iterable<T> 接口,这个接口可以返回一个迭代器,主要有一下三个方法:
List和Set都是继承Collection接口。
List 和 Set 区别
1.Set:集合中的对象不按特定方式排序(针对内存地址来说,即非线性),并且没有重复对象。它的有些实现类能对集合中的对象按特定方式排序。
- 不允许重复对象,只允许一个 null 元素,根据equals和hashcode判断,一个对象要存储在set中,必须重写equals和hashcode方法;
- 无序容器,你无法保证每个元素的存储顺序,TreeSet通过 Comparator 或者 Comparable 维护了一个排序顺序。
- Set 接口最流行的几个实现类是 HashSet、LinkedHashSet 以及 TreeSet。最流行的是基于 HashMap 实现的 HashSet;TreeSet 还实现了 SortedSet 接口,因此 TreeSet 是一个根据其 compare() 和 compareTo() 的定义进行排序的有序容器。
2.List:集合中的对象线性方式储存,可以有重复对象,允许按照对象在集合中的索引位置检索对象。有序可重复。
-
可以允许重复的对象,可以插入多个null元素。
-
是一个有序容器,保持了每个元素的插入顺序,输出的顺序就是插入的顺序。
-
常用的实现类有 ArrayList、LinkedList 和 Vector。ArrayList 最为流行,它提供了使用索引的随意访问,而 LinkedList 则对于经常需要从 List 中添加或删除元素的场合更为合适。
Set和hashCode以及equals方法的联系
因为set接口中是不允许存在重复的对象或者值的,所以需要对存入set中的对象或者值进行判断,而hashCode和equals就是用来对这些对象和值进行判断的。
List 和 Map 区别
1.Map:通过键值对进行取值,key-value一一对应的,其中key不可以重复,而value可以重复
区别:
- Map用 put(k,v) / get(k),还可以使用containsKey()/containsValue()来检查其中是否含有某个key/value。
- List通过get()方法来一次取出一个元素。使用数字来选择一堆对象中的一个,get(0)...。(add/get)
- Collection没有get()方法来取得某个元素。只能通过iterator()遍历元素。
Arraylist 与 LinkedList 区别
1.Arraylist(线程不安全):
- 底层是数组(数组在内存中是一块连续的内存,如果插入或删除元素需要移动内存),可以插入空数据
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
实现了 RandomAccess 接口,所以支持随机访问
private static final int DEFAULT_CAPACITY = 10;
数组的默认大小为 10。
-
插入数据的时候,会先进行扩容校验,添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为
oldCapacity + (oldCapacity >> 1)
,也就是旧容量的 1.5 倍。public boolean add(E e) { ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; }
- 首先进行扩容校验。
- 将插入的值放到尾部,并将 size + 1 。
- 如果是调用
add(index,e)
在指定位置添加的话:public void add(int index, E element) { rangeCheckForAdd(index); ensureCapacityInternal(size + 1); // Increments modCount!! //复制,向后移动 System.arraycopy(elementData, index, elementData, index + 1, size - index); elementData[index] = element; size++; }
- 也是首先扩容校验。
- 接着对数据进行复制,目的是把 index 位置空出来放本次插入的数据,并将后面的数据向后移动一个位置。
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); }
扩容最终调用的代码,也是一个数组复制的过程。由此可见
ArrayList
的主要消耗是数组扩容以及在指定位置添加数据,在日常使用时最好是指定大小,尽量减少扩容。更要减少在指定位置插入数据的操作。
-
删除元素
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() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。
-
由于 ArrayList 是基于动态数组实现的,所以并不是所有的空间都被使用。因此使用了
transient
修饰,可以防止被自动序列化。transient Object[] elementData;
保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{ // Write out element count, and any hidden stuff int expectedModCount = modCount; s.defaultWriteObject(); // Write out size as capacity for behavioural compatibility with clone() s.writeInt(size); // Write out all elements in the proper order. //只序列化了被使用的数据 for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } } private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; // Read in size, and any hidden stuff s.defaultReadObject(); // Read in capacity s.readInt(); // ignored if (size > 0) { // be like clone(), allocate array based upon size not capacity ensureCapacityInternal(size); Object[] a = elementData; // Read in all elements in the proper order. for (int i=0; i<size; i++) { a[i] = s.readObject(); } } }
当对象中自定义了 writeObject 和 readObject 方法时,JVM 会调用这两个自定义方法来实现序列化与反序列化。序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
2.LinkedList(线程不安全):
- 底层是基于双向链表实现的,(JDK1.7/8 之后取消了循环,修改为双向链表),不要求内存是连续的,在当前元素存放下一个或上一个元素的地址。
- 每次插入都是移动指针,改变引用指向即可,效率较高;
- 查询的时候使用二分法,利用了双向链表的特性,如果
index
离链表头比较近,就从节点头部遍历。否则就从节点尾部开始遍历。使用空间(双向链表)来换取时间。node()会以O(n/2)的性能去获取一个结点;如果索引值大于链表大小的一半,那么将从尾结点开始遍历。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 越接近 size 的中间值时。
区别:
- LinkedList 插入,删除都是移动指针效率很高;查找需要进行遍历查询,效率较低。
-
LinkedList比ArrayList更占内存,因为LinkedList为每一个节点存储了两个引用,一个指向前一个元素,一个指向下一个元素。
-
ArrayList是可改变大小的数组,而LinkedList是双向链接串列
-
在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的
ArrayList 与 Vector 区别
1.Vector(线程安全):
- 底层也是基于数组实现的,但是add方法的时候使用了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); }
Vector
是一个同步容器并不是一个并发容器。
区别:
- ArrayList和Vector都采用线性连续存储空间,当存储空间不足的时候,Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
- ArrayList线程不安全,Vector线程安全
- Vector可以设置capacityIncrement,而ArrayList不可以,从字面理解就是capacity容量,Increment增加,容量增长的参数。
HashMap 的工作原理及代码实现,什么时候用到红黑树
1.HashMap(线程不安全,基于jdk1.7):
- hashmap是无序的,因为每次根据 key 的 hashcode 映射到 Entry 数组上,所以遍历出来的顺序并不是写入的顺序
- HashMap 底层是基于数组和链表实现的,如图所示,其中两个重要的参数:容量和负载因子;容量的默认大小是 16,负载因子是 0.75,当
HashMap
的size > 16*0.75
时就会发生扩容(容量和负载因子都可以自由调整)。 -
内部包含了一个 Entry 类型的数组 table。
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 存储着键值对。它包含了四个字段,从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶,一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值相同的 Entry。
-
拉链法的工作原理
HashMap<String, String> map = new HashMap<>(); map.put("K1", "V1"); map.put("K2", "V2"); map.put("K3", "V3");
下面的桶对应数组的一个元素,即数组中的每个位置被当成一个桶,一个桶放一个链表。
- 新建一个 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> 后面,而是插入在链表头部。
-
查找需要分成两步进行:
- 计算键值对所在的桶;
- 在链表上顺序查找,时间复杂度显然和链表的长度成正比。
- put方法:首先会将传入的 Key 做hash运算计算出 hashcode,然后根据数组长度取模计算出在数组中的 index 下标。
由于在计算中位运算比取模运算效率高的多,所以 HashMap 规定数组的长度为
2^n
。这样用2^n - 1
做位运算与取模效果一致,并且效率还要高出许多。由于数组的长度有限,所以难免会出现不同的 Key 通过运算得到的 index 相同,这种情况可以利用链表来解决,HashMap 会在
table[index]
处形成链表,采用头插法将数据插入到链表中。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 允许插入键为 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++; } Entry(int h, K k, V v, Entry<K,V> n) { value = v; next = n; key = k; hash = h; }
resize是扩容,默认扩为原来的2倍大小。
注意:
-
在并发环境下使用
HashMap
容易出现死循环。 -
并发场景发生扩容,调用
resize()
方法里的rehash()
时,容易出现环形链表。这样当获取一个不存在的key
时,计算出的index
正好是环形链表的下标时就会出现死循环。 -
在
JDK1.8
中对HashMap
进行了优化: 当hash
碰撞之后写入链表的长度超过了阈值(默认为8),链表将会转换为红黑树。假设hash
冲突非常严重,一个数组后面接了很长的链表,此时重新的时间复杂度就是O(n)
。如果是红黑树,时间复杂度就是O(logn)
。
2.Hashtable(线程安全):
- 也是实现了Map接口,底层是链表和数组;
- 继承了Dictionary<K,V>
-
Hashtable的synchronized是对整张hash表进行锁定即让线程独享整张hash表,在安全同时造成了浪费。
HashMap 和 Hashtable 的区别:
- HashMap线程不安全,Hashtable因为很多地方加了synchronized,所以它是线程安全的;
- HashTable使用Enumeration,HashMap 使用Iterator。
- HashMap不能保证元素的顺序,HashMap能够将键设为null,也可以将值设为null,但是只有一个键为null,值可以多个为null,当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。Hashtable不能将键和值设为null,否则运行时会报空指针异常错误;
- hash值的使用方式不同,Hashtable直接使用对象的hashCode,对table数组的长度直接进行取模;而HashMap计算hash对key的hashcode进行了二次hash,以获得更好的散列值,然后对table数组长度取模;
-
Hashtable int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; HashMap static int hash(int h) { h ^= (h >>> 20) ^ (h >>> 12); return h ^ (h >>> 7) ^ (h >>> 4); } static int indexFor(int h, int length) { return h & (length-1); }
- HashMap没有contains方法,而Hashtabl有contains方法。
//以下是Hashtable的方法 public synchronized boolean contains(Object value) public synchronized boolean containsKey(Object key) public boolean containsValue(Object value) //以下是HashMap中的方法,注意,没有contains方法 public boolean containsKey(Object key) public boolean containsValue(Object value)
- Hashtable中hash默认数组大小是11,增加的方式是old*2+1;HashMap中hash数组大小默认是16,而且一定是2的倍数,HashMap会将其扩充为2的幂次方大小;
- Hashtable、HashMap都使用了 Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。
- 两者存储规则不一样:
-
HashMap的存储规则:优先使用数组存储, 如果出现Hash冲突, 将在数组的该位置拉伸出链表进行存储(在链表的尾部进行添加), 如果链表的长度大于设定值后, 将链表转为红黑树.
-
HashTable的存储规则:优先使用数组存储, 存储元素时, 先取出下标上的元素(可能为null), 然后添加到数组元素Entry对象的next属性中(在链表的头部进行添加).出现Hash冲突时, 新元素next属性会指向冲突的元素. 如果没有Hash冲突, 则新元素的next属性就是null。
Entry<K,V> e = (Entry<K,V>) tab[index]; tab[index] = new Entry<>(hash, key, value, e);
-
参照:https://blog.csdn.net/wangxing233/article/details/79452946
HashSet 和 HashMap 区别:
1.HashSet(线程不安全):
- 不允许存储重复元素的集合
- 基于哈希表实现,支持快速查找,但不支持有序性操作。
- 使用 Iterator 遍历 HashSet 得到的结果是不确定的。
- 成员变量:
private transient HashMap<E,Object> map; // Dummy value to associate with an Object in the backing Map private static final Object PRESENT = new Object();
两个变量:
map
:用于存放最终数据的。PRESENT
:是所有写入 map 的value
值。
-
构造函数:利用了
HashMap
初始化了map
public HashSet() { map = new HashMap<>(); } public HashSet(int initialCapacity, float loadFactor) { map = new HashMap<>(initialCapacity, loadFactor); }
-
add方法:
public boolean add(E e) { return map.put(e, PRESENT)==null; }
Hashtable将存放的对象当做了
HashMap
的健,value
都是相同的PRESENT
。由于HashMap
的key
是不能重复的,所以每当有重复的值写入到HashSet
时,value
会被覆盖,但key
不会受到影响,这样就保证了HashSet
中只能存放不重复的元素。
HashSet
的原理比较简单,几乎全部借助于 HashMap
来实现的。所以 HashMap
会出现的问题 HashSet
依然不能避免。
区别:
-
HashMap实现了Map接口,而Hashtable实现Set接口;
- HashMap存储键值对,Hashtable仅存储对象;
- HashMap调用put()向map中添加元素;Hashtable调用add()方法向Set中添加元素;
- HashMap比较快,因为是使用唯一的键来获取对象
ConcurrentHashMap 的工作原理及代码实现,如何统计所有的元素个数
1.ConcurrentHashMap(线程安全):
- 存储结构:
ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁(Segment),每个分段锁维护着几个桶(HashEntry),多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。static final class HashEntry<K,V> { final int hash; final K key; volatile V value; volatile HashEntry<K,V> next; }
- 数据结构(JDK1.7):
如图所示,是由 Segment
数组、HashEntry
数组组成,和 HashMap
一样,仍然是数组加链表组成。ConcurrentHashMap
采用了分段锁技术,其中 Segment
继承于 ReentrantLock
。不会像 HashTable
那样不管是 put
还是 get
操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel
(Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment
时,不会影响到其他的 Segment
。
- 默认的并发级别为 16,也就是说默认创建 16 个 Segment。
- get 方法:
ConcurrentHashMap
的get
方法是非常高效的,因为整个过程都不需要加锁。只需要将
Key
通过Hash
之后定位到具体的Segment
,再通过一次Hash
定位到具体的元素上。由于HashEntry
中的value
属性是用volatile
关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。 -
put 方法:
static final class HashEntry<K,V> { final int hash; final K key; volatile V value; volatile HashEntry<K,V> next; HashEntry(int hash, K key, V value, HashEntry<K,V> next) { this.hash = hash; this.key = key; this.value = value; this.next = next; } }
虽然 HashEntry 中的 value 是用 volatile
关键词修饰的,但是并不能保证并发的原子性,所以 put 操作时仍然需要加锁处理。首先也是通过 Key 的 Hash 定位到具体的 Segment,在 put 之前会进行一次扩容校验。这里比 HashMap 要好的一点是:HashMap 是插入元素之后再看是否需要扩容,有可能扩容之后后续就没有插入就浪费了本次扩容(扩容非常消耗性能)。而 ConcurrentHashMap 不一样,它是在将数据插入之前检查是否需要扩容,之后再做插入操作。
- size 方法:每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。
/** * The number of elements. Accessed only either within locks * or among other volatile reads that maintain visibility. */ transient int count;
在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。
/** * Number of unsynchronized retries in size and containsValue * methods before resorting to locking. This is used to avoid * unbounded retries if tables undergo continuous modification * which would make it impossible to obtain an accurate result. */ static final int RETRIES_BEFORE_LOCK = 2; public int size() { // Try a few times to get accurate count. On failure due to // continuous async changes in table, resort to locking. final Segment<K,V>[] segments = this.segments; int size; boolean overflow; // true if size overflows 32 bits long sum; // sum of modCounts long last = 0L; // previous sum int retries = -1; // first iteration isn't retry 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; }
每个
Segment
都有一个modCount
变量,每当进行一次put remove
等操作,modCount
将会 +1。只要modCount
发生了变化就认为容器的大小也在发生变化。 -
JDK1.8的实现:
抛弃了原有的 Segment 分段锁,而采用了 CAS + synchronized
来保证并发安全性。
也将 1.7 中存放数据的 HashEntry 改为 Node,但作用都是相同的。其中的 val next
都用了 volatile 修饰,保证了可见性。
put方法:
- 根据 key 计算出 hashcode 。
- 判断是否需要进行初始化。
f
即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。- 如果当前位置的
hashcode == MOVED == -1
,则需要进行扩容。 - 如果都不满足,则利用 synchronized 锁写入数据。
- 如果数量大于
TREEIFY_THRESHOLD
则要转换为红黑树。
get方法:
- 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 都不满足那就按照链表的方式遍历获取值。
总结
1.8 在 1.7 的数据结构上做了大的改动,采用红黑树之后可以保证查询效率(O(logn)
),甚至取消了 ReentrantLock 改为了 synchronized,这样可以看出在新版的 JDK 中对 synchronized 优化是很到位的。
HashMap 和 ConcurrentHashMap 的区别
- HashMap线程不安全,而ConcurrentHashMap线程安全;
多线程情况下HashMap死循环的问题
- 容量大于
总量*负载因子
发生扩容时会出现环形链表从而导致死循环。 - 并发场景发生扩容,调用
resize()
方法里的rehash()
时,容易出现环形链表。这样当获取一个不存在的key
时,计算出的index
正好是环形链表的下标时就会出现死循环。
https://www.cnblogs.com/dongguacai/p/5599100.html
https://blog.csdn.net/linsongbin1/article/details/54708694
介绍一下LinkedHashMap
-
这是一个有序的,底层是继承于 HashMap 实现的,由一个双向链表所构成,具有和 HashMap 一样的快速查找特性。
-
LinkedHashMap
的排序方式有两种:- 根据写入顺序排序。
- 根据访问顺序排序,每次
get
都会将访问的值移动到链表末尾,这样重复操作就能得到一个按照访问顺序排序的链表。
- 数据结构,通过以下代码调试可以看到
map
的组成:@Test public void test(){ Map<String, Integer> map = new LinkedHashMap<String, Integer>(); map.put("1",1) ; map.put("2",2) ; map.put("3",3) ; map.put("4",4) ; map.put("5",5) ; System.out.println(map.toString()); }
/**
* The head of the doubly linked list.
*/
private transient Entry<K,V> header;
/**
* The iteration ordering method for this linked hash map: <tt>true</tt>
* for access-order, <tt>false</tt> for insertion-order.
*
* @serial
*/
private final boolean accessOrder;
private static class Entry<K,V> extends HashMap.Entry<K,V> {
// These fields comprise the doubly linked list used for iteration.
Entry<K,V> before, after;
Entry(int hash, K key, V value, HashMap.Entry<K,V> next) {
super(hash, key, value, next);
}
}
其中 Entry
继承于 HashMap
的 Entry
,并新增了上下节点的指针,也就形成了双向链表。还有一个 header
的成员变量,是这个双向链表的头结点。
第一个类似于 HashMap
的结构,利用 Entry
中的 next
指针进行关联。下边则是 LinkedHashMap
如何达到有序的关键。就是利用了头节点和其余的各个节点之间通过 Entry
中的 after
和 before
指针进行关联。其中还有一个 accessOrder
成员变量,默认是 false
,默认按照插入顺序排序,为 true
时按照访问顺序排序,也可以调用:
这个构造方法可以显示的传入 accessOrder
。
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
-
构造方法就是调用
HashMap
的构造方法:public LinkedHashMap() { super(); accessOrder = false; } // HashMap实现: 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; threshold = initialCapacity; //HashMap 只是定义了改方法,具体实现交给了 LinkedHashMap init(); }
可以看到里面有一个空的
init()
,具体是由LinkedHashMap
来实现的:@Override void init() { header = new Entry<>(-1, null, null, null); header.before = header.after = header; }
其实也就是对
header
进行了初始化,从这个方法可以看出,实现了双向。 -
put() 方法:主体的实现都是借助于
HashMap
来完成的,只是对其中的recordAccess(), addEntry(), createEntry()
进行了重写。如下是HashMap的put方法:
public V put(K key, V value) { if (table == EMPTY_TABLE) { inflateTable(threshold); } if (key == null) return putForNullKey(value); int hash = hash(key); int i = indexFor(hash, table.length); 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; //空实现,交给 LinkedHashMap 自己实现 e.recordAccess(this); return oldValue; } } modCount++; // LinkedHashMap 对其重写 addEntry(hash, key, value, i); return null; } // LinkedHashMap 对其重写 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); } // LinkedHashMap 对其重写 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++; }
//就是判断是否是根据访问顺序排序,如果是则需要将当前这个 Entry 移动到链表的末尾 void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { lm.modCount++; remove(); addBefore(lm.header); } } //调用了 HashMap 的实现,并判断是否需要删除最少使用的 Entry(默认不删除) void addEntry(int hash, K key, V value, int bucketIndex) { super.addEntry(hash, key, value, bucketIndex); // Remove eldest entry if instructed Entry<K,V> eldest = header.after; if (removeEldestEntry(eldest)) { removeEntryForKey(eldest.key); } } void createEntry(int hash, K key, V value, int bucketIndex) { HashMap.Entry<K,V> old = table[bucketIndex]; Entry<K,V> e = new Entry<>(hash, key, value, old); //就多了这一步,将新增的 Entry 加入到 header 双向链表中 table[bucketIndex] = e; e.addBefore(header); size++; } //写入到双向链表中 private void addBefore(Entry<K,V> existingEntry) { after = existingEntry; before = existingEntry.before; before.after = this; after.before = this; }
以上是LinkedHashMap
的实现。 -
get 方法,LinkedHashMap 的
get()
方法也重写了:public V get(Object key) { Entry<K,V> e = (Entry<K,V>)getEntry(key); if (e == null) return null; //多了一个判断是否是按照访问顺序排序,是则将当前的 Entry 移动到链表头部。 e.recordAccess(this); return e.value; } void recordAccess(HashMap<K,V> m) { LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m; if (lm.accessOrder) { lm.modCount++; //删除 remove(); //添加到头部 addBefore(lm.header); } }
clear()方法:
//只需要把指针都指向自己即可,原本那些 Entry 没有引用之后就会被 JVM 自动回收。 public void clear() { super.clear(); header.before = header.after = header; }
总的来说
LinkedHashMap
其实就是对HashMap
进行了拓展,使用了双向链表来保证了顺序性。因为是继承与HashMap
的,所以一些HashMap
存在的问题LinkedHashMap
也会存在,比如不支持并发等。
HashMap出现Hash DOS攻击的问题
手写简单的HashMap
看过那些Java集合类的源码
什么是快速失败的故障安全迭代器?
快速失败的Java迭代器可能会引发ConcurrentModifcationException在底层集合迭代过程中被修改。故障安全作为发生在实例中的一个副本迭代是不会抛出任何异常的。快速失败的故障安全范例定义了当遭遇故障时系统是如何反应的。例如,用于失败的快速迭代器ArrayList和用于故障安全的迭代器ConcurrentHashMap。
Iterator和ListIterator的区别
●ListIterator有add()方法,可以向List中添加对象,而Iterator不能。
●ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。
●ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。
●都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改
什么是CopyOnWriteArrayList,它与ArrayList有何不同?
CopyOnWriteArrayList是ArrayList的一个线程安全的变体,其中所有可变操作(add、set等等)都是通过对底层数组进行一次新的复制来实现的。相比较于ArrayList它的写操作要慢一些,因为它需要实例的快照。
CopyOnWriteArrayList中写操作需要大面积复制数组,所以性能肯定很差,但是读操作因为操作的对象和写操作不是同一个对象,读之间也不需要加锁,读和写之间的同步处理只是在写完后通过一个简单的"="将引用指向新的数组对象上来,这个几乎不需要时间,这样读操作就很快很安全,适合在多线程里使用,绝对不会发生ConcurrentModificationException ,因此CopyOnWriteArrayList适合使用在读操作远远大于写操作的场景里,比如缓存。
迭代器和枚举之间的区别
如果面试官问这个问题,那么他的意图一定是让你区分Iterator不同于Enumeration的两个方面:
●Iterator允许移除从底层集合的元素。
●Iterator的方法名是标准化的。
https://blog.csdn.net/helongzhong/article/details/52869981
总结:
1. 如果涉及到堆栈,队列等操作,应该考虑用List,对于需要快速插入,删除元素,应该使用LinkedList,如果需要快速随机访问元素,应该使用ArrayList。
2. 如果程序在单线程环境中,或者访问仅仅在一个线程中进行,考虑非同步的类,其效率较高,如果多个线程可能同时操作一个类,应该使用同步的类。
3. 在除需要排序时使用TreeSet,TreeMap外,都应使用HashSet,HashMap,因为他们 的效率更高。
4. 要特别注意对哈希表的操作,作为key的对象要正确复写equals和hashCode方法。
5. 容器类仅能持有对象引用(指向对象的指针),而不是将对象信息copy一份至数列某位置。一旦将对象置入容器内,便损失了该对象的型别信息。
6. 尽量返回接口而非实际的类型,如返回List而非ArrayList,这样如果以后需要将ArrayList换成LinkedList时,客户端代码不用改变。这就是针对抽象编程。