文章目录
并发容器
阻塞队列
BlockingQueue是JUC包下的重要数据结构,区别于普通队列,BlockingQueue提供了线程安全的队列访问方式,并发包下很多高级同步类都是基于BlockingQueue实现的
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;
int remainingCapacity();
boolean remove(Object o);
public boolean contains(Object o);
int drainTo(Collection<? super E> c);
int drainTo(Collection<? super E> c, int maxElements);
}
add,offer,put,和offer(e,time,unit)的区别:
- add:当无法插入时会抛出异常
- offer:当无法插入时会立马返回false
- put:当无法插入时会阻塞等到,直到插入成功
- offer(e,time,unit):当无法插入时会阻塞等待指定时间,如果到时了还是无法插入则返回一个false
注意;
- 不能往阻塞队列中插入null,会抛出空指针异常。
- 可以访问阻塞队列中的任意元素,调用remove(o)可以将队列之中的特定对象移除,但并不高效,尽量避免使用。
BlockingQueue实现类
ArrayBlockingQueue
由数组实现的有界阻塞队列
final Object[] items;
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
// 默认是非公平锁
lock = new ReentrantLock(fair);
// 非空等待队列
notEmpty = lock.newCondition();
// 非满等待队列
notFull = lock.newCondition();
}
入队:
public boolean offer(E e) {
checkNotNull(e);
// 通过reentranlock保证线程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
if (count == items.length)
return false;
else {
enqueue(e);
return true;
}
} finally {
lock.unlock();
}
}
private void enqueue(E x) {
final Object[] items = this.items;
// putIndex是当前队尾,如果加到数组末尾了,重新回到数组头部开始
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
// 唤醒消费者
notEmpty.signal();
}
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();
}
}
出队:
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return (count == 0) ? null : dequeue();
} finally {
lock.unlock();
}
}
private E dequeue() {
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
// takeIndex标识出队的下标
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
// 唤醒生产者
notFull.signal();
return x;
}
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 如果队列是空的,消费者进入等待队列
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
LinkedBlockingQueue
LinkedBlockingQueue是基于链表实现的阻塞队列,最大大小是Integer.MAX_VALUE
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
public LinkedBlockingQueue(int capacity) {
if (capacity <= 0) throw new IllegalArgumentException();
this.capacity = capacity;
last = head = new Node<E>(null);
}
LinkedBlockingQueue内部维护两把锁,出队锁和入队锁
private final ReentrantLock putLock = new ReentrantLock();
private final ReentrantLock takeLock = new ReentrantLock();
出队的时候可以入队,入队的时候可以出队
public boolean offer(E e) {
// 不能插入null元素
if (e == null) throw new NullPointerException();
// 当前队列元素数目
final AtomicInteger count = this.count;
// 满了则无法添加元素
if (count.get() == capacity)
return false;
// 记录添加的元素数量
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
public E poll() {
final AtomicInteger count = this.count;
if (count.get() == 0)
return null;
E x = null;
int c = -1;
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
if (count.get() > 0) {
x = dequeue();
c = count.getAndDecrement();
if (c > 1)
notEmpty.signal();
}
} finally {
takeLock.unlock();
}
if (c == capacity)
signalNotFull();
return x;
}
PriorityBlockingQueue
优先队列,内部基于堆实现
其实也是由数组实现的队列,默认容量是11
private static final int DEFAULT_INITIAL_CAPACITY = 11;
private transient Object[] queue;
public PriorityBlockingQueue() {
this(DEFAULT_INITIAL_CAPACITY, null);
}
添加元素:
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
// 不能插入null
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;
}
private static <T> void siftUpComparable(int k, T x, Object[] array) {
Comparable<? super T> key = (Comparable<? super T>) x;
while (k > 0) {
int parent = (k - 1) >>> 1;
Object e = array[parent];
if (key.compareTo((T) e) >= 0)
break;
// 当前元素比父节点小则上浮,子节点和父节点交换位置
array[k] = e;
k = parent;
}
array[k] = key;
}
删除元素:
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);
else
siftDownUsingComparator(0, x, array, n, cmp);
size = n;
return result;
}
}
// 如果比左右节点大则下浮,将较小值交换到父节点
private static <T> void siftDownComparable(int k, T x, Object[] array,
int n) {
if (n > 0) {
Comparable<? super T> key = (Comparable<? super T>)x;
int half = n >>> 1; // loop while a non-leaf
while (k < half) {
int child = (k << 1) + 1; // assume left child is least
Object c = array[child];
int right = child + 1;
if (right < n &&
((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
c = array[child = right];
if (key.compareTo((T) c) <= 0)
break;
array[k] = c;
k = child;
}
array[k] = key;
}
}
SynchronousQueue
是一个容量为空的队列,每一个put必须等待一个take
- iterator() 永远返回空,因为里面没有东西
- peek() 永远返回null
- put() 往queue放进去一个element以后就一直wait直到有其他thread进来把这个element取走。
- offer() 往queue里放一个element后立即返回,如果碰巧这个element被另一个thread取走了,offer方法返回true,认为offer成功;否则返回false。
- take() 取出并且remove掉queue里的element,取不到东西他会一直等。
- poll() 取出并且remove掉queue里的element,只有到碰巧另外一个线程正在往queue里offer数据或者put数据的时候,该方法才会取到东西。否则立即返回null。
- isEmpty() 永远返回true
- remove()&removeAll() 永远返回false
注意:使用阻塞队列生产者生产的速度应该慢于消费者消费的速度,生产者生产过快导致消费者消费不过来的话有可能导致内存耗尽导致OOM
copyOnWrite容器
CopyOnWrite容器即写时复制容器,当我们往一个容器中添加元素的时候,不直接往容器中添加,而是将当前容器进行复制,在复制出来的容器上进行修改,修改完成后将原容器的引用指向新容器,好处是读容器时无需加锁,写时加锁,实现读写分离
CopyOnWriteArrayList
CopyOnWriteArrayList经常被用于读多写少的场景,因为读写分离,大大增强了读的性能,在Java遍历线程非安全的集合list时,如果在遍历途中对集合进行修改,那么会抛出并发修改异常,在CopyOnWriteArrayList则不会,因为遍历的list是一份元容器的拷贝
缺点:
- CopyOnWriteArrayList每执行异常写操作都必须拷贝一份原容器,消耗内存,引起频繁GC,GC会导致stop the world引起性能上有损耗
- 读写分离,写操作进行时不会阻塞读操作,导致读操作可能读到脏数据
读不加锁,性能高
public E get(int index) {
return get(getArray(), index);
}
public int indexOf(Object o) {
Object[] elements = getArray();
return indexOf(o, elements, 0, elements.length);
}
写时复制,加锁,因为写操作是不会阻塞读操作的,所以如果我们希望写入的数据马上能准确地读取,请不要使用CopyOnWrite容器。
public E set(int index, E element) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 容器拷贝
Object[] elements = getArray();
E oldValue = get(elements, index);
// 修改元素
if (oldValue != element) {
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len);
newElements[index] = element;
// 原数组引用指向新数组
setArray(newElements);
} else {
// Not quite a no-op; ensures volatile write semantics
setArray(elements);
}
return oldValue;
} finally {
lock.unlock();
}
}
final void setArray(Object[] a) {
array = a;
}
并发Map
锁分段技术:hashtable是使用一把全局synchronized锁实现同步的,每次所有线程竞争的都是同一把锁,在concurrentHashmap中有多把锁,每一把锁分别锁住一部分数据,那么当多线程访问不同段的数据时,竞争的不是同一把锁,会大大提高并发效率
在jdk1.7中使用的还是分段锁,在jdk1.8采取cas和synchronized的方式
节点类:存储key和value,其中key和value使用volatile保证并发可见性
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
重要概念:
/**
* 默认是0、
* -1:table正在初始化
* -N:表示有N-1个线程正在执行扩容操作
* 如果table未初始化,则表示为table需要初始化的大小
* 如果table初始化完成,表示table的容量
*
*/
private transient volatile int sizeCtl;
// nextTable:默认为null,扩容时新生成的数组,其大小为原数组的两倍。
private transient volatile Node<K,V>[] nextTable;
初始化table,因为是延迟初始化,所以会等到第一次put时才会初始化表,put操作是可以多线程并发的,所以,table表只可以初始化一次,如何保证只可以初始化一次呢?
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 只有是空表才能进入到次循环,第一个限定条件
while ((tab = table) == null || tab.length == 0) {
if ((sc = sizeCtl) < 0)
// 如果发现有其他线程在执行初始化表或者扩容操作,则让出CPU,第二个限定条件
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// CAS将sizeCtl修改为正在初始化状态
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);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
添加元素
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 与hashmap不同点:key和value不能为null
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();
// 要添加的节点位置没有哈希冲突,直接CAS添加
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
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);
else {
// 没有在扩容则采用synchronized锁住当前头结点节点(分段锁思想,之前1.8之前是锁足segment数组元素)
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) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}