Set
无序,不包含重复元素,可以插入 null
HashSet:为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。
TreeSet: 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。
LinkedHashSet:具有HashSet的查询速度,且内部使用链表维护元素的顺序(插入的次序)。于是在使用迭代器遍历Set时,结果会按元素插入的次序显示。
List
有顺序以线性方式存储,可以存放重复对象
Queue
Map
key 可以为 null,键也可以为 null
键必须是唯一,
ArrayList(✔)
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
private static final int DEFAULT_CAPACITY = 10;
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8; // 超过了直接扩容为 Integer.MAX_VALUE
public boolean add(E e) { //将 size+1 作为最小容量,判断是否需要扩容.第一次插入时,需要扩容成 size=10 //超过 10 之后在扩容,扩容容量为现在的 1.5 倍 //扩容操作需要调用 Arrays.copyof(),把原来数组整个复制到新数组中,这个操作代价很高. //因此最好在创建 ArrayList 对象时,就指定大概的容量,减少扩容次数. ensureCapacityInternal(size + 1); // Increments modCount!! elementData[size++] = e; return true; } private void grow(int minCapacity) { // overflow-conscious code int oldCapacity = elementData.length; int newCapacity = oldCapacity + (oldCapacity >> 1); //右移相当于除二操作, 扩容1.5倍 if (newCapacity - minCapacity < 0) //扩容后的容量 < 最小容量 newCapacity = minCapacity; //扩容容量大小为 minCapacity if (newCapacity - MAX_ARRAY_SIZE > 0) newCapacity = hugeCapacity(minCapacity); // minCapacity is usually close to size, so this is a win: //Arrays.copyOf:new一个新数组,长度为newCapacity,然后将elementData数组复制过去,最后返回一个新的数组 elementData = Arrays.copyOf(elementData, newCapacity); } private static int hugeCapacity(int minCapacity) { if (minCapacity < 0) // overflow throw new OutOfMemoryError(); return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE; }
remove
public E remove(int index)
- 注意: 移除之后 elementData[–size] = null; //为了使 GC 起作用
public boolean remove(Object o)
- o 为 null ,遍历 list 移除第一个 为 null 的元素
- 不为 null,判断 移除第一个 相等 即 equals 的元素
- 注意: 移除之后 elementData[–size] = null; //为了使 GC 起作用
遍历方式
for
增强 for 循环 — 还是用迭代器实现的
迭代器
fail-fast
modCount 记录 list 结构发生改变(添加/删除)的次数
if (modCount != expectedModCount) throw new ConcurrentModificationException();
两个坑
顺序删除
@Test //错误示范 public void delete(){ List<Integer> list = new ArrayList<>(); for (int i = 1; i < 5; i++) { list.add(i); } System.out.println(list);//[1, 2, 3, 4] for (int i = 0; i < list.size(); i++) { list.remove(i); } System.out.println(list);//[2, 4] } //正确的方法 @Test public void delete2(){ List<Integer> list = new ArrayList<>(); for (int i = 1; i < 5; i++) { list.add(i); } System.out.println(list);//[1, 2, 3, 4] for (int i = (list.size()-1); i >= 0; i--) { list.remove(i); } System.out.println(list);//[] }
增强 for 循环 / 迭代器遍历,不能对数组进行增删操作,只能使用迭代器的 remove 方法
@Test public void iteratorTest(){ List<Integer> list = new ArrayList<>(); for (int i = 1; i < 5; i++) { list.add(i); } Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()){ list.add(1); // 抛异常 ConcurrentModificationException -> checkForComodification System.out.println(iterator.next()); } } @Test public void foreachTest(){ List<Integer> list = new ArrayList<>(); for (int i = 1; i < 5; i++) { list.add(i); } for (Integer a:list){ list.add(10);// ConcurrentModificationException System.out.println(a); } } //正确的方法 @Test public void iteratorTest2(){ List<Integer> list = new ArrayList<>(); for (int i = 1; i < 5; i++) { list.add(i); } Iterator<Integer> iterator = list.iterator(); while (iterator.hasNext()){ System.out.println(iterator.next()); iterator.remove();//执行remove之前需要执行next方法,给 lastRet 赋值才能 remove } System.out.println(list);//[] }
序列化/反序列化
private void readObject(java.io.ObjectInputStream s) private void writeObject(java.io.ObjectOutputStream s)
LinkedList(✔)
小结:
(1)LinkedList是一个以双链表实现的List;
(2)LinkedList还是一个双端队列,具有队列、双端队列、栈的特性;
(3)LinkedList在队列首尾添加、删除元素非常高效,时间复杂度为O(1);
(4)LinkedList在中间添加、删除元素比较低效,时间复杂度为O(n);
(5)LinkedList不支持随机访问,所以访问非队列首尾的元素比较低效;
(6)LinkedList在功能上等于ArrayList + ArrayDeque;
和 ArrayList 对比
LinkedList (双向链表结构)
- 优点:不需要扩容和预留空间,不需要连续的存储空间
- 优点: 添加和 删除 效率高
- 缺点: 随机访问效率低,需要遍历整个链表
- 缺点:改查效率低
ArrayList (顺序表结构—数组)
- 优点: 支持随机访问
- 缺点:添加/删除效率低,需要拷贝数组(也可能涉及扩容操作)
双向链表查找 index 位置的节点时,有一个加速动作:若index < 双向链表长度的1/2,则从前向后查找; 否则,从后向前查找。
常用方法
//可以用作 list, 双端队列,, 也可以作为栈使用 //Deque getFirst //返回此列表的第一个元素。 getLast //返回此列表的第一个元素。 removeFirst //移除并返回此列表的第一个元素。 removeLast addFirst //将指定元素插入此列表的开头。 addLast element peek //获取但不移除此列表的头(第一个元素)。 peekFirst //获取但不移除此列表的第一个元素;如果此列表为空,则返回 null。 peekLast poll //获取并移除此列表的头(第一个元素) pollFirst //获取并移除此列表的第一个元素;如果此列表为空,则返回 null。 pollLast remove //获取并移除此列表的头(第一个元素)。 offer //将指定元素添加到此列表的末尾(最后一个元素)。 offerFirst // 在此列表的开头插入指定的元素。 offerLast push pop //从此列表所表示的堆栈处弹出一个元素。 //AbstractSequentialList get set add remove
其余看脑图
和 ArrayList 对比
相同点:
和 ArrayList 实现了相同的接口, 继承了相同的父类,
底层都是数组实现
初始默认长度都是 10
public class Vector<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable
不同:
- Vector 是线程安全的,因为使用了 Synchronized
- 扩容容量: Vector 2倍, ArrayList: 1.5倍
参考文献1 参考文献2
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
final transient ReentrantLock lock = new ReentrantLock();
private transient volatile Object[] array;
public CopyOnWriteArrayList() // 构造一个空数组 setArray(new Object[0]);
public CopyOnWriteArrayList(Collection<? extends E> c) // 将 传入的 Collection 转为Object[] 赋值给 array
public CopyOnWriteArrayList(E[] toCopyIn) // 将传入的数组 toCopyIn 赋值给 array
}
add (总的来说:就是先将array拷贝一份,然后进行添加,最后在赋值回去)
public boolean add(E e) public void add(int index, E element) { final ReentrantLock lock = this.lock; lock.lock(); try { Object[] elements = getArray(); int len = elements.length; //检查越界情况 if (index > len || index < 0) throw new IndexOutOfBoundsException("Index: "+index+ ", Size: "+len); Object[] newElements; //移动的元素的个数 int numMoved = len - index; if (numMoved == 0) //插在末尾 newElements = Arrays.copyOf(elements, len + 1); else { newElements = new Object[len + 1]; System.arraycopy(elements, 0, newElements, 0, index); System.arraycopy(elements, index, newElements, index + 1, numMoved); } newElements[index] = element; setArray(newElements); } finally { lock.unlock(); } } public boolean addIfAbsent(E e) private boolean addIfAbsent(E e, Object[] snapshot) ---加锁的添加方法,详情看 jdk souorceCode注释
get 未加锁
public E get(int index) { return get(getArray(), index); }
remove : 和 add 一样,复制在删除在赋值. 加锁
迭代器
static final class COWIterator<E> implements ListIterator<E> { /* CopyOnWriteArrayList 在使用迭代器遍历的时候,操作的都是原数组, 没有像 ArrayList 那样进行修改次数判断,所以不会抛异常! */ private final Object[] snapshot; private int cursor; private COWIterator(Object[] elements, int initialCursor) { cursor = initialCursor; snapshot = elements; } //不支持 remove 操作,只能依靠 CopyOnWriteArrayList 的 remove()方法 public void remove() { throw new UnsupportedOperationException(); } }
小结
(1)CopyOnWriteArrayList使用 ReentrantLock 重入锁加锁,保证线程安全;
(2)CopyOnWriteArrayList的写操作都要先拷贝一份新数组,在新数组中做修改,修改完了再用新数组替换老数组,所以空间复杂度是O(n),性能比较低下;
(3)CopyOnWriteArrayList的读操作支持随机访问,时间复杂度为O(1);
(4)CopyOnWriteArrayList采用读写分离的思想,读操作不加锁,写操作加锁,且写操作占用较大内存空间,所以适用于读多写少的场合;
(5)CopyOnWriteArrayList只保证最终一致性,不保证实时一致性;
缺陷,对于边读边写的情况,不一定能实时的读到最新的数据
数组+双向链表+红黑树+单链表
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
//LinkedHashMap 的 Entry
static class Entry<K,V> extends HashMap.Node<K,V> {
Entry<K,V> before, after; // 用于维护 插入结点的顺序
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
//HashMap 的 Node
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next; //用于 链接 哈希桶中的 单链表
}
//双向链表的头结点
transient LinkedHashMap.Entry<K,V> head;
//双向链表的尾结点
transient LinkedHashMap.Entry<K,V> tail;
// true:按照访问顺序存储元素(LRU) false:插入顺序存储元素 --默认:false
final boolean accessOrder;
//
public LinkedHashMap(int initialCapacity,float loadFactor, boolean accessOrder)
保证 顺序的 函数
//HashMap 中,这三个方法都是没实现的,在 LinkedHashMap 中实现来维护结点顺序 // void afterNodeAccess(Node<K,V> p) { } void afterNodeInsertion(boolean evict) { } void afterNodeRemoval(Node<K,V> p) { } //LinkedHashMap /* 在节点访问之后被调用,主要在put()已经存在的元素或get()时被调用, 如果accessOrder为true,调用这个方法把访问到的节点移动到双向链表的末尾。 */ void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; // accessOrder = true则执行,否则结束 // accessOrder = true, e 不是 tail 尾结点 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; } } /* 在节点插入之后做些什么,在HashMap中的putVal()方法中被调用,可以看到HashMap中这个方法的实现为空。 evict:驱逐的意思 如果 evict 为 true,则移除最老的元素(head) 默认removeEldestEntry()方法返回false,也就是不删除元素。 */ void afterNodeInsertion(boolean evict) { // possibly remove eldest LinkedHashMap.Entry<K,V> first; //如果evict为true,且头节点不为空,且 确定移除最老的元素,即移除 head //head 为 双向链表的头结点 if (evict && (first = head) != null && removeEldestEntry(first)) { K key = first.key; //HashMap.removeNode()从HashMap中把这个节点移除之后,会调用 afterNodeRemoval() 方法; removeNode(hash(key), key, null, false, true); } } //传进来的参数 是 双向链表的头结点 (即最老的结点) protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; } /* 在节点被删除之后调用的方法 afterNodeInsertion -> HashMap.removeNode() -> afterNodeRemoval 从双向链表中 删除结点 e */ void afterNodeRemoval(Node<K,V> e) { // unlink LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; // 把节点p从双向链表中删除。 p.before = p.after = null; if (b == null) head = a; else b.after = a; if (a == null) tail = b; else a.before = b; } /* 因此使用 LinkedHashMap 实现 LRU, 1) 设置 accessOrder 为 true --> 把最近访问的结点移动到尾部 2) 重写 removeEldestEntry 方法 --> 返回 true 会删除该结点, false 不删除 */
get
public V get(Object key) { Node<K,V> e; if ((e = getNode(hash(key), key)) == null) return null; //如果查找到了元素,且accessOrder为true, //则调用afterNodeAccess()方法把访问的节点移到双向链表的末尾。 if (accessOrder) afterNodeAccess(e); return e.value; }
小结
(1)LinkedHashMap继承自HashMap,具有HashMap的所有特性;
(2)LinkedHashMap内部维护了一个双向链表存储所有的元素;
(3)如果accessOrder为false,则可以按插入元素的顺序遍历元素;
(4)如果accessOrder为true,则可以按访问元素的顺序遍历元素;
(5)LinkedHashMap的实现非常精妙,很多方法都是在HashMap中留的钩子(Hook),直接实现这些Hook就可以实现对应的功能了,并不需要再重写put()等方法;
(6)默认的LinkedHashMap并不会移除旧元素,如果需要移除旧元素,则需要重写removeEldestEntry()方法设定移除策略;
(7)LinkedHashMap可以用来实现LRU缓存淘汰策略;
WeakHashMap(✔)
强/软/弱/虚引用
强应用、软引用、弱引用、虚引用 分别是什么?
- 强引用: 内存不足,也不会对对象进行回收
- 软引用:内存不足,可以被回收GC
- 弱引用:只要有GC运行就会被回收
- 引用队列: ReferenceQueue —> 弱引用、软引用、虚引用 在被GC回收之后,会被放入引用队列
- 虚引用: 如果一个对象只具有虚引用,那么它就和没有任何引用一样,任何时候都可能被gc回收。
/* 强引用: 当内存不足,JVM开始垃圾回收,对于==强引用对象,就算出现了OOM也不会对该对象进行回收== 强引用是造成Java内存泄漏的主要原因之一。 */ Object o = new Object(); /* 软引用: 软引用是相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现 对于软引用的对象来说:当系统内存充足时他不会被回收,当系统内存不足时,他会被回收 应用举例: 假如有一个应用程序需要读取大量图片, 如果每次读取图片都从硬盘读取则会严重影响性能, 如果一次性全部加载到内存中又可能造成内存溢出。 此时可以使用软引用来解决。 设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系, 当内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。 Map
> imageCache = new HashMap @Test public void softReferenceTest(){ Object o = new Object(); SoftReference<Object> softReference = new SoftReference<>(o); System.out.println(o);//java.lang.Object@32a1bec0 System.out.println(softReference.get());//java.lang.Object@32a1bec0 o = null; System.out.println(o);//null System.gc(); System.out.println(o);//null System.out.println(softReference.get());//java.lang.Object@32a1bec0 try { // -Xms1024m -Xmx1024m byte[] bytes = new byte[ 1024 *1024 *1024 ]; // java.lang.OutOfMemoryError: Java heap space }catch (Exception e){ e.printStackTrace(); }finally { System.out.println(o); //null System.out.println(softReference.get()); //null } } /* 弱引用需要用 java.lang.ref.WeakReference 类来实现,他比软引用生存周期更短; 不管JVM内存是否够用,只要有GC运行就会被回收 */ @Test public void weakReferenceTest(){ Object o = new Object(); WeakReference<Object> weakReference = new WeakReference<>(o); System.out.println(o); //java.lang.Object@32a1bec0 System.out.println(weakReference.get()); //java.lang.Object@32a1bec0 o = null; System.gc();//o 置为 null,之后才能进行 gc System.out.println(o); // null System.out.println(weakReference.get()); //null } /** * 引用队列:ReferenceQueue * 弱引用、软引用、虚引用 在被GC回收之后,会被放入引用队列 */ @Test public void referenceQueueTest(){ Object o = new Object(); ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); WeakReference<Object> weakReference = new WeakReference<>(o, referenceQueue); System.out.println(o); //java.lang.Object@32a1bec0 System.out.println(weakReference.get()); //java.lang.Object@32a1bec0 System.out.println(referenceQueue.poll()); // null o = null; System.gc(); System.out.println(o); // null System.out.println(weakReference.get()); // null System.out.println(referenceQueue.poll()); //java.lang.ref.WeakReference@22927a81 } /* 虚引用:需要用java.lang.ref.PhantomReference类来实现 虚引用并不会决定对象的生命周期. 如果一个对象持有虚引用,那么他就和没有任何引用一样,随时会被GC回收, 也不能单独通过它来访问对象,虚引用必须和引用队列(ReferenceQueue)配合使用 虚引用的主要作用是跟踪对象被垃圾回收的状态。 设置虚引用的唯一目的就是在这个对象被垃圾回收的时候 收到一个系统通知或者后续添加进一步的处理。 Java中允许使用finalize()【完成】方法在GC将对象从内存中清除出去之前做必要的清理工作。 */ @Test public void phantomReferenceTest(){ Object o = new Object(); ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); PhantomReference<Object> phantomReference = new PhantomReference<>(o, referenceQueue); System.out.println(o); //java.lang.Object@32a1bec0 System.out.println(phantomReference.get()); //null System.out.println(referenceQueue.poll()); // null o = null; System.gc(); System.out.println(o); // null System.out.println(phantomReference.get()); // null System.out.println(referenceQueue.poll()); //java.lang.ref.PhantomReference@22927a81 }>(); */
WeakHashMap是一种弱引用map,内部的key会存储为弱引用,当jvm gc的时候,如果这些key 没有强引用存在 的话,会被gc回收掉,下一次当我们操作map的时候会把对应的Entry整个删除掉,基于这种特性,WeakHashMap特别适用于缓存处理。
存储结构: WeakHashMap因为gc的时候会把没有强引用的key回收掉,所以注定了它里面的元素不会太多,因此也就不需要像HashMap那样元素多的时候转化为红黑树来处理了.因此,WeakHashMap的存储结构只有(数组 + 链表)。
public class WeakHashMap<K,V> extends AbstractMap<K,V> implements Map<K,V> {
//桶
Entry<K,V>[] table;
//引用队列,当弱键失效的时候会把Entry添加到这个队列中,当下次访问map的时候会把失效的Entry清除掉。
private final ReferenceQueue<Object> queue = new ReferenceQueue<>();
}
Entry 内部类
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V> { // 可以发现没有key, 因为key是作为弱引用存到Referen类中 V value; final int hash; Entry<K,V> next; /** * Creates new entry. */ Entry(Object key, V value,ReferenceQueue<Object> queue,int hash, Entry<K,V> next) { // 调用WeakReference的构造方法初始化key和引用队列 /* WeakReference 又调用父类的构造方法 public WeakReference(T referent, ReferenceQueue super T> q) { super(referent, q); } */ super(key, queue); this.value = value; this.hash = hash; this.next = next; } } public abstract class Reference<T> { // 实际存储key的地方 //key就是Reference的referent属性,它会被gc特殊对待,即当没有强引用存在时,当下一次gc的时候会被清除。 private T referent; /* Treated specially by GC */ // 引用队列 volatile ReferenceQueue<? super T> queue; Reference(T referent, ReferenceQueue<? super T> queue) { this.referent = referent; this.queue = (queue == null) ? ReferenceQueue.NULL : queue; } } private T referent; /* Treated specially by GC */
put
自己看 JDK 1.8 注释
计算 hash -> 确定哈希桶 -> 遍历链表 -> 找到替换 -> 没找到插在哈希桶的开头 -> size++ 判断是否需要扩容
和 HashMap 不同:
HashMap key 为空,直接返回 0, 这里使用 空对象
//HashMap return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); //WeakHashMap return (key == null) ? NULL_KEY : key; --> hash(key)
求 hash 值 : HashMap只用了一次异或,这里用了四次
扩容: HashMap中是大于threshold才扩容,这里等于threshold就开始扩容了
//WeakHashMap if (++size >= threshold) resize(tab.length * 2); //HashMap if (++size > threshold) resize();//两倍扩容使用的是 oldCap << 1
resize
/* ? 不确定 : getTable() 调用 expungeStaleEntries() 清除 queue 中的结点即 无效的 key, 此时 value,next 即 Entry 还存在 Entry
[] table 中,在扩容转移元素的时候,会判断 key 是否为 null, key 为 null 表示已经无效了(在queue中,已被清除),此时需要 清除对应的 Entry. 因此, transfer 之后 会出现 table 中元素减少的情况. if (size >= threshold / 2) --> 继续扩容, 否则还原,不扩容. */ //详细代码注释看 彤哥读源码公众号 // transfer -> 1) 扩容数组,转移数据(头插法) 2)清除失效的 key 对应的 Entry private final ReferenceQueue<Object> queue = new ReferenceQueue<>();// 引用队列,存放可以已经回收的对象? private void expungeStaleEntries() //删除 queue 队列中的结点
使用案例
@Test public void useWeakHashMap(){ Map<String,Integer> map = new WeakHashMap<>(); map.put(new String("1"),1); map.put(new String("2"),2); map.put(new String("3"),3); map.put("6",6); //使用 key 强引用 "3" 这个字符串 String key = null; for (String s:map.keySet()){ if (s.equals("3")){ key = s; } } System.out.println(map);//{6=6, 1=1, 2=2, 3=3} System.gc(); map.put(new String("4"),4); // gc 后 放入的值 (4)和 强引用的 key (3,6) 可以打印出来 System.out.println(map);//{4=4, 6=6, 3=3} key = null; System.gc(); System.out.println(map);//{6=6} /* Entry(Object key, V value,ReferenceQueue }
小结
(1)WeakHashMap使用(数组 + 链表)存储结构;
(2)WeakHashMap中的key是弱引用,gc的时候会被清除;
(3)每次对map的操作都会剔除失效key对应的Entry;
(4)使用String作为key时,一定要使用new String()这样的方式声明key,才会失效,其它的基本类型的包装类型是一样的;
(5)WeakHashMap常用来作为缓存使用;
参考: 彤哥读源码
构造方法以及成员变量
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable{
//65535,标识并发扩容最多线程数
private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
transient volatile Node<K,V>[] table;
//===============================================================================================
//扩容
private transient volatile Node<K,V>[] nextTable;//扩容后的新数组,扩容完成在赋值给 table
/**
* The next table index (plus one) to split while resizing.
* 记录扩容过程中,记录当前进度,所有线程都需要从 transferIndex 中分配区间任务,去执行自己的任务
*/
private transient volatile int transferIndex;
//===============================================================================================
//用来计算容量 思想和 LongAdder 一样
//LongAdder 中的 baseCount未发生竞争时,或者当前 LongAdder 处于加锁状态时,增量累加到 baseCount 中
private transient volatile long baseCount;
/**
* Spinlock (locked via CAS) used when resizing and/or creating CounterCells.
* LongAdder 中的 cellsBusy :0 无锁 1:加锁
*/
private transient volatile int cellsBusy;
/**
* Table of counter cells. When non-null, size is a power of 2.
* LongAdder 中的 cells 数组,当 baseCount 发生竞争后,会创建 cells 数组,
* 线程会通过计算 hash 值 取到 自己的 cell,将增量累加到指定的 cell 中
*/
private transient volatile CounterCell[] counterCells;
/*sizeCtl < 0
* 1) -1 : 表示当前 table 正在初始化(有线程正在创建 table 数组),当前线程需要自旋等待...
* 2) 表示当前 map 正在扩容, 高 16 位表示扩容的标识戳,低16位表示 (1+nThread) 当前参与并发的线程数量
* sizeCtl = 0: 表示创建 table[] 时,使用 DEFAULT_CAPACITY 大小
* sizeCtl > 0
* 1) 如果 table 未初始化,表示初始化大小
* 2) 如果 table 已经初始化,表示下次扩容时的触发条件 (阈值)
*/
private transient volatile int sizeCtl;
//===============================================================================================
// 构造方法
public ConcurrentHashMap() {
}
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
//sizeCtl > 0
//如果 table 未初始化,表示初始化大小
this.sizeCtl = cap;
}
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
}
put 操作
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent){
//...
// 如果是 FWD 结点,当前线程帮助扩容
//final Node[] helpTransfer(Node[] tab, Node f)
/*
ForwardingNode extends Node
ForwardingNode(Node[] tab) {
super(MOVED, null, null, null);
this.nextTable = tab;
}
*/
// binCount 不同值表示的含义
// 表示当前 k-v 封装成 node 后插入到指定桶位后,在桶位中所属链表的下标位置
// 0 :表示当前桶位为 null, node 可以直接放
//2:表示当前桶位已经树化为红黑树
addCount(1L, binCount);
}
//涉及到别的方法
//数组初始化
private final Node<K,V>[] initTable()
//插入成功后,计算容量大小 (多线程一起计算,参考 LongAdder)
private final void addCount(long x, int check)
//涉及到数组的扩容 (多线程并发扩容)
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab)
put 的 流程 以及加锁的地方
(1)如果桶数组未初始化,则初始化;(自旋 + CAS)
(2)如果待插入的元素所在的桶为空,则尝试把此元素直接插入到桶的第一个位置;(CAS)
(3)如果正在扩容,则当前线程一起加入到扩容的过程中;// helpTransfer
(4)如果待插入的元素所在的桶不为空且不在迁移元素,则锁住这个桶(分段锁,使用 synchronized 锁住 桶的头结点);
(5)如果当前桶中元素以链表方式存储,则在链表中寻找该元素或者插入元素;
(6)如果当前桶中元素以红黑树方式存储,则在红黑树中寻找该元素或者插入元素;
(7)如果元素存在,则返回旧值;
(8)如果元素不存在,整个Map的元素个数加1,并检查是否需要扩容;
流程图:
initTable() 步骤: (来自彤哥读源码)
(1)使用CAS锁控制只有一个线程初始化桶数组;
(2)sizeCtl在初始化后存储的是扩容门槛;
(3)扩容门槛写死的是桶数组大小的0.75倍,桶数组大小即map的容量,也就是最多存储多少个元素。
addCount : 每次添加元素后,元素数量加1,并判断是否达到扩容门槛,达到了则进行扩容或协助扩容。
(1)元素个数的存储方式类似于LongAdder类,存储在不同的段上,减少不同线程同时更新size时的冲突;
(2)计算元素个数时把这些段的值及baseCount相加算出总的元素个数;
(3)正常情况下sizeCtl存储着扩容门槛,扩容门槛为容量的0.75倍;
(4)扩容时sizeCtl高位存储扩容邮戳(resizeStamp),低位存储扩容线程数加1(1+nThreads);
(5)其它线程添加元素后如果发现存在扩容,也会加入的扩容行列中来;
//addCount 调用的 是 transfer 扩容方法 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) public int size() { long n = sumCount(); return ((n < 0L) ? 0 : (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : (int)n); } // 计算出 baseCount 和 cells 数组中每个 cell 的值 final long sumCount() { CounterCell[] as = counterCells; CounterCell a; long sum = baseCount; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
transfer 扩容时容量变为两倍,并把部分元素迁移到其它桶中。
- 16位 扩容到 32 位, xxxx … xxxH 0110 (原来在第 5 个桶中) , 根据 hash 值 高一位(H)值,判断扩容后落在哪个桶, 如果 H 为 1,则落到 5+16 = 21 , 如果 H = 0, 则还在 第 5 个桶
- private final void transfer(Node
[] tab, Node [] nextTab) 步骤:
(1)新桶数组大小是旧桶数组的两倍;
(2)迁移元素先从靠后的桶开始; (transferIndex = n;)
(3)迁移完成的桶在里面放置一ForwardingNode类型的元素,标记该桶迁移完成;
(4)迁移时根据hash&n是否等于0把桶中元素分化成两个链表或树;
(5)低位链表(树)存储在原来的位置;
(6)高位链表(树)存储在原来的位置加n的位置;
(7)迁移元素时会锁住当前桶,也是分段锁的思想;
流程图:
- me:
- 首先判断当前线程是不是第一次扩容,是则初始化 扩容后得新数组 nextTable
- 在有新的线程来,从 最后一个桶分配任务区间(区间由 CPU 个数决定,假设为 16),分配到任务后,执行扩容任务,扩容任务完成后将 旧桶的头结点 设置 FWD 结点. (实际进行迁移时, 给桶的头结点加上 synchronized,锁住当前的桶)
- 新的线程继续进来时,如果任务已经分配完毕,则将线程数量-1,直接 return 退出循环,如果是最后一个线程,则最后一个线程还要在检查一遍,是否全部迁移完成,完成了在 给 table 重新赋值,以及计算扩容阈值==(sizeCtl==),还要给 nextTable = null
- 迁移数据时,低位链表一起迁移,高位链表一起迁移,少 new 了一些对象
- 红黑树的结点: 拆分成高低链表后,如果长度小于6,则退化成 链表插入新的 桶中
helpTransfer 线程添加元素时发现正在扩容且当前元素所在的桶元素已经迁移完成了,则协助迁移其它桶的元素。
都是满足当前桶的头结点是 FWD 结点,才会调用 helpTransfer 方法
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) //判断当前还在扩容,则 调用 transfer 方法进行扩容 -> transfer(tab, nextTab);
get 获取元素,根据目标key所在桶的第一个元素的不同采用不同的方式获取元素,关键点在于find()方法的重写。
public V get(Object key)
(1)hash到元素所在的桶;
(2)如果桶中第一个元素就是该找的元素,直接返回;
(3)如果是树(Treebin)或者正在迁移元素(fwd),则调用各自Node子类的find()方法寻找元素;
(4)如果是链表,遍历整个链表寻找元素;
(5)获取元素没有加锁;
//volatile可以修饰数组是指array的地址是volatile的 transient volatile Node<K,V>[] table; private transient volatile Node<K,V>[] nextTable; static class Node<K,V> implements Map.Entry<K,V> { final int hash; final K key; volatile V val; volatile Node<K,V> next; }
为什么 get 不需要加锁?
在1.8中ConcurrentHashMap的get操作全程不需要加锁,这也是它比其他并发集合比如hashtable、用Collections.synchronizedMap()包装的hashmap;安全效率高的原因之一。
get操作全程不需要加锁是因为Node的成员val 以及 next 是用volatile修饰的和数组用volatile修饰没有关系。
刚插入的结点会更新到 next 上,因为 是 volatile 修饰的,因此也能看到.
数组用volatile修饰主要是保证在数组扩容的时候保证可见性。
remove 删除元素跟添加元素一样,都是先找到元素所在的桶,然后采用分段锁的思想锁住整个桶,再进行操作。
// value == null,就是删除操作 (即新值为 null) // value 是新值, cv 是旧值/null final V replaceNode(Object key, V value, Object cv) //1) public V remove(Object key) { return replaceNode(key, null, null); } //2) public boolean replace(K key, V oldValue, V newValue) { if (key == null || oldValue == null || newValue == null) throw new NullPointerException(); return replaceNode(key, newValue, oldValue) != null; } //3) public V replace(K key, V value) { if (key == null || value == null) throw new NullPointerException(); return replaceNode(key, value, null); } //4) public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) { //.... if (replaceNode(key, newValue, oldValue) != null ||... }
(1)计算hash;
(2)如果所在的桶不存在,表示没有找到目标元素,返回;
(3)如果正在扩容,则协助扩容完成后再进行删除操作;
(4)如果是以链表形式存储的,则遍历整个链表查找元素,找到之后再删除;
(5)如果是以树形式存储的,则遍历树查找元素,找到之后再删除;
(6)如果是以树形式存储的,删除元素之后树较小,则退化成链表;
(7)如果确实删除了元素,则整个map元素个数减1,并返回旧值;
(8)如果没有删除元素,则返回null;
key
小结
(1)ConcurrentHashMap是HashMap的线程安全版本;
(2)ConcurrentHashMap采用(数组 + 链表 + 红黑树)的结构存储元素;
(3)ConcurrentHashMap相比于同样线程安全的HashTable,效率要高很多;
(4)ConcurrentHashMap采用的锁有 synchronized,CAS,自旋锁,分段锁,volatile等;
(5)ConcurrentHashMap中没有threshold和loadFactor这两个字段,而是采用sizeCtl来控制;
(6)sizeCtl = -1,表示正在进行初始化;
(7)sizeCtl = 0,默认值,表示后续在真正初始化的时候使用默认容量;
(8)sizeCtl > 0,在初始化之前存储的是传入的容量,在初始化或扩容后存储的是下一次的扩容门槛;
(9)sizeCtl = (resizeStamp << 16) + (1 + nThreads),表示正在进行扩容,高位存储扩容邮戳,低位存储扩容线程数加1;
(10)更新操作时如果正在进行扩容,当前线程协助扩容;
(11)更新操作会采用synchronized锁住当前桶的第一个元素,这是分段锁的思想;
(12)整个扩容过程都是通过CAS控制sizeCtl这个字段来进行的,这很关键;
(13)迁移完元素的桶会放置一个ForwardingNode节点,以标识该桶迁移完毕;
(14)元素个数的存储也是采用的分段思想,类似于LongAdder的实现;
(15)元素个数的更新会把不同的线程hash到不同的段上,减少资源争用;
(16)元素个数的更新如果还是出现多个线程同时更新一个段,则会扩容段(CounterCell);
(17)获取元素个数是把所有的段(包括baseCount和CounterCell)相加起来得到的;
(18)查询操作是不会加锁的,所以ConcurrentHashMap不是强一致性的;
并发读写数据一致性保证(一)Java并发容器
(19)ConcurrentHashMap中不能存储key或value为null的元素;
思考之学习到的技术
(1)CAS + 自旋,乐观锁的思想,减少线程上下文切换的时间;
(2)分段锁的思想,减少同一把锁争用带来的低效问题;
(3)CounterCell,分段存储元素个数,减少多线程同时更新一个字段带来的低效;
(4)@sun.misc.Contended(CounterCell上的注解),避免伪共享;(p.s.伪共享我们后面也会讲的^^)
(5)多线程协同进行扩容;
ConcurrentHashMap 不能解决的问题
// ConcurrentHashMap 不能解决的问题 private static final Map<Integer,Integer> concurrentMap = new ConcurrentHashMap<>(); /** * 如果key不存在,则将 key value 插入到 map 中 * 多线程调用还是会出现安全问题,当线程 A,B同时走到 if 判断都为 null, * 如果线程A先行执行完 if 中的语句,那么线程 B 会再次执行一次,覆盖之前的数据 * @param key * @param value */ public void test01(Integer key,Integer value){ Integer oldValue = concurrentMap.get(key); if (oldValue == null){ concurrentMap.put(key,value); //解决方案,使用 concurrentMap.putIfAbsent(key,value); //final V putVal(K key, V value, boolean onlyIfAbsent) onlyIfAbsent 为 true,而 put 调用时为 false // 当 元素不存在时,才插入 } } /** * 不和 null 比较, 和 1 比较,此时 putIfAbsent 就不能使用了, * 需要调用 replace(K key, V oldValue, V newValue) 方法 * @param key * @param value */ public void test02(Integer key,Integer value){ Integer oldValue = concurrentMap.get(key); if (oldValue == 1){ concurrentMap.put(key,value); } // 把上面的 if 换成 即可 concurrentMap.replace(key,1,value); // 注意: 如果 value 为 null,则 此时为删除结点操作 } /** * 此时就无法使用 ConcurrentHashMap 提供的方法来保证线程安全了 * 只能自己在外面加锁 如: synchronized ,此时可以直接用 HashMap, 用 ConcurrentHashMap 也就没有意义了 * @param key * @param value */ public void test03(Integer key,Integer value){ Integer oldValue = concurrentMap.get(key); if (oldValue == 1){ /* * 执行一些其他的业务操作 * xxxxxxxxx * xxxxxxxxx * */ System.out.println("do something"); concurrentMap.put(key,value); } } @Test //来个错误示范, 这里涉及到了两个操作,put 和 get,合起来用是线程不安全的. public void test04() throws InterruptedException { concurrentMap.put(1,0); ExecutorService executorService = Executors.newFixedThreadPool(1000); System.out.println(concurrentMap.get(1));//0 for (int i = 0; i < 1000; i++) { executorService.execute(()->{ /*Integer a = concurrentMap.get(1)+1; concurrentMap.put(1,a);*/ concurrentMap.put(1,concurrentMap.get(1)+1); /*synchronized (this){ concurrentMap.put(1,concurrentMap.get(1)+1); }*/ }); } Thread.sleep(5000); System.out.println(concurrentMap.get(1)); //992,不是 1000 }