先看Collections结构
面试常问问题:
1、ArrayBlockQueue和LinkedBlockingQueue有什么区别
答:二者都是通过reentrantLock进行加锁的,但是区别在于ArrayBlockQueue是读写不分离的,也就是说要么进行读操作,要么进行写操作,而且因为用的是ReentrantLock,所以一个线程是可以重复写或者读的;LinkedBlockingQueue则是读写分离,可以同时读或者写,但是因为用的是ReentrantLock,所以每次只能有一个线程读和一个写。这个需要专门注意下读和写的并发操作。
2、PriorityBlockingQueue是无界队列,基于数组,数据结构为二叉堆,数组的第一个节点也是树的根节点总是最小值
一、BlockingQueue
1、BlockingQueue是双缓冲队列。BlockingQueue内部使用两条队列,允许两个线程同时向队列一个存储,一个取出操作。在保证并发安全的同时,提高了队列的存取效率。
2、常用的几种BlockingQueue:
(1)ArrayBlockingQueue(int i):规定大小的BlockingQueue,其构造必须指定大小。其所含的对象是FIFO顺序排序的。
(2)LinkedBlockingQueue()或者(int i):大小不固定的BlockingQueue,若其构造时指定大小,生成的BlockingQueue有大小限制,不指定大小,其大小由Integer.MAX_VALUE来决定。其所含的对象是FIFO顺序排序的。
(3)PriorityBlockingQueue()或者(int i):类似于LinkedBlockingQueue,但是其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定。
(4)SynchronizedQueue():特殊的BlockingQueue,对其的操作必须是放和取交替完成。
3、常用方法
4、应用
(1)生产者消费者模式:有了阻塞队列,我们实现生产者消费者模式只需要调用接口就行了,不用自己实现线程的阻塞和唤醒,也就是说阻塞队列接口的实现方式就是生产者消费者模式的实现。
(2)线程池:线程池的底层也用到了阻塞队列,其实它的本质也是生产者消费者模式,只不过这里的资源是线程。
(3)消息中间件:RabbitMQ等消息队列的底层数据结构就是用到了阻塞队列,因为它的模型也是生产者消费者模式。
二、CopyOnWriteArrayList(线程安全,解决iterator遍历的java.util.ConcurrentModificationException)
1、原理:写操作并不是直接修改原本的array,而是复制一份进行修改,同时也加了锁,在多线程的情况下不会复制出多个副本,其读操作没有任何锁,直接返回原本的array。
2、CopyOnWriteArrayList,对它的操作可以做到写写互斥、其他三个操作不互斥。
3、复制的原因是为了读写分离,为了在写的时候操作不阻塞也能不出现问题
未读写分离时,如果不阻塞读操作,由于读和写操作都不是原子操作,它们可能会交替执行,如:
当一个线程执行到len = len + 1 的时候,另外一个线程来进行读操作了,比如执行getLast()方法,即执行get(len - 1),但是现在这个位置是没有赋值上的,这就出现了问题。但是如果修改时是赋值一份来修改,另外一个线程读的时候就没有任何影响。这也就是为什么在没有这个容器之前,只能坐到读读不加锁,读写、写读、写写都要加锁。有了这个容器,就可以只在写写的时候加锁了。
4、写操作的源码(通过ReentrantLock加锁实现)
三、ConcurrentMap
1、ConcurrentHashMap
a、JDK1.7之前ConcurrentHashMap采用锁分段机制
如图所示,ConcurrentHashMap默认分成了16个segment,每个segment都对应一个Hash表,且都由独立的锁。所以这样就每个线程访问一个Segment,就可以并行访问了,从而提高了效率,这就是锁分段。
JDK1.8之后采用的是和HashMap一样的结构:数组+链表/红黑树
问:JDK1.8ConcurrentHashMap怎样保证线程安全?
使用Synchronized + CAS 的方法来实现线程安全。在用hash定位到数组的头结点时,如果没有发生冲突就用CAS来实现写操作的线程安全,如果发生了冲突,则锁住这个头节点。
java.util.concurrent包还提供了设计用于多线程上下文中的 Collection 实现: ConcurrentHashMap、ConcurrentSkipListMap、ConcurrentSkipListSet、CopyOnWriteArrayList 和 CopyOnWriteArraySet。当期望许多线程访问一个给 定 collection 时,ConcurrentHashMap 通常优于同步的 HashMap,ConcurrentSkipListMap 通常优于同步的 TreeMap。当期望的读数和遍历远远 大于列表的更新数时,CopyOnWriteArrayList 优于同步的 ArrayList。下面看看部分用法:
10个线程并发访问这个集合,读取集合数据的同时再往集合中添加数据,运行这段代码会报错,并发修改异常
(1)出现并发修改异常的原因及解决办法
原因:当线程1添加了元素时,modCount+1,并会修改其他线程的expectedModCount+1,而此时线程1中的expectedModCount值为0,虽然modCount不是volatile变量,不保证线程1一定看得到线程2修改后的modCount的值,但是也有可能看得到线程2对modCount的修改,这样就有可能导致线程1中比较expectedModCount和modCount不等,而抛出异常。
解决:
1、将集合创建方式改成:
private static CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
这样就不会有并发异常了,因为这个是写入并复制,每次生成新的,所以如果添加操作比较多的话,开销非常大,适合迭代操作比较多的时候使用。
2、在使用iterator迭代的时候使用synchronized或Lock进行同步