1. CopyOnWriteArrayList
2. BlockingQueue
3. 并发容器总结
1.1 诞生历史和原因
Vector和SynchronizedList的锁的粒度太大,并发效率相对比较低,并且迭代时无法编辑
问题:为什么在迭代时无法编辑?(先看代码演示)
Vector<String> vector = new Vector<>();
vector.add("1");
vector.add("2");
vector.add("3");
Iterator<String> iterator1 = vector.iterator();
while(iterator1.hasNext()) {
String next = iterator1.next();
System.out.println(next);
vector.add("2");
}
List<String> list = Collections.synchronizedList(new ArrayList<String>());
list.add("1");
list.add("2");
list.add("3");
list.add("4");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
String next = iterator.next();
System.out.println(next);
// 对list进行编辑,结果会报错
list.add("2");
}
由错误可知,两者均在iterator.next()时候发生了错误,为什么呢?怎么迭代时候会发生错误?因为在每次迭代后,我都对vector或者list进行了添加操作,从而引起了下一次打印输出错误~,我们跟踪一下iterator.next()源码
1. String next = iterator.next();
2. public E next() {
checkForComodification(); // 每次进行迭代的时候都会进行检查是否有被修改
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
3. final void checkForComodification() {
if (modCount != expectedModCount) // 一旦发现被修改,就抛出异常
throw new ConcurrentModificationException();
}
从源码知道,modCount 在每次增加、修改等操作的时候会进行+1操作,而expectedModCount是ArrayList一旦创建就有了的,一开始值为modCount, 所以我们在进行迭代的时候,如果发现集合被修改,modCount就+1,此时两者不相等,就抛出异常了,那有没有什么方法解决呢?就是接下来要讲的CopyOnWriteArrayList
1.2 CopyOnWriteArrayList实现原理
CopyOnWriteArrayList list = new CopyOnWriteArrayList();
list.add("1");
list.add("2");
list.add("3");
list.add("4");
list.add("5");
Iterator<String> iterator = list.iterator();
while(iterator.hasNext()) {
System.out.println("list is" + list);
String next = iterator.next();
System.out.println(next);
if(next.equals("2")) {
list.remove("5");
}
if(next.equals("3")) {
list.add("3 found");
}
}
1.4 CopyOnWriteArrayList源码分析
get
CopyOnWriteArrayList.java
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
由源码可知get操作并没有上锁
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();
}
}
由add源码可知,会使用独占锁进行锁定(防止多线程同时add时候数组被覆盖),以及每次添加都会复制一个新的数组,将新元素添加到尾部
1.5 CopyOnWriteArrayList适合场景及读写规则
适合场景:
读写规则:
2.1 为什么要使用队列
2.2 并发队列简介
Queue
用来保存一组等待处理的数据,有很多种实现,比如ConcurrentLinkedQueue(非阻塞队列)、LinkedBlockingQueue(阻塞队列)等
BlockingQueue
扩展了Queue,增加了可阻塞的插入和获取操作,如果队列为空,获取的操作会一直阻塞,直到里面有数据,如果队列为满,插入的操作会一直阻塞,直到队列有可用空间
2.4.1 什么是阻塞队列
2.4.2 BlockingQueue主要方法
2.4.3 ArrayBlockingQueue
有界
指定容量
公平:如果想保证公平的话,那么等待了最长时间的线程会被优先处理,不过这会同时带来一定的性能消耗
使用案例: 有10个面试者,一共只有1个面试官,大厅只有3个位置供面试者休息,每个人的面试时间为1秒,模拟所有人面试的场景
public class ArrayBlockingQueueDemo {
public static void main(String[] args) {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(3);
Interviewer r1 = new Interviewer(queue);
Consumer r2 = new Consumer(queue);
new Thread(r1).start();
new Thread(r2).start();
}
}
class Interviewer implements Runnable{
BlockingQueue<String> queue;
public Interviewer(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
System.out.println("10个候选人都来了");
for (int i = 0; i < 10; i++) {
String candidate = "Candidate" + i;
try {
queue.put(candidate);
System.out.println("安排好了" + candidate);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
try {
queue.put("stop");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable{
BlockingQueue<String> queue;
public Consumer(BlockingQueue queue) {
this.queue = queue;
}
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
String msg;
try {
while(!(msg = queue.take()).equals("stop")) {
System.out.println(msg + "到了");
}
System.out.println("所有候选人都结束了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
对put和take源码分析
put源码
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
// 获得锁
lock.lockInterruptibly();
try {
// 若队列已满,循环等待被通知,再次检查队列是否未满
while (count == items.length)
// notFull表示未满,进行入队操作,由于已经满了,所以调用await方法,使线程阻塞
notFull.await();
// 入队,此操作会唤醒出队操作,即 notEmpty.signal()
enqueue(e);
} finally {
// 解锁
lock.unlock();
}
}
#await()
方法,等待被通知。take源码
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
// 获得锁
lock.lockInterruptibly();
try {
// 若队列已空,循环等待被通知,再次检查队列是否非空
while (count == 0)
// notEmpty表示非空,执行出队操作,由于此次为空,所以让出队操作阻塞,调用await方法,等待被唤醒
notEmpty.await();
// 出列,此操作会唤醒入列操作,即notFull.signal();
return dequeue();
} finally {
// 解锁
lock.unlock();
}
}
2.4.4 LinkedBlockingQueue
无界
容量Integer.MAX_VALUE
内部结构: Node、两把锁。分析put方法
/** 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();
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
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();
}
从上面的属性我们知道,每个添加到LinkedBlockingQueue队列中的数据都将被封装成Node节点,添加的链表队列中,其中head和last分别指向队列的头结点和尾结点。与ArrayBlockingQueue不同的是,LinkedBlockingQueue内部分别使用了takeLock 和 putLock 对并发进行控制,也就是说,添加和删除操作并不是互斥操作,可以同时进行,这样也就可以大大提高吞吐量。
如果不指定队列的容量大小,也就是使用默认的Integer.MAX_VALUE,如果存在添加速度大于删除速度时候,有可能会内存溢出,这点在使用前希望慎重考虑。
另外,LinkedBlockingQueue对每一个lock锁都提供了一个Condition用来挂起和唤醒其他线程。
2.4.5 PriorityBlockingQueue
2.4.6 SynchronousQueue
注意点
2.4.7 DelayQueue
2.5 非阻塞队列
2.6 如何选择适合自己的队列