public class LinkedBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
// Node为LinkedBlockingQueue内部的节点
static class Node<E> {
E item;
/**
* 下列三种情况之一
* - 真正的后继节点
* - 自己, 发生在出队时
* - null, 表示是没有后继节点, 是最后了
*/
Node<E> next;
Node(E x) {
item = x; }
}
}
初始化链表 last = head = new Node(null);
Dummy
节点用来占位,item
为 null
当一个节点入队 last = last.next = node
再来一个节点入队 last = last.next = node;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
h = head
first = h.next
h.next = h
head = first
E x = first.item;
first.item = null;
return x;
高明之处在于用了两把锁和 dummy 节点
dummy
节点),putLock
保证的是 last
节点的线程安全,takeLock
保证的是head 节点的线程安全。两把锁保证了入队和出队没有竞争dummy
节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会竞争dummy
节点)这时 take
线程会被 notEmpty
条件阻塞,有竞争,会阻塞// 用于 put(阻塞) offer(非阻塞)
private final ReentrantLock putLock = new ReentrantLock();
// 用户 take(阻塞) poll(非阻塞)
private final ReentrantLock takeLock = new ReentrantLock();
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;
// count 用来维护元素计数
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 满了等待
while (count.get() == capacity) {
// 倒过来读就好: await notFull
notFull.await();
}
// 有空位, 入队且计数加一
enqueue(node);
// 返回的是c加1之前的数值
c = count.getAndIncrement();
// 除了自己 put 以外, 如果队列还有空位, 由自己叫醒其他 put 线程
// 这里与其它地方不同的是,put线程的唤醒是由其它put线程唤醒的;
if (c + 1 < capacity)
notFull.signal();
} finally {
putLock.unlock();
}
// 如果队列中只有一个元素, 叫醒 take 线程
if (c == 0)
// 这里调用的是 notEmpty.signal() 而不是 notEmpty.signalAll() 是为了减少竞争
signalNotEmpty();
}
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)
// 唤醒其它take线程
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 如果队列中只有一个空位时, 叫醒 put 线程
// 如果有多个线程进行出队, 第一个线程满足 c == capacity, 但后续线程 c < capacity
if (c == capacity)
// 这里调用的是 notFull.signal() 而不是 notFull.signalAll() 是为了减少竞争
signalNotFull();
return x;
}
主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较
ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是
事实上,ConcurrentLinkedQueue 应用还是非常广泛的
例如之前讲的 Tomcat 的 Connector 结构时,Acceptor 作为生产者向 Poller 消费者传递事件信息时,正是采用了ConcurrentLinkedQueue 将 SocketChannel 给 Poller 使用
CopyOnWriteArraySet 是它的马甲
底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,更改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。 以新增为例
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;
}
}
这里的源码版本是 Java 11,在 Java 1.8 中使用的是可重入锁而不是 synchronized
其它读操作并未加锁,例如:
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
for (Object x : getArray()) {
@SuppressWarnings("unchecked") E e = (E) x;
action.accept(e);
}
}
适合『读多写少』的应用场景
时间点 | 操作 |
---|---|
1 | Thread-0 getArray() |
2 | Thread-1 getArray() 先得到数组再进行拷贝 |
3 | Thread-1 setArray(arrayCopy) 修改完数组再将数组设置回去 |
4 | Thread-0 array[index] 读到的还是旧数组的信息,就是说array[0]虽然被Thread-1删除了,但是还是能Thread-0可以读取到 |
不容易测试,但问题确实存在
CopyOnWriteArrayList<Integer> list = new CopyOnWriteArrayList<>();
list.add(1);
list.add(2);
list.add(3);
Iterator<Integer> iter = list.iterator();
new Thread(() -> {
list.remove(0);
System.out.println(list);
}).start();
sleep1s();
while (iter.hasNext()) {
System.out.println(iter.next());
}
不要觉得弱一致性就不好
- 数据库的 MVCC 都是弱一致性的表现
- 并发高和一致性是矛盾的,需要权衡