同步类容器的问题
- 同步类容器都是线程安全的,但是在某些场景下可能需要加锁来保护复合操作
- 复合类操作:迭代(反复访问元素,遍历容器中里面的所有的元素)、跳转(根据指定的顺序找到当前的元素的下一个元素)、以及条件运算
- 这些复合操作在多线程并发地修改容器时,可能会表现出意外的行为,最经典的便是ConcurrentModificationException,原因是当容器并发的修改了内容
同步类容器的使用
-
同步类容器
-
如Vector、HashTable
-
容器的同步功能其实都是JDK的Collections、synchronized等工厂的方法去实现的
-
其底层的机制无非就使用synchronized关键字对每个公用的方法进行同步,或者使用Object mutex对象锁的机制使得每次只能有一个线程访问容器的状态
-
List<String> list = new ArrayList<>(); Collections.synchronizedCollection(list);
并发类容器的概念
- jdk提供了多种并发类容器来替代同步类容器从而改善性能
- 同步类容器的状态是串行化的
- 他们虽然实现了线程安全,但是严重降低了并发性,在多线程环境时,严重降低了应用程序的吞吐量
ConcurrentMap
- ConcurrentMap接口有两个最重要的实现 ConcurrentHashMap和ConcurrentSkipListMap(支持并发排序的功能)
- ConcurrentSkipListMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的HashTable,每个段都有自己的锁
- 只要多个修改操作发生在不同的段上,他们就可以并发进行。把一个整体分成16个段,也就意味着最高支持16个线程的并发修改操作
- 这也是在多线程场景时减小锁的粒度从而降低锁竞争的一种方案。并且代码中大多共享变量使用volatile关键字声明,目的是第一时间获取修改内容,性能非常好
Copy-On-Write容器
- Copy-On-Write容器简称COW,是一种程序设计的优化策略
- JDK容器里面COW容器有两种,CopyOnWriteArrayList
- Copy-On-Write容器即写时复制的容器
- 通俗的理解就是当我们往一个容器添加元素的时候,不直接往当前的容器添加,而是对当前的容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器
- 这样的好处是我们可以对Copy-On-Write容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素
- Copy-On-Write容器依据读写分离的思想,读和写基于不同的容器
并发Queue
- 在并发队列上JDK的实现方式有两种(他们都继承于Queue接口)
- ConcurrentLinkedQueue为代表的高性能队列
- BlockingQueue为代表的阻塞队列
ConcurrentLinkedQueue
- ConcurrentLinkedQueue:是一个适用于高并发场景下的队列,通过无锁的方式,实现了高并发状态下的高性能,通常ConcurrentLinkedQueue一般性能好于BlockQueue。ConcurrentLinkedQueue是一个基于链接节点的无界线程安全队列
- ConcurrentLinkedQueue元素遵循先进先出的原则,头是最先加入的,尾是最后加入的,该队列不允许null元素
- add()和offer()都是加入元素的方法(这两个方法没有任何区别)
- poll()和peek()都是取头元素的节点,区别在于前者会删除元素,后者不会
BlockingQueue接口的重要方法
- offer(anObject):表示如果可能的话,将按Object加到BlockingQueue里,即如果BlockingQueue可以容纳,则返回True,否则返回False(本方法不阻塞当前执行方法的线程)
- offer(E o,long timeout,TimeUnit unit):可以设定等待的时间,如果在指定的时间内,还不能往队列中加入BlockingQueue,则返回失败
- put(anObject):把anObject加入到BlockingQueue里面,如果BlockingQueue没有空间,则调用此方法的线程被阻断,直到BlockingQueue里面有空间再继续
- poll(long timeout,TimeUnit unit):从BlockingQueue取出一个队首的对象,如果在指定的时间内,队列一旦有数据可以取,则立即返回队列中的数据。否则直到时间超时还没有数据可以取出的话,返回失败
- take():取出BlockingQueue里面排在首位的对象,如果BlockingQueue为空,阻断进入等待状态直到BlockingQueue有新的数据被加入
- drainTo():一次性从BlockingQueue获取所有可用的数据对象(还可以指定获取数据的个数),通过该方法,可以提升获取数据的效率;不需要多次分批加锁或者释放锁
阻塞队列的模拟
- 拥有固定长度承装元素的容器
- 计数器统计容器容量的大小
- 当队列里面没有元素的时候执行线程-》需要等待
- 当队列元素已满的时候执行线程也需要等待
ArrayBlockingQueue
- ArrayBlockingQueue:基于数组的阻塞队列的实现
- 在ArrayBlockingQueue的内部,维护了一个定长的数组,用于缓存队列中的数据对象,其内部没有读写分离,也就意味着生产和消费不能完全并行,长度是需要定义的,可以指定先进先出或者先进后出,也叫有界队列,在很多时候非常适合使用
LinkedBlockingQueue
-
LinkedBlockingQueue:基于链表的阻塞队列
-
和ArrayBlockingQueue类似,其内部维持着一个数据的缓冲队列(该队列由一个链表构成),
LinkedBlockingQueue之所以能高效处理并发数据,是因为其内部实现采用分离锁(读写分离两个锁),从而实现生产者和消费者的完全并行运行。它是一个无界队列
SynchronousQueue
- SynchronousQueue:一种没有缓冲的队列
- 生产者产生的数据直接会被消费者获取并且消费
PriorityBlockingQueue
- PriorityBlockingQueue:基于优先级的阻塞队列
- 优先级的判断通过构造函数传入的Compator对象来决定,也就是说传入队列的对象必须实现Comparable接口,在实现PriorityBlockingQueue时,内部控制线程同步的锁采用得是公平锁,他也是一个无界的队列
DelayQueue
-
DelayQueue:带有延迟时间的Queue
-
其中的元素只有当其指定的延迟时间到了,才能从队列中获取该元素。DelayQueue中的元素必须实现Delayed接口,
DelayQueue是一个没有大小限制的队列,应用场景很多,比如对于缓存超时的数据进行移除、任务超时处理、空闲链接的关闭等等