ArrayBlockingQueue
BlockingQueue的有界队列实现,主要字段如下:
/** 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;
/*
* Concurrency control uses the classic two-condition algorithm
* found in any textbook.
*/
/** Main lock guarding all access */
final ReentrantLock lock;
/** Condition for waiting takes */
private final Condition notEmpty;
/** Condition for waiting puts */
private final Condition notFull;
可以看到内部持有一个数组,有一个可重入锁和两个condition,通过这两个codition可以实现生产者消费者模式,重点看一下put和take方法;
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
可以看到当队列满时,会一直阻塞等待notFull条件满足,然后进行入队操作
private void enqueue(E x) {
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
入队后会调用notEmpty.singal()唤醒take()的线程,从而实现生产者消费者模式。
LinkedBlockingQueue
与ArrayBolckingQueue不同的是,LinkedBlockingQueue持有两把锁,入队和出队的锁是分开的,这样的好处是,入队和出队操作可以并发的执行,提升并发效率。
private final int capacity;
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();
/**
* Head of linked list.
* Invariant: head.item == null
*/
transient Node<E> head;
/**
* Tail of linked list.
* Invariant: last.next == null
*/
private transient Node<E> last;
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
可以注意到的是,LinkedBlockingQueue的锁都是非公平锁,而ArrayBlockingQueue可以指定为公平锁。
ConcurrentLinkedQueue
与LinkedBlockingQueue区别的是ConcurrentLinkedQueue属于并发容器,而不是阻塞容器,所以里面并没有lock的身影,那么ConcurrentLinkedQueue是通过什么保证线程安全的呢?答案依然是CAS+volital
volatile E item;
volatile Node<E> next;
/**
* Constructs a new node. Uses relaxed write because item can
* only be seen after publication via casNext.
*/
Node(E item) {
UNSAFE.putObject(this, itemOffset, item);
}
boolean casItem(E cmp, E val) {
return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val);
}
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
boolean casNext(Node<E> cmp, Node<E> val) {
return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val);
}
重点看一下offer方法,与LinkedBlockingQueue的对比
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
for (Node<E> t = tail, p = t;;) {
Node<E> q = p.next;
if (q == null) {
// p is last node
if (p.casNext(null, newNode)) {
// Successful CAS is the linearization point
// for e to become an element of this queue,
// and for newNode to become "live".
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
比LinkedBlockingQueue要复杂的多,重点在于所有的写操作都是用CAS+自旋完成,所以才能保证线程安全,过程主要分为两步:
1.定位tail节点(在ConcurrentLinkedQueue里tail节点不一定是尾结点【延迟更新,主要是为了通过增加read次数减少writeTail的此时来提高并发效率】);
2.将新增节点设置为tail节点的next节点
CoucurrentHashMap
java7中,由分段锁实现并发控制,put时步骤:
1.由key定位到具体的segment(继承于ReentrantLock),
2.每个segment中持有HashEntry<K,V>[],然后通过对key做hash后定位到具体的bucket,也就是确定在数组的哪个位置
3.如果此时发生hash冲突,则比较key值,key不一样则形成链表,加在tail.next
java8中,put流程:
1.首先对于每一个放入的值,首先利用spread方法对key的hashcode进行一次hash计算,由此来确定这个值在 table中的位置;
2.如果当前table数组还未初始化,先将table数组进行初始化操作;
3.如果这个位置是null的,那么使用CAS操作直接放入;
4.如果这个位置存在结点,说明发生了hash碰撞,首先判断这个节点的类型。如果该节点fh==MOVED(代表forwardingNode,数组正在进行扩容)的话,说明正在进行扩容;
5.如果是链表节点(fh>0),则得到的结点就是hash值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到key相同的节点,则只需要覆盖该结点的value值即可。否则依次向后遍历,直到链表尾插入这个结点;
6.如果这个节点的类型是TreeBin的话,直接调用红黑树的插入方法进行插入新的节点;
7.插入完节点之后再次检查链表长度,如果长度大于8,就把这个链表转换成红黑树;
8.对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容
CopyOnWriteArrayList
add方法流程:
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();
}
}
1、采用ReentrantLock,保证同一时刻只有一个写线程正在进行数组的复制,否则的话内存中会有多份被复制的数据;
2、数组引用是volatile修饰的,因此将旧的数组引用指向新的数组,根据volatile的happens-before规则,写线程对数组引用的修改对读线程是可见的。
3、由于在写数据的时候,是在新的数组中插入数据的,从而保证读写实在两个不同的数据容器中进行操作。