ArrayList
ArrayList概览
数组大小默认为10
private static final int DEFAULT_CAPACITY = 10;
ArrayList序列化
之前序列化章节专门介绍过
ArrayList扩容机制
// JDK 1.8
// ArrayList的添加操作里首先会检查是否越界,如果越界则扩容成原来数组大小的1.5倍
// ArrayList的add传入的是地址值副本,也就是说我在外面改变e的属性,ArrayList里的e也会改变!!!
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
public boolean addAll(Collection<? extends E> c) {
// 但是ArrayList的addAll方法不会,因为源码里面是对传入参数复制了一份并没有使用传入参数的地址
Object[] a = c.toArray();
//........
}
// 判断数组是否越界
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {//这里是懒加载机制,添加元素时才初始化数组
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
//这里ArrayList由于添加操作改变了原有结构,modCount++
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); // 扩容大小是原来的1.5倍
//如果扩容后还是比minCapacity小,说明这是第一次初始化数组,则直接赋值为minCapacity
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果扩容的大小大于允许的数组最大值,那就直接赋值为Integer.MAX_VALUE
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
//Arrays.copy()方法封装了system.arrayCopy()方法
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;
}
ArrayList删除机制
//ArrayList删除机制首先检查index是否合法,然后将删除元素后面的元素移动,最后将新数组的最后一位置为null,引发GC回收,然后返回被删除的元素。
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;
}
ArrayList的Fail-Fast机制
fail-fast机制中的核心字段就是之前扩容操作中的modcount,在使用iterator遍历集合或者进行序列化的时候如果集合的结构发生了变化,会抛出Concurrent Modification Exception异常。
在使用iterator遍历集合或者序列化之前,会将之前保存过的modcount与当前modcount作比较,如果不同就说明数组结构已经发生了改变,如果继续下去遍历和序列化都将无意义。
public E next() {
checkForComodification();
int i = cursor;
if (i >= SubList.this.size)
throw new NoSuchElementException();
Object[] elementData = root.elementData;
if (offset + i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[offset + (lastRet = i)];
}
final void checkForComodification() {
if (root.modCount != expectedModCount)
throw new ConcurrentModificationException();
}
LinkedList
LinkedList的底层数据结构是由双向链表实现的,它并没有实现RandomAccess接口,但在add和remove方法中同样可以指定index,并且为了提高效率方法中还会先检查index是离对首比较近还是离队尾比较近。
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
Node<E> node(int index) {
// assert isElementIndex(index);
//检查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;
}
}
LinkedList的插入和删除的效率都很高,都是常数操作,但是不支持RandomAccess,查找操作都需要进行遍历,效率很低。
Vector
//vector有两种创建方式,其中一种可以指定每次扩容增加的空间
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
//vector的扩容机制与ArrayList扩容机制不太一样,ArrayList扩容倍数是1.5,vevtor是2倍
private int newCapacity(int minCapacity) {
...
int oldCapacity = elementData.length;
//这儿的capacityIncrement就是之前提到的扩容空间,如果我们没有特别指定的话就是0,如果是0要扩容的时候扩容值就是以前old数组的两倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
...
}
vector的方法都被synchronized关键字修饰,用于多线程同步,但一般情况不建议使用vector,因为效率较低。
效率较低的原因有
- 方法都是线程安全的
- 每次扩容都是扩两倍
- 插入删除操作都只能在尾部操作
几种线程安全的List
- Vector
- 使用工具类collections中的synchronizedList方法返回一个线程安全的List,方法如下
public static <T> List<T> synchronizedList(List<T> list) {
return (list instanceof RandomAccess ?
new SynchronizedRandomAccessList<>(list) :
new SynchronizedList<>(list));
}
//首先对输入参数list进行检查,看是否支持随机访问,比如linkedList就不支持,因为他是链表
//然后返回一个线程安全的list。
- CopyOnWriteArrayList
CopyOnWriteArrayList 类在concurrent并发包下, CopyOnWriteArrayList基本思想是读写分离,当对CopyOnWriteArrayList容器进行写操作时,会在自己的线程下复制一份完全相同的容器,然后对复制的容器进行操作,完成写操作后再将原来的引用指向这个复制的容器。
//写操作加锁,读操作get()不加锁
public boolean add(E e) {
//加锁
synchronized (lock) {
Object[] es = getArray();
int len = es.length;
//在这里容器进行了复制,生成了一个新的数组
es = Arrays.copyOf(es, len + 1);
es[len] = e;
setArray(es);
return true;
}
}
这样的好处就是在多线程情况下可以并发的读取。但是读的数据会是旧容器里的内容。这样的坏处就是比如当一个线程在执行读操作,另一个线程在执行remove操作时,remove操作完毕size-1,有可能会出现查询index数组越界的情况。总的来说就是不能保持数据的实时一致性。还有一个缺点就是内存消耗过大,在进行写操作时内存区中会同时存在新旧两个对象。会导致频繁的Young GC和Full GC
HashMap
transient int size;//HashMap中实际键值对数量
transient Node<K,V>[] table;//HashMap的数组
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;//懒加载,默认数组初始容量16
transient int modCount;//与之前的arratlist一样
final float loadFactor;//负载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;//默认负载因子0.75
int threshold;//承载量,当大于承载量会促发扩容操作。threshold = capacity * loadFactor
static final int TREEIFY_THRESHOLD = 8;//红黑树默认阈值8,当链表长度为8时会转为红黑树
HashMap中的hash()方法
//HashMap中是可以存储null值的,如果是null直接放在数组0处
//key值不为null会首先获取key类型的hashcode方法的hash值,然后对这个值无符号右移16位,再与原值异或
//使用^而不使用&或|的原因是因为异或操作更能保留各部分的特征。关于为何要右移16位后面有讲
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
HashMap中的putVal()方法
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//1.第一步先判断数组尺寸是否为空,为空的进行resize()
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
//2.获取key值得hash值然后确定哈希桶数组的索引,若那个index的‘桶’里连头节点都还没有,
//将这个值设为头节点。
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//如果数组index那已经有节点了,进行比较如果与头节点key值相同,替换,并记录被替换的值
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
//如果数组index那的节点已经是红黑树的节点了,进入红黑树模式
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果key值与头节点不一样,那么就顺着节点一个一个比较直到被比较的节点next为null
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//如果此时链表长度等于单个链表承载阈值-1的话,需要转化为红黑树结构
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
//如果e有值,说明这次put的值与链表或者红黑树中的某个节点key值一样,e保存了被替换了的 //节点的值,因为是被替换数组结构并被被改变所以modcount不需要改,e为null才需要改
if (e != null) { // existing mapping for 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;
}
hashmap扩容机制
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
//newCap,newThr表示接下来一样是创建一个数组然后把原数组内容复制进去
int newCap, newThr = 0;
if (oldCap > 0) {
//如果原数组长度已经达到int最大值了说明不能进行长度扩容了
//此时只能将负载量扩大
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//如果原数组没有达到最大,将原数组长度扩大两倍然后复制给newCap
//负载因子不变,所以newThr=oldThr
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
//这是当oldThr小于等于0的情况,这种情况的发生是初次创建hashMap没有设置threshold值
//相当于懒加载机制
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
//新建了初始容量为newCap的Node数组
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//对原哈希桶数组进行遍历
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//如果e节点的next节点为空
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
//如果节点是红黑树类型,则把书上的节点放导newTab中
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
//如果next节点不为空,则分为两组,low组和high组
else { // preserve order
//这里定义的loHead,loTail,hiHead,hiTail本人猜测分别是low和high
//因为数组扩容了一倍,并不需要把原数组的索引和链表完完全全照搬到新数组
//所以jdk1.8中对其进行了改进
//例如有一个hash=18 二进制为0001 0010,还有个hash=2 二进制为0000 0010
//原数组长度为16,二进制为0001 0000,那么这两个节点索引都为2
//新数组长度为32,二进制为0010 0000,这两个节点一个会放到高位(high)索引
//一个会被放到低位(low)索引去。
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//将节点与原数组长度相与,如果等于0,就跟以前的索引位一样
//这里的数据结构就是一个链表,其中头部包含一头一尾两节点
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//如果不等于0,就放到高位索引上去
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
//!!!!这里就是高位与低位的不同处,上面我们其实可以通过例子运算
//得到结果,就是高位会比低位多一个原数组长度
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
这里还有一点值得说的是,jdk1.7之前的resize()方法使用的是头插法,上面jdk1.8我们可以很明显看出是尾插法,头插法的缺点就是当在多线程的使用情况下,出现扩容操作时会因为头插法可能产生环形链表的问题。
hashmap的put方法中确定索引的方式分析
p = tab[i = (n - 1) & hash]
确定索引的方式是将数组长度大小减一然后&上hash值,因为规定了hashmap数组的大小只能为2的n次方大小,所以n-1相当于时低位掩码,实际上就相当于hash值对数组长度取模,不用'%'的原因是为运算的效率要远远大于它,如果hashmap数组长度大小不是2的n次方,那么会造成hash冲突
hashmap的hash方法中为什么右移16位 ?
(h = key.hashCode()) ^ (h >>> 16)
首先对key使用key类型的hashcode方法获得一个int类型的散列值,再跟它无符号右移16位的数进行异或,这样做的好处是因为我们的数组容量大小通常设置的不会很大,初始容量n只有16,在进行对hash值分析找数组的index的时候会进行 (n - 1) & hash ,如果不右移16位异或的话hash的高位值几乎用不上,这样会导致hash冲突。
hashmap中为何链表节点数到8才会触发红黑树机制?
实际上并不只是到8才会变成红黑树,此时数组的长度还得必须大于64,满足这两个条件才会出发红黑树机制,具体查看treeifyBin()方法,而为何要选用8这个数字是因为链表长度符合泊松分布,链表长度为8概率已经非常低了。
static final int MIN_TREEIFY_CAPACITY = 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)
...
}
红黑树查找的时间复杂度 红黑树和二叉树的区别 ?
ConcurrentHashMap(JDK1.8)
折磨王来了
ConcurrentHashMap几个重要成员属性(后面还有)
//ConcurrentHashMap的用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修饰,保持内存可见性
volatile V val;
volatile Node<K,V> next;
//......
}
ConcurrentHashMap的putVal()方法
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//计算hash值,与hashMap一样
int hash = spread(key.hashCode());
int binCount = 0;
//进入死循环
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh; K fk; V fv;
//1.如果tab还没初始化,进行初始化,否则进入步骤2,(注意,这是懒加载机制)
//*懒加载机制好处:为了防止初始化后的首次操作就需要扩容(比如putAll),从而影响效率
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//2.如果tab中的i索引处还没有节点,使用CAS原语将该处设置为输入节点,否则进入3
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
break; // no lock when adding to empty bin
}
//3.若桶中该节点的hash值为MOVED,则对该桶中的节点进行转移,否则进入4
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
//4.如果输入节点的key值与桶的头节点一样,则替换桶中的节点,否则进入5
else if (onlyIfAbsent // check first node without acquiring lock
&& fh == hash
&& ((fk = f.key) == key || (fk != null && key.equals(fk)))
&& (fv = f.val) != null)
return fv;
else {
//5.对该处桶节点进行加锁操作,之后的操作与hashmap一样,相同则替换,直到末尾还没有 //相同的就放到队尾
V oldVal = null;
//这里是对桶的单个对象进行加锁,并不是整个数组加锁也不是1.7分段加锁,粒度更细
synchronized (f) {
// 再次检查,即double check
// 即避免进入同步块之前,链表被修改了
if (tabAt(tab, i) == f) {
//如果fh大于0那么就是链表形式
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);
break;
}
}
}
//之前判断过了fh既不是moved状态也不是链表,那么得判断是不是红黑树结构
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;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
ConcurrentHashMap的initTable()方法
//sizeCtl默认为0,用来控制数组初始化和扩容
//sizeCtl=-1,代表此时数组正在初始化
//sizeCtl=-N,代表此时数组正在被N-1个线程扩容
//当数组还没被初始化,表示数组需要初始化的大小
//当数组已经被初始化,表示数组的扩容阈值,为数组长度的0.75倍
private transient volatile int sizeCtl;
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
//如果sizeCtl小于0说明是其他线程已经在进行初始化工作了,此时只需要让出时间片
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
//如果不小于0则将其设置为-1进行初始化操作,防止其他线程进入
else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) {
try {
//二次检查
if ((tab = table) == null || tab.length == 0) {
//ConcurrentHashMap构造函数里sizeCtl被赋值为DEFAULT_CAPACITY,16
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
//这里相当于0.75倍,位运算效率高
sc = n - (n >>> 2);
}
} finally {
//此时sizeCtl变为扩容阈值12而不是数组容量16
sizeCtl = sc;
}
break;
}
}
return tab;
}
ConcurrentHashMap的addCount()方法(putVal代码有使用)
//该方法可能会触发扩容机制
private final void addCount(long x, int check) {
CounterCell[] cs; long b, s;
if ((cs = counterCells) != null ||
!U.compareAndSetLong(this, BASECOUNT, b = baseCount, s = b + x)) {
//省略.......首先会对basecount进行一个争抢
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
//这里的s是sumCount()方法获得的,通过反射可以知道这个方法是获取当前KV键值对的个数
//如果s大于了阈值sizeCtl则说明此时数组要么是需要扩容要么是数组还没初始化
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n) << RESIZE_STAMP_SHIFT;
if (sc < 0) {
//如果nextTable为null,transferIndex小于0,
//且是在sc小于0的情况下,说明数组并不是在扩容而是在初始化
if (sc == rs + MAX_RESIZERS || sc == rs + 1 ||
(nt = nextTable) == null || transferIndex <= 0)
break;
//其他线程在扩容,协助其他线程
if (U.compareAndSetInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
//仅当前线程扩容
else if (U.compareAndSetInt(this, SIZECTL, sc, rs + 2))
transfer(tab, null);
s = sumCount();
}
}
}
ConcurrentHashMap的transfer()方法(多线程扩容核心)
//transferIndex表示为'数组迁移索引',ConcurrentHashMap的扩容任务会被分成多个任务来让多个线程
//一起完成或者是一个线程一点一点完成,代码中与之配和的一个属性stride,'步长',一个线程会从
//transferIndex开始从后往前开始迁移,迁移stride个桶。一开始transferIndex在数组最右边
private transient volatile int transferIndex;
//ForwardingNode,字面意思即正在被迁移的节点,它的hash值被设置为moved,-1。相当于一个标志
static final class ForwardingNode<K,V> extends Node<K,V> {
final Node<K,V>[] nextTable;
ForwardingNode(Node<K,V>[] tab) {
super(MOVED, null, null);
this.nextTable = tab;
}
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
//1.设置stride步长
// stride 在单核下直接等于 n,多核模式下为 (n>>>3)/NCPU,最小值是 16
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
//2.nextTab为空,对nextTab初始化,容量为原数组的两倍
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
//注意这里的transferIndex=n的情况是nextTab为空的条件赋予的,其他线程来帮忙转移的时候 //transferIndex不等于n
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
//*这里的advance可以认为是一个迁移任务的完成标志,当他为true即可领取下一个迁移任务
//*finishiing可以认为是整个数组是否迁移完毕
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
//进入死循环
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
//3.设置nextBound,这个相当于每次迁移任务只迁移到某个索引位前
//这个while循环大致意思就是“i指向transferIndex,bound指向transferIndex-stride”。
while (advance) {
int nextIndex, nextBound;
//如果i--小于bound说明该线程的迁移任务全部完全或者第一次进入循环
if (--i >= bound || finishing)
advance = false;
//这个是当其他线程来领取迁移下标的时候transferIndex==0的情况
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSetInt
(this, TRANSFERINDEX, nextIndex,
//这里nextBound选取就是当前准备开始的index减去步长stride,如果大于0,
//就把这个大于0的结果赋值给bound,如果小于0就说明此次任务长度不够一个 //步长,bound直接赋值为0.
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
//4.检查是否完成了整个数组的迁移任务
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
//如果完成了将nextTable设为null,sizeCtl设为原数组容量的1.5倍,即现数组的0.75倍
if (finishing) {
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1);
return;
}
//如果没完成
//在进行迁移之前,sizeCtl被设置为rs+2,每有一个线程参与扩容操作,sizectl就会+1
//*加1操作具体参考helpTransfer()方法,完成迁移任务后使用CAS将sizeCtl减1
if (U.compareAndSetInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
//(sc - 2)等于resizeStamp(n) << RESIZE_STAMP_SHIFT,说明所有迁移任务完成
finishing = advance = true;
i = n; // recheck before commit
}
}
//5.如果没有完成,检查桶索引处节点是否为null,为null的话在此处放置fwd节点
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//6.如果此处节点为fwd,说明该位置已经被迁移过了
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
//7.如果节点不为null也不为fwd,那就需要对此处节点加锁进行迁移
else {
synchronized (f) {
//二次检查
if (tabAt(tab, i) == f) {
//这里的迁移方法跟hashmap扩容有点类似,都是为了使散列值更加均匀
//一个链表会被分成低位,高位两条链表
//1.首先在链表中找到一个lastRun节点,该节点之后的runbit都跟他一样
//2.lastRun节点的runbit如果假设0,那他就属于低位链表ln,否则属于高位链表hn
//3.从桶索引头节点开始遍历,直到lastRun节点因为lastRun后面节点跟他一样
//4.如果节点runbit为0,那它的next节点就接上上一个ln节点,高位同理
Node<K,V> ln, hn;
if (fh >= 0) {
int runBit = fh & n;
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
//迁移完将此处设置为fwd节点
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
advance = true;
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
}
}
}
ConcurrentHashMap的jdk1.7与1.8的区别`
- 锁的对象不同。1.7中ConcurrentHashMap是使用的 segment锁来保持线程安全的,1.8直接舍弃了segmen概念,将table数组中每个节点Node都使用volatile关键定义。
- 数据结构不同。1.7中ConcurrentHashMap数据结构是数组+链表,1.8是数组+链表+红黑树
- 锁的粒度不同。1.7是对需要操作的segment加锁,而1.8是对需要操作的数组元素加锁,这样锁的粒度更细
- 锁的方法不同。segment是使用ReentraintLock,而1.8是使用synchronized+CAS。
- 遍历时间复杂度不同。链表是O(n),红黑树是O(logn).
Hashtable
为什么Hashtable ,ConcurrentHashmap不支持key或者value为null?
Hashtable 与ConcurrentHashmap都是支持并发的,当get(key)获得的value为null时无法判断是这个key没做过映射,还是本来值设置的就是null,所以它得用contains(key)来去检查这个key是否做过映射,但在高并发的环境下让他这样去检查很可能情况已经改变了。