Java面试题系列——JavaSE面试题(集合一)

1、HashMap和ConcurrentHashMap的区别有哪些?

(1)HashMap不是线程安全的,而ConcurrentHashMap是线程安全的。

(2)ConcurrentHashMap采用锁分段技术,将整个Hash桶进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都有锁存在,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里还需要获取segment锁。

(3)ConcurrentHashMap让锁的粒度更精细一些,并发性能更好。

2、线程安全的集合有哪些?

(1)Vector:Vector是常用的Collection集合中的线程安全的集合,其实现线程安全的原理是为其所有需要保证线程安全的方法都添加了synchronized关键字,锁住了整个对象。使用锁的种类:互斥锁。

(2)Hashtable:HashtableVector类似,都是为每个方法添加了synchronized关键字,来实现的线程安全,锁住了整个对象。使用锁的种类:互斥锁。

(3)ConcurrentHashMap :采用了分段锁(Segment),HashTable的加锁方法是给每个方法加上synchronized关键字,线程安全。

(4)Stack:继承于Vector, 栈是后进先出的。

(5)ArrayBlockingQueue:是一个阻塞队列,底层使用数组结构实现,按照先进先出(FIFO)的原则对元素进行排序。

3、 如何做到集合边遍历,边删除?

1、使用Iterator的remove()方法:

    List platformList = new ArrayList<>();
    platformList.add("博客园");
    platformList.add("CSDN");
    platformList.add("掘金");
 
    Iterator iterator = platformList.iterator();
    while (iterator.hasNext()) {
        String platform = iterator.next();
        if (platform.equals("博客园")) {
            iterator.remove();
    }

 2、使用for循环正序遍历:


    List platformList = new ArrayList<>();
    platformList.add("博客园");
    platformList.add("CSDN");
    platformList.add("掘金");
 
    for (int i = 0; i < platformList.size(); i++) {
        String item = platformList.get(i);
        if (item.equals("博客园")) {
            platformList.remove(i);
            i = i - 1;
        }
    }

 3、注意事项:不能使用foreach进行循环遍历,不然会抛出并发异常。

4、HashMap的底层原理是什么?如何扩容,并解决Hash碰撞的?

(1)HashMap的底层原理:

(2)HashMap的扩容机制

        (2.1)HashMap的默认负载因子:默认的负载因子是0.75f,也就是75% 负载因子的作用就是计算扩容阈值用,比如说使用无参构造方法创建的HashMap 对象,他初始长度默认是16 阈值 = 当前长度 * 0.75 就能算出阈值,当当前长度大于等于阈值的时候HashMap就会进行自动扩容。

   为什么HashMap的默认负载因子是0.75,而不是0.5或者是整数1呢?1、阈值(threshold) = 负载因子(loadFactor) x 容量(capacity) 根据HashMap的扩容机制,他会保证容量(capacity)的值永远都是2的幂 为了保证负载因子x容量的结果是一个整数,这个值是0.75(3/4)比较合理,因为这个数和任何2的次幂乘积结果都是整数。2、理论上来讲,负载因子越大,导致哈希冲突的概率也就越大,负载因子越小,费的空间也就越大,这是一个无法避免的利弊关系,所以通过一个简单的数学推理,可以测算出这个数值在0.75左右是比较合理的。

 static final float DEFAULT_LOAD_FACTOR = 0.75f;

(2.2) HashMap的扩容机制: 

        阈值(threshold) = 负载因子(loadFactor) x 容量(capacity)当HashMap中table数组(也称为桶)长度 >= 阈值(threshold) 就会自动进行扩容。扩容的规则是这样的,因为table数组长度必须是2的次方数,扩容其实每次都是按照上一次tableSize位运算得到的就是做一次左移1位运算,
假设当前tableSize是16的话 16转为二进制再向左移一位就得到了32 即 16 << 1 == 32 即扩容后的容量,也就是说扩容后的容量是当前容量的两倍,但记住HashMap的扩容是采用当前容量向左位移一位(newtableSize = tableSize << 1),得到的扩容后容量,而不是当前容量x2。问题又来了,为什么计算扩容后容量要采用位移运算呢,怎么不直接乘以2呢?因为cpu毕竟它不支持乘法运算,所有的乘法运算它最终都是再指令层面转化为了加法实现的,这样效率很低,如果用位运算的话对cpu来说就非常的简洁高效。

(3)HashMap如何解决Hash碰撞:

        1. 使用链地址法(使用散列表)来链接拥有相同hash值的数据;2. 使用2次扰动函数(hash函数)来降低哈希冲突的概率,使得数据分布更平均;3. 引入红黑树进一步降低遍历的时间复杂度,使得遍历更快; 

5、HashMap 在什么条件下,单链表需要转为红黑树?

1、链表长度>=8。2、数组长度大于64。

6、ConcurrentHashMap为什么是线程安全的,咋做到的?

        JDK1.7中,ConcurrentHashMap使用的锁分段技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。刚刚说的一段一段就是指Segment,它继承了ReentrantLock,具备锁和释放锁的功能。ConcurrentHashMap只有16个Segment,并且不会扩容,最多可以支持16个线程并发写。

        JDK1.8放弃了锁分段的做法,采用CAS和synchronized方式处理并发。以put操作为例,CAS方式确定key的数组下标,synchronized保证链表节点的同步效果。

持续更新中,敬请期待!

参考文章:

Java集合—List如何一边遍历,一边删除?_测试狗一枚的博客-CSDN博客_集合边遍历边删除

HashMap是怎么解决哈希冲突的?_deep sleep的博客-CSDN博客_hashmap解决hash冲突的方法

 Java面试题系列——JavaSE面试题(一)_循环网络不循环的博客-CSDN博客

你可能感兴趣的:(java)