1. 怎样确保一个集合不能被修改
使用
Collections.unmodifiableCollection(Collection c)
方法来创建一个只读集合。
2. Comparable和Comparator区别
- Comparable是让集合元素自身具备比较性,即元素所属的类需要实现Comparable接口,并覆盖compareTo(T o)方法,这意味着比较的逻辑是嵌入在元素类中的。
- Comparator:是让集合具备比较性,
这种方式允许在不修改元素类的情况下,为集合提供自定义的比较逻辑
。
3. ArrayList默认大小是多少?如何进行扩容?
在用无参构造来创建对象时创建一个空数组,长度为0.这个时候先分配一个
默认大小为10
,在进行扩容。
在有参数构造中,传入的参数是正整数就按照传入的参数来确定创建数组的大小,在进行扩容。一般扩容到原来的1.5倍
。
4. 如何解决并发下ArrayList不安全的问题
- 使用Vector。
- 使用Collections工具类:Collections.synchronizedList(new ArrayList<>());
- 使用CopyOnWriteArrayList。
5. CopyOnWriteArrayList是什么?可以用于什么场景?有哪些缺点?
CopyOnWriteArrayList(免锁容器)是一个并发容器。适合读多写少的场景。
在CopyOnWriteArrayList中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全的执行
。
缺点:
- 由于写操作的时候,需要拷贝整个数组,消耗内存,如果原数组内容过大,可能会导致young gc或者full gc。
- 不能用于实时读的场景,像拷贝数组,新增元素都需要时间,所以调用一个set操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList能做到最中意执行,但是无法满足实时性要求。
CopyOnWriteArrayList 透露的重要思想
- 读写分离
- 最终一致性
- 使用另外开辟空间的思路,来解决并发冲突。
6. HashMap实现原理
HashMap中put元素时,首先根据key的hashcode计算hash值,根据hash值得到这个元素在数组中的位置,如果数组在该位置上已经存在元素,则将元素以链表的方式存放,如果链表长度大于8,则判断数组长度是否大于16,若大于则转换成红黑树,否则,扩容数组长度。
7. HashMap的key是自定义的类吗
可以,但是
HashMap的key如果是自定义的类,就需要重写hashcode()和equals()方法
。
8. 为什么String、Integer这样的包装类适合作为HashMap的key
String、Integer等包装类特性能够保证Hash值的不可更改性和计算准确性,能够有效减少Hash碰撞的几率。
- 都是final类型,即不可变性,保证key的不可更改性,不会存在获取Hash值不同的情况。
- 内部已经重写了equals()、hashcode()方法,遵守了HashMap内部的规范,不容易出现Hash值计算错误的情况。
9. ConcurrentHashMap和HashTable的区别
- 底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段数组+链表,JDK1.8采用的数据结构跟HashMap一样,数组+链表/红黑树。
- 实现线程安全的方式
- 在JDK1.7的时候,
ConcurrentHashMap采用分段锁对整个数组进行了分割,每个锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在竞争问题,提高了并发效率。JDK1.8摒弃了Segment概念,而是直接用Node数组+链表+红黑树的结构来实现,并发控制使用synchronized和CAS。(根据key计算出hash值,如果hash无冲突,则cas放入数组的节点中,如果在数组中hash值已经存在,锁定数组的头节点,添加到链表中 )
整个过程看起来就像是优化过且线程安全的HashMap。- HashTable,使用synchronized来保证安全。效率非常低下,当一个线程访问同步方法时,另一个线程也访问同步方法,可能会进入阻塞或者轮询状态。如果一个线程调用put操作,则另一个线程无法调用put以及get操作。
10. 为什么ConcurrentHashMap和HashTable不支持key,value为null
因为HashMap是非线程安全的,默认单线程环境中使用,如果get(key)为null。可以通过containsKey(key)方法来判断这个key的value为null,还是不存在这个key。而ConcurrentHashMap、HashTable是线程安全的,在多线程操作时,因为get(key)和contains(key)两个操作和在一起不是一个原子性操作,可能在contains(key)时发现存在这个键值对,但是get(key)时,其他线程删除了键值对,导致get(key)返回的是null,所以无法区分value为null还是不存在这个key(
核心:并发环境下,containsKey返回key存在,但是其他线程删除这个key,get的时候为空。所以无法判断出这个key是否存在,从而导致异常。
)
11. 快速失败(fail-fast)和安全失败(fail-safe)区别
Iterator安全失败是基于对底层集合做拷贝,因此,不受源集合上修改的影响。java.util包下面所有的集合类都是快速失败的,而java.util.concurrent包下所有类都是安全失败的。快速失败的迭代器会抛出ConcurrentModificationException异常,而安全失败不会抛出这种异常。
12. ConcurrentModificationException是什么?
并发修改异常,一些对象不允许并发修改,当这些修改行为被检测到的时候,就会抛出这个异常。
一些集合的Iterator实现中,如果提供这种并发修改异常检测,那么这些Iterator可以称为【fail-fast Iterator】。即检测到并发修改时,直接抛出异常,而不是继续执行,等到获取到一些错误值时在抛出异常。
异常检测主要是通过modCount和expectedModCount两个变量实现。
- modCount集合被修改的次数,一般是被集合持有,每次调用add、remove方法会导致modCount+1.
- expectedModCount,一般是被Iterator持有,一般在Iterator初始化时赋初始值,在调用Iterator的remove()方法时更新。
总结:
ArrayList和HashMap的Iterator都是fail-fast,Iterator在获取下一个元素,删除元素时,都会比较modCount和expectedModCount,不一致就会抛出异常。所以,当使用Iterator遍历元素时,需要删除元素,一定要使用Iterator的remove()方法来删除。而不是直接调用ArrayList或者HashMap的remove方法。否则会导致Iterator中的expectedModCount没有被及时修改,之后获取下一个元素或者删除元素时,modCount和expectedModCount不一致,然后抛出ConcurrentModificationException
。
13. 什么是阻塞队列,阻塞队列的实现原理是什么
阻塞队列是一个支持两个附加操作的队列。
两个附加操作分别是:
- 在队列为空时,获取元素的线程会等待队列变为非空。
- 队列满时,存储元素的线程会等待元素可用。
JDK提供了7个阻塞队列,分别为:- ArrayBlockingQueue:一个由数组结构组成的有界阻塞队列
- LinkedBlockingQueue: 一个由链表结构组成的有界阻塞队列
- PriorityBlockingQueue:一个支持优先级排序的无界阻塞队列
- DelayQueue: 一个使用优先级队列实现的无界阻塞队列
- SynchronousQueue: 一个不存储元素的阻塞队列
。。。。。Java5 之前实现同步存取时,可以使用普通的一个集合,然后在使用线程的协作和同步实现生产者、消费者模式。主要技术是用好wait、notify、synchronized这些。在Java5之后,可以使用阻塞队列实现。
14. 什么是SynchronousQueue
SynchronousQueue是一个没有容量、无缓冲等待队列,他不存储元素,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。