目录
5.1 BlockingQueue
5.2 BlockingDeque
5.3 CopyOnWrite
5.4 ConcurrentLikedQueue/Deque
5.5 ConcurrentHashMap
5.6 ConcurrentSkipListMap/Set
5.1 BlockingQueue
BlockingQueue是一个带有阻塞功能的队列,在Cocurrent包中,BlockingQueue是一个接口
BlockingQueue是具有阻塞功能的队列,如下源码:
public interface BlockingQueue<E> extends Queue<E> {
boolean add(E e);//不具有阻塞功能
boolean offer(E e);//不具有阻塞功能
void put(E e) throws InterruptedException;//具有阻塞功能
boolean offer(E e, long timeout, TimeUnit unit)
throws InterruptedException;//具有阻塞功能
E take() throws InterruptedException;//具有阻塞功能
E poll(long timeout, TimeUnit unit)
throws InterruptedException;//具有阻塞功能
boolean remove(Object o);//不具有阻塞功能
}
不具有阻塞功能的是BlockingQueue的父接口Queue中的方法。
下面介绍BlockingQueue各种实现
5.1.1 ArrayBlockingQueue
数组实现的环形队列,核心源码如下:
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
/** The queued items */
final Object[] items;
/** items index for next take, poll, peek or remove */
int takeIndex;
/** items index for next put, offer, or add */
int putIndex;
/** Number of elements in the queue */
int count;
/** Main lock guarding all access */
final ReentrantLock lock;//一把锁两个条件
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
}
其put/take函数如下所示:
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
public void put(E e) throws InterruptedException {
Objects.requireNonNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();//当队列满了,阻塞等待唤醒
enqueue(e);
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length) putIndex = 0;
count++;
notEmpty.signal();//唤醒队列不为空条件
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await();//当队列为空则阻塞等待被唤醒
return dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length) takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();//唤醒不为满的条件
return x;
}
}
5.1.2 LinkedBlockingQueue
单向链表的阻塞队列,因为对头和队尾是两个指针分别操作的,所以需要两把锁和两个条件,同时需要一个AtomicInteger的原子变量记录count数。两把锁的目的是为了更好的高并发处理
public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
private final AtomicInteger count = new AtomicInteger();
transient Node<E> head;
private transient Node<E> last;
private final ReentrantLock takeLock = new ReentrantLock();
private final Condition notEmpty = takeLock.newCondition();
private final ReentrantLock putLock = new ReentrantLock();
private final Condition notFull = putLock.newCondition();
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
while (count.get() == capacity) {
notFull.await();//队列满了等待
}
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;//唤醒前需要获取takeLock锁
takeLock.lock();
try {
notEmpty.signal();//通知队列不为空唤醒
} finally {
takeLock.unlock();
}
}
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
while (count.get() == 0) {
notEmpty.await();//队列为空,等待
}
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;//唤醒前需要获取putLock锁
putLock.lock();
try {
notFull.signal();//队列不满唤醒
} finally {
putLock.unlock();
}
}
}
如上,两把锁是为了更好的并发执行,比如:put和put阻塞,take和take阻塞,put和take不阻塞。并且由于是不用锁,所以唤醒前需要获取需要被唤醒条件的锁。对于count变量是put和take一起操作的,所以需要是原子变量。
5.1.3 PriorityBlockingQueue
队列通常是先进先出,而PriorityBlockingQueue是按照元素的优先级从小到大出队列的。原理是实现了Comparable接口。
源码如下:
public class PriorityBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
private transient Object[] queue;//用数组实现的二叉小根堆
private transient int size;
private transient Comparator<? super E> comparator;
private final ReentrantLock lock;//一把锁+一个条件,没有非满条件
private final Condition notEmpty;
public void put(E e) {
offer(e); // never need to block
}
public boolean offer(E e) {
if (e == null)
throw new NullPointerException();
final ReentrantLock lock = this.lock;
lock.lock();
int n, cap;
Object[] array;
while ((n = size) >= (cap = (array = queue).length))
tryGrow(array, cap);//当超过当前队列最大容量,则进行扩容
try {
Comparator<? super E> cmp = comparator;
if (cmp == null)//没有定义比较操作符,使用元素自带的比较功能
siftUpComparable(n, e, array);//元素入堆
else
siftUpUsingComparator(n, e, array, cmp);
size = n + 1;
notEmpty.signal();//不为空唤醒
} finally {
lock.unlock();
}
return true;
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
E result;
try {
while ( (result = dequeue()) == null)
notEmpty.await();//为空等待
} finally {
lock.unlock();
}
return result;
}
private E dequeue() {
int n = size - 1;
if (n < 0)
return null;
else {
Object[] array = queue;
E result = (E) array[0];//因为是最小二叉堆,堆顶即是要出队的元素
E x = (E) array[n];
array[n] = null;
Comparator<? super E> cmp = comparator;
if (cmp == null)
siftDownComparable(0, x, array, n);//调整堆,也就是执行siftDown操作
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
}
和ArrayBlockingQueue相似,区别是多了一个二叉堆,用于排序。另一个区别是没有notFull条件,当超出长度进行扩容。
5.1.4 DelayQueue
延迟队列,也就是按照延迟时间从小到大出队的PriorityQueue
//放入DelayQueue的元素必须实现Delayed 接口
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);//延迟时间
}
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
private final PriorityQueue<E> q = new PriorityQueue<E>();//优先级队列
private final transient ReentrantLock lock = new ReentrantLock();//一把锁+一个条件
private final Condition available = lock.newCondition();
public void put(E e) {
offer(e);
}
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);//元素放入二叉堆
if (q.peek() == e) {//如果元素刚好在堆顶,则通知等到线程
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
E first = q.peek();//取出二叉堆的堆顶元素,也就是延迟时间最小的
if (first == null)
available.await();//队列为空,阻塞等待
else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0L)//延迟时间小于或者等于0,出队列,返回
return q.poll();
first = null; // don't retain ref while waiting
if (leader != null)
available.await();//如果有其他线程在等待,则无限期等待
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);//如果没有其他线程在等待,则根据延迟时间有限期等待
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
}
5.1.5 SynchronousQueue
是一种特殊的BlockingQueue,它本身没有容量。先调用put(),线程阻塞;直到另外一个线程调用了take(),两个线程才同时解锁。
比如调用三次put,三个线程都会阻塞,直到另外的线程调用3次take(),6个线程才能同时解锁。
源码不贴了,比较复杂,并且具有公平模式和非公平模式,有兴趣可以看114页
5.2 BlockingDeque
是一个阻塞的双端队列接口,接口源码如下:
//可以在头部或者尾部插入或者删除
public interface BlockingDeque<E> extends BlockingQueue<E>, Deque<E> {
void putFirst(E e) throws InterruptedException;
void putLast(E e) throws InterruptedException;
E takeFirst() throws InterruptedException;
E takeLast() throws InterruptedException;
}
如上所示,BlockingDeque就是继承了BlockingQueue并且实现了双端队列。该接口只有一个实现,就是LinkedBlockingDeque。
核心源码如下所示:
public class LinkedBlockingDeque<E>
extends AbstractQueue<E>
implements BlockingDeque<E>, java.io.Serializable {
static final class Node<E> {
E item;
Node<E> prev;//双向链表
Node<E> next;
Node(E x) {
item = x;
}
}
transient Node<E> first;//队列的头和尾
transient Node<E> last;
final ReentrantLock lock = new ReentrantLock();//一把锁+两个条件
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
}
对应的实现和LinkedBlockingQueue基本一样,LinkedBlockingQueue是单向链表,LinkedBlockingDeque是双向链表。
源码不贴了,见122页
5.3 CopyOnWrite
原理:写数据之前将数据拷贝一份进行修改,再通过悲观锁或者乐观锁的方式写回。
5.3.1 CopyOnWriteArrayList
和ArrayList一样,CopyOnWriteArrayList的核心数据结构也是一个数组。代码如下:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
}
下面是CopyOnWriteArrayList的几个“读”函数:
public class CopyOnWriteArrayList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
final Object[] getArray() {
return array;
}
public boolean isEmpty() {
return size() == 0;
}
private static int indexOf(Object o, Object[] elements,
int index, int fence) {
if (o == null) {
for (int i = index; i < fence; i++)
if (elements[i] == null)
return i;
} else {
for (int i = index; i < fence; i++)
if (o.equals(elements[i]))
return i;
}
return -1;
}
public boolean contains(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length) >= 0;
}
}
可以看出“读”函数都没有加锁,保证线程安全是在“写”函数中进行的
public boolean add(E e) {
synchronized (lock) {//加悲观锁
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);//先拷贝数组
newElements[len] = e;//进行add操作
setArray(newElements);//把新数组赋值给老数组
return true;
}
}
//其他写函数set remove和add类似
5.3.2 CopyOnwriteArraySet
CopyOnwriteArraySet就是Array实现的一个Set,保证所有元素都不重复,其内部封装的是一个CopyOnWriteArrayList。
public class CopyOnWriteArraySet<E> extends AbstractSet<E>
implements java.io.Serializable {
private final CopyOnWriteArrayList<E> al;//封装的CopyOnWriteArrayList
public CopyOnWriteArraySet() {
al = new CopyOnWriteArrayList<E>();
}
public boolean add(E e) {
return al.addIfAbsent(e);//不重复加进去
}
}
5.4 ConcurrentLikedQueue/Deque
与AQS内部的阻塞队列类似:同样是基于CAS,同样是通过head/tail指针记录队列头部和尾部,但是指针移动方式不同。在AQS阻塞队列中每次入队,tail一定后移一个位置,每次出队,head一定前移一个位置,以保证head指向队列头部,tail指向队列尾部。
但是ConcurrentLinkedQueue中,head/tail的更新可能落后于节点的入队和出队,因为它不是直接对head/tail指针做CAS操作,而是对Node中的item进行操作的。
下面进行详细分析:
见126页
5.5 ConcurrentHashMap
HashMap在jdk1.7的实现方式是“数组+链表”,在jdk1.8中的实现方式是“数组+链表+二叉树”,这种方式被称为“拉链法”。ConcurrentHashMap在这基础上做了优化,在JDK 7 和JDK 8中实现方式有很大差异,下面分开讨论。
5.5.1 JDK 7中的实现方式
为了提高并发度,在JDK7中将一个HashMap被拆分成多个子的HashMap。每一个HashMap被称为一个Segment,多线程操作多个Segment相互独立。如下图所示:
具体来说,每个Segment都继承自ReentrantLock,Segment的数量等于锁的数量,这些锁彼此之间相互独立,即所谓的“分段锁”。代码如下:
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V>, Serializable {
static final class Segment<K,V> extends ReentrantLock implements Serializable {//分段锁
...
}
/**
* The segments, each of which is a specialized hash table.
*/
final Segment<K,V>[] segments;
}
接下来由构造函数的分析入手,剖析ConcurrentHashMap的实现原理。
1.构造函数分析
//第一个参数是ConcurrentHashMap初始大小
//第二个参数是负载因子
//第三个参数是并发度,就是Segment数量
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) { //保证并发度是2的整数次方
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;//对应后面的参数,方便计算hash使用
this.segmentMask = ssize - 1;//对应后面的参数,方便计算hash使用
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;//每个Segment的初始大小
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)//保证每个Segment的容量,也是2的整数次方
cap <<= 1;
// create segments and segments[0]
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);//构造第0个Segment
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];//Segment的数组大小是ssize,也就是2的整数次方
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
2.put(...)函数分析
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);//把key映射到一个32位整数
int j = (hash >>> segmentShift) & segmentMask;//再把该整数映射到第j个Segment
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);//对第j个Segment进行初始化
return s.put(key, hash, value, false);//找到对应的Segment[j],调用其put
}
可以看出上段put代码没有加锁操作,因为锁是加在s.put内部的。另外多个线程可能同时调用ensureSegment对Segment[j]进行初始化,在这个函数的内部要避免重复初始化,下面详细分析:
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K,V> proto = ss[0]; // use segment 0 as prototype
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // recheck
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))//CAS操作保证初始化并发正常
break;
}
}
}
return seg;
}
segments[j]被成功地初始化了,下面进入内部,了解如何把元素放进去。
static final class Segment<K,V> extends ReentrantLock implements Serializable {
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
//当执行到这个地方时一定拿到锁了
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;//因为tab.length为2的整数次方,这个地方等价于hash对tab.length取模
HashEntry<K,V> first = entryAt(tab, index);//定位到第index个HashEntry
for (HashEntry<K,V> e = first;;) {
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;//修改次数增加
}
break;//key相等或者hash值相等,不会重复插入,直接返回
}
e = e.next;//遍历链表
}
else {//已经遍历到链表尾部,没有发现重复元素
if (node != null)//在上面的scanAndLockForPut里面,已经建立了节点
node.setNext(first);//把node插入链表头部
else
node = new HashEntry<K,V>(hash, key, value, first);//新建node插入链表头部
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);//超出阈值,扩容
else
setEntryAt(tab, index, node);//把node赋值给tab[index]
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
}
如果tryLock()成功则拿到锁进入上面的代码,如果tryLock()不成功,则进入scanAndLockForPut(key,hash,value),如下:
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node 创建一个新节点
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) { //自旋
lock(); //阻塞
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
上面的函数做的事是一拿不到锁,先自旋,然后自旋到一定次数仍拿不到锁,则阻塞;二是在自旋过程中遍历链表,若发现没有重复节点,则新建节点,为后面在插入节省时间。
3.扩容
超过一定阈值后Segment内部会进行扩容,如下:
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1; //在旧容量的基础上扩容二倍
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;//如果一个节点之前在第i个位置,
那么新的hash表中,一定处于i或者i+oldCapacity位置
if (next == null) // Single node on list
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;//寻找链表中最后一个hash值不等于lastIdx的元素
lastRun = last;
}
}
newTable[lastIdx] = lastRun;//关键的一行:把在lastRun之后的链表
元素直接链接到新hash表中的lastIdx位置,在lastRun之前的所有链表元素,需要在新的位置逐个拷贝
// Clone remaining nodes
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // 把新节点加入新的hash表
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
关于上面扩容函数代码,有几点需要说明:
①.函数的参数,也就是将要加入的最新节点。在扩容完成之后,把该节点加入新的Hash表。
②.整个数组的长度是2的整数次方,每次按二倍扩容,而hash函数就是对数组长度取模,即node.hash & sizeMake。因此,如果元素之前处于第i个位置,当再次hash时,必然处于第i个或者第i+oldCapacity位置。
③.上面的扩容进行了一次优化,并没有对元素依次拷贝,而是先找到lastRun位置。lastRun到链表末尾的所有元素,其hash值没有变化,所以不需要依次重新拷贝,只需要把这部分链表链接到新链表所对应的位置即可,也就是newTable[index]=lastRun。lastRun之前的元素需要依次拷贝。因此即使把for循环去掉,整个程序的逻辑仍然是正确的。
4.get实现分析
//整个过程完全没有锁,通过UNSAFE.getObjectVolatile函数
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;第一次hash
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);//第二次hash
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
5.5.2 JDK 8中的实现方式
jdk 8有两大变换:①.没有分段锁,②.引入了红黑树,其原理如下图:
如果头节点是Node类型,则尾随它的就是链表,如果头节点是TreeNode类型,则它后面是红黑树。链表和红黑树之间可以相互转换,初始时是链表,当链表中的元素超过某个阈值则把链表转换成红黑树,反之,当红黑树中的元素个数小于某个阈值,则再转换成链表。
JDK 8与JDK 7对比,JDK 7分段锁的三个好处:
①.减少Hash冲突,避免一个槽中太多元素。
②.提交读与写的并发度。段与段之间相互独立。
③.提供扩容的并发度。扩容的时候,不是ConcurrentHashMap一起扩容,而是每个Segment独立扩容。
针对这三个好处,JDK 8中的处理方式:
①.使用红黑树,当一个槽里面很多元素时,其查询和更新速度会比链表快很多,Hash冲突的问题由此得到更好的解决。
②.加锁的粒度,并非整个ConcurrentHashMap,而是对每个头节点分别加锁,即并发度就是Node数组的长度,初始长度为16,和JDK 7中初始的Segment的个数相同。
③.并发扩容。在JDK 7中一旦Segment个数初始化时候确立,不能在更改,并发度固定。之后只是在每个Segment内部独立扩容,互不影响,不存在并发扩容问题。但是在JDK 8中,当一个线程要扩容Node数组的时候,其他线程还要读写,因此处理过程很复杂。
总结:JDK 8一方面降低了hash冲突,一方面提升了并发度。
1.下面从构造方法一步步分析:
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));//cap是Node数组长度,保持为2的整数次方
this.sizeCtl = cap;//sizeCtl用于控制初始化或者并发扩容时候的线程数,只不过其初始值设置为cap
}
2.初始化:解决多线程初始化可能存在重复初始化问题
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
Thread.yield(); // 自旋等待
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {//关键:把sizeCtl设为-1
try {
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];//初始化
table = tab = nt;
sc = n - (n >>> 2);//sizeCtl并非表示数组长度,所以初始化成功后,就不再等于数组长度,
而是n-(n>>>2)=n-n/4=0.75n,表示下一次扩容的阈值
}
} finally {
sizeCtl = sc;//把sizeCtl设回去
}
break;
}
}
return tab;
}
多个线程竞争通过sizeCtl进行CAS操作实现的。如果某个线程成功把sizeCtl设置为-1,它就拥有初始化权利,进入初始化代码块,等初始化完成,再把sizeCtl设置回去,其他线程则一直执行while循环,自旋等待,直到数组不为null,即初始化结束,退出整个函数。
因为初始化工作量很小,所以此处选择的策略是让其他线程一直等待,而没有帮助其初始化。
3.put(...)实现分析
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();//分支1 整个数组初始化
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {//分支2 第i个元素初始化
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)
tab = helpTransfer(tab, f);//分支3 扩容
else {//分支4 放入元素
V oldVal = null;
synchronized (f) {//关键一句:加锁
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) {//如果是链表,binCount累加
if (binCount >= TREEIFY_THRESHOLD)//TREEIFY_THRESHOLD初始是八
treeifyBin(tab, i);//超过阈值,转换成红黑树
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);//总元素个数累加1
return null;
}
上面for循环4个分支:
第一个分支,是整个数组初始化,前面讲过了;
第二个分支,是所在的槽是空的,说明该元素是该槽的第一个元素,直接新建一个头节点,然后返回;
第三个分支,说明该槽正在进行扩容,帮助其扩容;
第四个分支,就是把元素放入槽内。
4.扩容
详细见143页
5.6 ConcurrentSkipListMap/Set
ConcurrentSkipListMap/Set是key有序的hashMap,实现了NavigableMap接口,此接口又继承SortedMap接口。
原理:基于SkipList(跳查表)来实现的