【Java面试】- 并发容器篇
JDK 提供的并发容器
ConcurrentHashMap
: 线程安全的 HashMapCopyOnWriteArrayList
: 线程安全的 List,在读多写少的场合性能非常好,远远好于 Vector.ConcurrentLinkedQueue
: 高效的并发队列,使用链表实现。可以看做一个线程安全的 LinkedList,这是一个非阻塞队列。BlockingQueue
: 这是一个接口,JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。ConcurrentSkipListMap
: 跳表的实现。这是一个 Map,使用跳表的数据结构进行快速查找
ConcurrentHashMap
- 在
ConcurrentHashMap
中,无论是读操作还是写操作都能保证很高的性能:在进行读操作时(几乎)不需要加锁,而在写操作时通过锁分段技术只对所操作的段加锁而不影响客户端对其它段的访问
CopyOnWriteArrayList
CopyOnWriteArrayList
类的所有可变操作(add
,set
等等)都是通过创建底层数组的新副本来实现的。当List
需要被修改的时候,我并不修改原有内容,而是对原有数据进行一次复制,将修改的内容写入副本。写完之后,再将修改完的副本替换原来的数据,这样就可以保证写操作不会影响读操作了- 从
CopyOnWriteArrayList
的名字就能看出CopyOnWriteArrayList
是满足CopyOnWrite
的ArrayList
,所谓CopyOnWrite
也就是说:在计算机,如果你想要对一块内存进行修改时,我们不在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后呢,就将指向原来内存指针指向新的内存,原来的内存就可以被回收掉了 - 源码解析:
CopyOnWriteArrayList
读取操作的实现: 读取操作没有任何同步控制和锁操作,理由就是内部数组array
不会发生修改,只会被另外一个array
替换,因此可以保证数据安全
/** The array, accessed only via getArray/setArray. */
private transient volatile Object[] array;
public E get(int index) {
return get(getArray(), index);
}
@SuppressWarnings("unchecked")
private E get(Object[] a, int index) {
return (E) a[index];
}
final Object[] getArray() {
return array;
}
CopyOnWriteArrayList
写入操作的实现:CopyOnWriteArrayList
写入操作add()
方法在添加集合的时候加了锁,保证了同步,避免了多线程写的时候会copy
出多个副本出来
/**
* Appends the specified element to the end of this list.
*
* @param e element to be appended to this list
* @return {@code true} (as specified by {@link Collection
*/
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();//释放锁
}
}
ConcurrentLinkedQueue
Java
提供的线程安全的Queue
可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue
,非阻塞队列的典型例子是ConcurrentLinkedQueue
,在实际应用中要根据实际需要选用阻塞队列或者非阻塞队列。 阻塞队列可以通过加锁来实现,非阻塞队列可以通过CAS
操作实现- 从名字可以看出,
ConcurrentLinkedQueue
这个队列使用链表作为其数据结构.ConcurrentLinkedQueue
应该算是在高并发环境中性能最好的队列了。它之所有能有很好的性能,是因为其内部复杂的实现 ConcurrentLinkedQueue
适合在对性能要求相对较高,同时对队列的读写存在多个线程同时进行的场景,即如果对队列加锁的成本较高则适合使用无锁的ConcurrentLinkedQueue
来替代
BlockingQueue
- 阻塞队列(
BlockingQueue
)被广泛使用在“生产者-消费者”问题中,其原因是BlockingQueue
提供了可阻塞的插入和移除的方法。当队列容器已满,生产者线程会被阻塞,直到队列未满;当队列容器为空时,消费者线程会被阻塞,直至队列非空时为止ArrayBlockingQueue
ArrayBlockingQueue
是BlockingQueue
接口的有界队列实现类,底层采用数组来实现。ArrayBlockingQueue
一旦创建,容量不能改变。其并发控制采用可重入锁来控制,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞ArrayBlockingQueue
默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到ArrayBlockingQueue
。而非公平性则是指访问ArrayBlockingQueue
的顺序不是遵守严格的时间顺序,有可能存在,当ArrayBlockingQueue
可以被访问时,长时间阻塞的线程依然无法访问到ArrayBlockingQueue
。如果保证公平性,通常会降低吞吐量。如果需要获得公平性的ArrayBlockingQueue
,可采用如下代码:
private static ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10,true);
LinkedBlockingQueue
inkedBlockingQueue
底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足FIFO
的特性,与ArrayBlockingQueue
相比起来具有更高的吞吐量,为了防止LinkedBlockingQueue
容量迅速增,损耗大量内存。通常在创建LinkedBlockingQueue
对象时,会指定其大小,如果未指定,容量等于Integer.MAX_VALUE
PriorityBlockingQueue
PriorityBlockingQueue
是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现compareTo()
方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。PriorityBlockingQueue
并发控制采用的是ReentrantLock
,队列为无界队列(ArrayBlockingQueue
是有界队列,LinkedBlockingQueue
也可以通过在构造函数中传入capacity
指定队列最大的容量,但是PriorityBlockingQueue
只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容)。- 简单地说,它就是
PriorityQueue
的线程安全版本。不可以插入null
值,同时,插入队列的对象必须是可比较大小的(comparable
),否则报ClassCastException
异常。它的插入操作put
方法不会block
,因为它是无界队列(take
方法在队列为空的时候会阻塞)
ConcurrentSkipListMap
- 对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是
O(logn)
所以在并发数据结构中,JDK 使用跳表来实现一个Map
,其本质就是同时维护了多个链表 - 使用跳表实现
Map
和使用哈希算法实现Map
的另外一个不同之处是:哈希并不会保存元素的顺序,而跳表内所有的元素都是排序的。因此在对跳表进行遍历时,你会得到一个有序的结果。所以,如果你的应用需要有序性,那么跳表就是你不二的选择。JDK
中实现这一数据结构的类是ConcurrentSkipListMap
- 对于一个单链表,即使链表是有序的,如果我们想要在其中查找某个数据,也只能从头到尾遍历链表,这样效率自然就会很低,跳表就不一样了。跳表是一种可以用来快速查找的数据结构,有点类似于平衡树。它们都可以对元素进行快速的查找。但一个重要的区别是:对平衡树的插入和删除往往很可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对整个数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,你会需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,你只需要部分锁即可。这样,在高并发环境下,你就可以拥有更好的性能。而就查询的性能而言,跳表的时间复杂度也是
【Java面试】- 并发容器篇的更多相关文章
- Java并发容器篇
作者:汤圆 个人博客:javalover.cc 前言 断断续续一个多月,也写了十几篇原创文章,感觉真的很不一样: 不能说技术有很大的进步,但是想法确实跟以前有所不同: 还没开始的时候,想着要学的东西太 ...
- Java多线程-并发容器
Java多线程-并发容器 在Java1.5之后,通过几个并发容器类来改进同步容器类,同步容器类是通过将容器的状态串行访问,从而实现它们的线程安全的,这样做会消弱了并发性,当多个线程并发的竞争容器锁的时 ...
- 【JAVA秒会技术之秒杀面试官】秒杀Java面试官——集合篇(一)
[JAVA秒会技术之秒杀面试官]秒杀Java面试官——集合篇(一) [JAVA秒会技术之秒杀面试官]JavaEE常见面试题(三) http://blog.csdn.net/qq296398300/ar ...
- Java面试之框架篇(九)
spring现在无疑是Java中最火的框架,使用范围广,几乎每个公司面试都会涉及spring和数据库,你可以对Struts不熟悉,但一定不能表现出对spring不了解.第九篇赢在面试全篇介绍sprin ...
- Java面试之框架篇(9)
spring现在无疑是Java中最火的框架,使用范围广,几乎每个公司面试都会涉及spring和数据库,你可以对Struts不熟悉,但一定不能表现出对spring不了解.第九篇赢在面试全篇介绍sprin ...
- Java面试系列第一篇-基本类型与引用类型
这篇文章总结一下我认为面试中最应该掌握的关于基本类型和引用类型的面试题目. 面试题目1:值传递与引用传递 对于没有接触过C++这类有引用传递的Java程序员来说,很容易误将引用类型的参数传递理解为引用 ...
- Java面试准备基础篇_11.24
Java类加载机制 Java内存模型JMM 为什么 Redis 单线程能支撑高并发? 高并发下的接口幂等性解决方案! 面试官问:平常你是怎么对 Java 服务进行调优的? JAVA虚拟机(JVM)六: ...
- Java面试总结-基础篇1
java多线程-- 自旋锁,偏向锁 好处:可以举Servlet和CGI的对比用户线程和守护线程的区别:用户线程结束后JVM会退出,然后守护线程才会终止(比如垃圾回收线程),如何在java中创建守护线程 ...
- Java面试之框架篇(八)
71,谈谈你对Struts的理解. 1. struts是一个按MVC模式设计的Web层框架,其实它就是一个Servlet,这个Servlet名为ActionServlet,或是ActionServle ...
随机推荐
- weblogic高级进阶之ssl配置证书
1.首先需要明白ssl的原理 这里我们使用keytool的方式为AdminServer配置ssl证书 配置证书的方式如下所示: C:\Users\Administrator\Desktop\mykey ...
- Redis:rdb和aof
由于redis的数据都直接存储在内存里,在服务器发生宕机时内存的数据会瞬间清空,那么必须要有重启时恢复数据的方法. redis通过持久化机制将数据存储到磁盘中从而在服务器重启时恢复数据,这篇文章主要简 ...
- php - 如何解决CURL错误( 7 ): 无法连接到主机?
php - 如何解决CURL错误( 7 ): 无法连接到主机? 1. 问题描述 调用第三方接口,本地调用一切ok,线上调用失败! 本地通过curLRequest()方法调用第三方接口时,curl_er ...
- Spring Boot2+Resilience4j实现容错之Bulkhead
Resilience4j是一个轻量级.易于使用的容错库,其灵感来自Netflix Hystrix,但专为Java 8和函数式编程设计.轻量级,因为库只使用Vavr,它没有任何其他外部库依赖项.相比之下 ...
- JVM的堆内存泄漏排查-性能测试
JVM异常说明 https://testerhome.com/articles/24259 一文中已介绍了,JVM每个运行时区域--程序计数器 .Java虚拟机栈.本地方法栈.Java堆.方法区.直接 ...
- java后端无法接收到前端传递的json对象
java后端无法接收到前端传递的json对象 一·可能是因为未使用@RequestBody 在Controller层中,要么使用@RestController要么使用@Controller+@@Req ...
- cv2 exposureFusion (曝光融合)
import cv2 import numpy as np import sys filenames = ['./images/memorial0061.jpg', './images/memoria ...
- 在MFC下绘制直线,使用橡皮筋技术,可以使直线效果跟随鼠标移
void CGraphic1View::OnMouseMove(UINT nFlags, CPoint point) { if(MK_LBUTTON == nFlags) { ...
- Python之浅谈面向对象
目录 面向对象和面向过程 面向过程 面向对象 类与对象 定义类 在现实世界中:先有对象,再有类 在程序中,务必保证:先定义(类),后使用(产生对象) 产生对象 类 对象 产生对象 属性查找顺序 对象赋 ...
- 从发布-订阅模式谈谈 Flask 的 Signals
发布-订阅模式 发布-订阅模式,顾名思义,就像大家订报纸一样,出版社发布不同类型的报纸杂志不同的读者根据不同的需求预定符合自己口味的的报纸杂志,付费之后由邮局安排人员统一派送. 上面一段话,提到了发布 ...