目录
1. CopyOnWriteArrayList
2. BlockingQueue
3. 并发容器总结
1. CopyOnWriteArrayList
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实现原理
- CopyOnWrite的含义
只有容器中内容被修改的时候,就会copy出一个容器,然后在这个新的容器里改,之后将原容器的引用指向这个新的容器;好处就是对这个容器进行并发读写操作的时候,不需要额外加锁 - 创建新副本、读写分离
- "不可变"原理
- 迭代的时候
如果数组原内容被修改过了,但是迭代器是不知道的,迭代器依然使用的是旧数组,而且也不会报错
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.3 CopyOnWriteArrayList的缺点
- 数据一致性问题: CopyOnWrite容器只能保证数据的最终一致性,不能保证数据的实时一致性。所以如果希望写入的数据,马上能读到,请不要使用CopyOnWrite容器
- 内存占用问题: 因为CopyOnWrite的写是复制机制,所以在进行写操作的时候,内存里会同时驻扎两个对象的内存
1.4 CopyOnWriteArrayList源码分析
-
get
CopyOnWriteArrayList.javaprivate 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. BlockingQueue
2.1 为什么要使用队列
- 用队列可以在线程间传递数据:生产者消费者模式、银行转账
- 考虑锁等线程安全问题的重任从"你"转移到了"队列"上
2.2 并发队列简介
-
Queue
用来保存一组等待处理的数据,有很多种实现,比如ConcurrentLinkedQueue(非阻塞队列)、LinkedBlockingQueue(阻塞队列)等
-
BlockingQueue
扩展了Queue,增加了可阻塞的插入和获取操作,如果队列为空,获取的操作会一直阻塞,直到里面有数据,如果队列为满,插入的操作会一直阻塞,直到队列有可用空间
2.3 各并发队列关系图
2.4 阻塞队列
2.4.1 什么是阻塞队列
- 阻塞队列是具有阻塞功能的队列,所以它首先是一个队列,其次是具有阻塞功能
- 通常,阻塞队列的一端是给生产者放数据用,另一端给消费者拿数据用。阻塞队列是线程安全的,所以生产者和消费者都可以是多线程的
- 阻塞功能:最有特点的两个带有阻塞功能的方法是:
- take()方法:获取并移除队列的头节点,一旦如果执行take的时候,队列里无数据,则阻塞,直到队列里有数据
- put()方法:插入数据,但是如果队列已满,那么就无法继续插入,则阻塞,直到队列里有了空闲空间
2.4.2 BlockingQueue主要方法
- put,take: 这两个方法都会阻塞
- add,remove,element: 为空为满进行操作会抛出异常
- offer,poll,peek: 返回布尔或者null值
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(); } }
- 若队列已满,调用 notFull 的
#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(); } }
- 若队列已满,调用 notFull 的
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
- 支持优先级
- 自然顺序(而不是先进先出)
- 无界队列
- PriorityQueue的线程安全版本
2.4.6 SynchronousQueue
- 它的容量为0
- 注意:容量不是1而是0,因为SynchronousQueue不需要去持有元素,它所做的就是直接传递(direct handoff)
注意点
- SynchronousQueue没有peek函数,因为peek的含义是取出头结点,但是SynchronousQueue的容量是0,所以连头节点都没有,也就是没有peek方法,同理,没有iterate相关方法
- 是一个极好的用来直接传递的并发数据结构
- SynchronousQueue是线程池**Excutors.newCachedThreadPool()**使用的阻塞队列
2.4.7 DelayQueue
- 无边界
- 延迟队列,根据延迟时间顺序
- 元素需要实现Delayed接口,规定排序规则
2.5 非阻塞队列
- JUC中的非阻塞队列只有ConcurrentLinkedQueue这一种,是使用链表作为其数据结构的,使用CAS非阻塞算法来实现线程安全(不具备阻塞功能),适合用在对性能要求较高的并发场景。用的相对比较少一些
- 看源码的offer方法的CAS思想,内有p.casNext方法,用了UNSAFE.compareAndSwapObject
2.6 如何选择适合自己的队列
- 边界
- 空间
- 吞吐量
3. 并发容器总结
- java.util.concurrent包提供的容器,分为三类: Concurrent*、CopyOnWrite*、Blocking*
- Concurrent*的特点是大部分通过CAS实现并发
- CopyOnWrite * 则是通过复制一份原数据来实现的
- BlockingQueue通过AQS实现的
- 参考: 慕课网悟空老师课程