常见JAVA面试题总结<2020 java面试必备>(一)

tech2022-07-14  149

常见JAVA面试题总结<2020 java面试必备>(一)

Java 基础类

== 和 equals的区别

==:

基本类型:比较的是值是否相同   引用类型:比较的是地址是否相同,也就是说 new 出来的对象进行 == 比较,始终返回false

equals:

默认情况下,如果我们没有对对象的 equals 方法进行重写,那 equals 比较的就是地址 而我们使用的 String 是对 equals 进行了重写,把它变成了值比较 ​ 注意:重写时 equals 和 hashCode 必须同时重写 两个对象的 hashCode()相同,则 equals()也一定为 true,对吗 不对,两个对象的 hashCode()相同,equals()不一定 true 因为在散列表中,hashCode()相等即两个键值对的哈希值相等 ​然而哈希值相等,并不一定能得出键值对相等 代码示例

String str1 = "通话"; ​String str2 = "重地"; ​System.out.println(String.format("str1:%d | str2:%d", str1.hashCode(), str2.hashCode())); ​System.out.println(str1.equals(str2));

执行的结果 str1:1179395 | str2:1179395 哈希值相等 false equals结果为false

final 在 java 中有什么作用?

final 修饰的类叫最终类,该类不能被继承。 final 修饰的方法不能被重写。 final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改

String StringBuffer StringBuilder

String 字符串常量、StringBuffer 字符串变量(线程安全)、StringBuilder 字符串变量(非线程安全)

String 底层是final修饰的不可变对象,因此在每次对 String 类型进行改变的时候其实都等同于生成了一个新的 String 对象,然后将指针指向新的 String 对象 所以经常改变内容的字符串最好不要用String,因为每次生成对象都会对系统性能产生影响,特别当内存中无引用对象多了以后,JVM 的 GC 就会开始工作,那速度是一定会相当慢的 StringBuilder类则结果就不一样了,每次改变都会对 StringBuilder对象本身进行操作,而不是生成新的对象,再改变对象引用,所以在一般情况下我们推荐使用 StringBuilder,特别是字符串对象经常改变的情况下 而StringBuffer与StringBuilder不同就在于StringBuffer是线程安全的,底层的append方法使用了synchronized关键字进行修饰​

String str = “i” 与 String str = new String(“i”)一样吗

不一样,因为内存的分配方式不一样。 ​String str = "i" 的方式,java 虚拟机会将其分配到常量池中; ​而 String str = new String("i") 则会被分到堆内存中

重载与重写的区别

重载 参数类型、个数、顺序至少有一个不相同 不能重载只有返回值不同的方法名 存在于父类和子类、同类中 ​ 重写 方法名、参数、返回值相同。 子类方法不能缩小父类方法的访问权限。 子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。 存在于父类和子类之间。 方法被定义为final不能被重写。

int 与 Integer 区别

1**.int**是基本数据类型,Integer是包装类,是一个类 2.int类型的变量初始值为0,而Integer包装类初始值为null 3.Integer类的作用 一是在类中拥有很多方法,方便Int和其他数据类型进行转换,比如parseInt()和toString()方法等等 二是向ArrayList或HashMap存数据时不能用基本类型,只能用包装类

抽象类接口区别

(1)抽象类可以有默认的方法实现。接口根本不存在方法的实现 ​(2)子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现,子类使用关键字implements来实现接口。它需要提供接口中所有声明的方法的实现 ​(3)抽象类可以有构造器 接口不能有构造器 (4)抽象方法可以有public、protected和default这些修饰符 ,接口方法默认修饰符是public 你不可以使用其它修饰符 ​(5)抽象类在java语言中所表示的是一种继承关系,一个子类只能继承一个父类,但是可以实现多个接口 (6)如果你往抽象类中添加新的方法,你可以给它提供默认的实现。因此你不需要改变你现在的代码。 如果你往接口中添加方法,那么你必须改变实现该接口的类

传值和传引用的区别?

传值 实际是将一个值的拷贝传递至方法内部,这个值的原始数据是不会改变的 无论你内部进行的是何种操作,都不会改变这个源数据的值;

​传引用 传递进去的则是指向一个对象的地址,那么在方法内部进行实际操作的时候 就很可能会改变该对象的属性值(当然具体是否改变,还需要结合具体的业务)

说说深拷贝和浅拷贝

深拷贝和浅拷贝是只针对Object和Array这样的引用数据类型的,最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用 浅拷贝仅仅是指向被复制的内存地址,如果原地址中对象被改变了,那么拷贝出来的对象也会相应改变。 深拷贝是开辟了一块新的内存地址用于存放复制的对象。 通俗一点理解就是浅拷贝出来的数据并不独立,如果被复制的对象改变了,那么浅拷贝的对象也会改变,深拷贝之后就会完全独立,与浅拷贝断绝关系。

说说Java中的自动拆箱和装箱

装箱就是自动将基本数据类型转换为包装类型; ​拆箱就是自动将包装类型转换为基本数据类型

自动拆装箱是怎么实现的

是编译器帮我们自动调用了拆装箱的方法,以 Integer/int 为例子 自动装箱就是编译器自动调用了 valueOf(int i) 方法 自动拆箱就是调用了 intValue() 方法,其他基本类型类推

以Integer为例,来分析一下它valueOf方法的源码

public static Integer valueOf(int i) { return i >= 128 || i < -128 ? new Integer(i) : SMALL_VALUES[i + 128]; }

它会首先判断i的大小:如果i小于-128或者大于等于128 ​就创建一个Integer对象,否则执行SMALL_VALUES[i + 128] ​

private static final Integer[] SMALL_VALUES = new Integer[256];

SMALL_VALUES 它是一个静态的Integer数组对象 ​也就是说最终valueOf返回的都是一个Integer对象

自动拆箱和装箱引出的问题,代码举例

在循环的时候

Integer sum = 0; for(int i=0; i < 4000; i++){ sum += i; } 上面的代码`sum +=i` 可以看成`sum = sum + i`,在sum被+操作符操作的时候,会对sum进行自动拆箱操作

​进行数值相加操作,最后发生自动装箱操作转换成Integer对象。其内部变化如下 ​

sum = sum.intValue() + i; Integer sum = new Integer(result);

sum为Integer类型,在上面的循环中会创建4000个无用的Integer对象 ​在这样庞大的循环中,会降低程序的性能并且加重了垃圾回收的工作量

为什么Java中不支持多重继承?

围绕钻石💎形继承问题产生的歧义,考虑一个类 A 有 `foo()` 方法, 然后 B 和 C 派生自 A, 并且有自己的 foo() 实现,现在 D 类使用多个继承派生自 B 和C,如果我们只引用 foo(), 编译器将无法决定它应该调用哪个 foo()。

为什么 String 在 Java 中是 Final 的?

(1)为了实现字符串池 只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约很多堆空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么String interning将不能实现,因为这样的话,如果变量改变了它的值,那么其它指向这个值的变量的值也会一起改变。

​(2)为了线程安全 因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步。字符串自己便是线程安全的。 ​ (3)为了实现String可以创建HashCode不可变性 因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算。这就使得字符串很适合作为Map中的键,字符串的处理速度要快过其它的键对象。这就是HashMap中的键往往都使用字符串

为什么 char[] 比 String 更适合存储密码

由于String在 Java 中是不可变的,如果你将密码存储为String类型,它将在内存中一直可用,直到垃圾收集器清除它,并且由于String存在于字符串池中,所以它很可能会保留在内存中持续很长时间,从而构成安全威胁 一般使用加密密码而不是纯文本。由于字符串是不可变的,所以不能更改字符串的内容,因为任何更改都会产生新的字符串,但是原来的密码字符串并没有改变,而如果你使用char[],你就可以将所有元素设置为空白或零 因此,在char[]中存储密码可以明显降低窃取密码的安全风险

Java集合

ArrayList 和 HashSet 的区别

ArrayList 中的数据对象有顺序且可以重复、HashSet 中的数据对象没有顺序且不可以重复

HashSet底层数组结构

HashSet 底层就是 HashMap,但是 HashSet 添加元素时,只关心 key,不关心 value ​因为 HashSet 源码中 value 的值默认就是一个 PRESENT 的 Object类型常量

HashSet用什么方法来区分重复

HashSet 里面存放的是对象的引用,equals是用来判断是否引用同一个对象,所以当两个元素只要满足了equals()时就已经指向同一个对象,也就出现了重复元素。所以是用equals()来判断。

ArrayList 和 Vector 的区别

Vector增长率为目前数组长度的100%,而ArrayList增长率为目前数组长度的50% Vector是同步的,而ArrayList不是。如果在迭代的时候想对列表进行改变,你应该使用CopyOnWriteArrayList。 ArrayList比Vector快,它因为有同步,不会过载。

ArrayList 和 LinkedList 的区别

数组结构上区别

1)ArrayList是动态数组的数据结构,LinkedList基于链表的数据结构 2)当 new ArrayList() 时,默认构造的是大小为 10的一个 Object[] 数组,当数组的元素超过其容量大小后,会扩容为原来的1.5倍,调用的是 Arrays.copyOf() 方法进行扩容 3)ArrayList 源码中最大的数组容量是Integer.MAX_VALUE-8, 对于空出的8位JDK文档中说是避免一些机器内存溢出,减少出错几率,所以少分配

数据读写上区别

1)ArrayList 对于随机访问元素,向数组尾部添加元素的效率高于LinkedList,但是,删除以及向数组中间添加数据效率低,因为 ArrayList 需要移动数组 ​ 2)ArrayList 添加元素源码分析 ArrayList 源码中添加元素时,会判断如果是尾部,则直接返回尾部的索引 size+1,先改变尾部的索引,再将新元素添加至数组位置 size + 1 如果不是尾部添加,则源码中会进行删除、移动数组的操作。而在数组移动复制时,最终将调用System.arraycopy() 方法,所有这一步就会使得效率较 LinkedList 慢​ ​ 3)LinkedList 基于链表的动态数组,数据添加删除效率高,只需要改变指针指向即可,但是访问数据的平均效率低,需要对链表进行遍历

一个ArrayList在循环过程中删除,会不会出问题,为什么

(1)ArrayList通过foreach迭代是调用的其内部类iterator的next方法。 ​(2)如果通过foreach循环,要去删除某些元素,只能通过迭代器删除​ ​(3)因为迭代器删除后会对modCount设值,不会再循环过程因为modCount值不相等而抛出异常 ​(4)如果是通过ArrayList的删除,则内部迭代器iterator的属性modCount却没有得到更新,所以会抛异常 CopyOnWriteArrayList

问题前言

ArrayList默认情况下肯定是线程不安全的,要是多个线程并发读和写这个ArrayList就会报ConcurrentModificationException的异常 如果想让 ArrayList 变成线程安全的,可以对这个ArrayList的访问都加上线程同步的控制。Synchronized 或 ReadWriteLock 读写锁的方式来控制,都可以 最大的问题,在于写锁和读锁的互斥。假设是写少读多的场景,那么偶尔执行一个写操作的时候,是不是会加上写锁,此时大量的读操作过来就会被阻塞住,无法执行

##CopyOnWrite思想

不使用锁,而是在写数据的时候利用拷贝的副本来执行 (1)将要操作的共享变量 object[] 数组使用volatile关键字修饰 (2)让写线程在往集合中添加数据的时候,先拷贝存储的数组,然后添加元素到拷贝好的数组中,再用现在的数组去替换成员变量的数组 (3)读线程根据 volatile关键字的可见性原则,就能够感知到变化 ​ 下面是JDK里的 CopyOnWriteArrayList 的源码​ ​

// 这个数组是核心的,因为用volatile修饰了 // 只要把最新的数组对他赋值,其他线程立马可以看到最新的数组 private transient volatile Object[] array; // 写操作 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; // 然后把副本数组赋值给volatile修饰的变量 setArray(newElements); return true; } finally { lock.unlock(); } }// 读线程根据 volatile关键字的可见性原则,就能够感知到变化 private E get(Object[] a, int index) { // 最简单的对数组进行读取 return (E) a[index]; }

存在的问题及适合场景

(1)CopyOnWrite的机制虽然是线程安全的,但是在add操作的时候需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可导致young gc或者full gc,所以使用到这个集合的时候尽量不要出现频繁的添加操作 (2)​而且在迭代的时候如果数据量太多的时候,实时性可能就差距很大了。在多读取,少添加的时候,他的效果还是不错的(数据量大无所谓,只要你不添加,他都是好用的)

Comparable 和 Comparator 是什么,列出它们的区别

Comparable(康怕瑞儿珀)

Comparable是一个内比较器,实现了Comparable接口的一个compareTo方法,如果开发者add进入一个Collection的对象想要Collections的sort方法帮你自动进行排序的话,那么这个对象必须实现Comparable接口

Comparator(康盘瑞儿特儿)

​Comparator可以认为是一个外比较器,个人认为有两种情况可以使用实现Comparator接口的方式: ​1、一个对象不支持自己和自己比较(没有实现Comparable接口),但是又想对两个对象进行比较。 ​2、一个对象实现了Comparable接口,但是开发者认为compareTo方法中的比较方式并不是自己想要的那种比较方式。 ​Comparator接口里面有一个compare方法,方法有两个参数T o1和T o2,是泛型的表示方式,分别表示待比较的两个对象,方法返回值和Comparable接口一样是int

你了解 WeakHashMap 吗?说说他与其它Map有什么不同

WeakHashMap 与其它 HashMap有点不同,它里面存入的对象全是弱引用对象,所谓的弱引用就是指:当垃圾回收期回收时,不管内存空间是否足够,都会回收这些对象。 弱引用通常用在对内存敏感的程序中,比如高速缓存就有用到弱引用,当垃圾回收期回收时....

HashMap 和 Hashtable 有什么区别?

HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解 HashMap允许将null作为一个entry的key或者value,而Hashtable不允许 HashTable的方法是Synchronize修饰的,是线程安全的,也就是说是同步的 HashMap是线程序不安全的,不是同步的,由于非线程安全,在只有一个线程访问的情况下,效率要高于Hashtable

HashMap 与 TreeMap 的区别

HashMap是基于哈希散列表实现,在使用HashMap要求添加的键类明确定义了hashCode()和equals(),为了优化HashMap空间的使用,您可以调优初始容量和负载因子 TreeMap是基于红黑树实现的,TreeMap没有调优选项,因为该树总处于平衡状态 对于数据的插入、删除、和定位元素,HashMap性能要高于TreeMap,因为TreeMap始终要保持树的平衡情况 对于数据的遍历,TreeMap遍历是排好序的,而HashMap遍历则是无序的,所以遍历时对数据顺序没有要求的情况下,HashMap的效率要高于TreeMap

HashMap

HashMap整体阐述

HashMap是一个“链表散列”的数据结构,即数组 + 单向链表的结合,在JDK1.8之后是引入了红黑树,当链表长度 > 8 并且数组长度大于64时,单向链表将转换为红黑树,以提高查询效率 HashMap对数据的存取是无序的,键和值位置都可以是null,但是键位置只能是一个null 当存储容量达到临界值12时(临界值计算:容量16 * 负载因子0.75 = 12),自动扩容数组长度2倍​

HashMap如何确定对象在数组中的位置

在 put 时先调用 `hash(Object key)` 方法 ,获取key的Hash值,当 key 等于null 时返回0,所以说为什么 HashMap 支持空的键,而且空键是唯一的 当 key 不等于 null 的时候,将 `key.hashCode()` 值,与 hashCode无符号右移(>>>)16位后的值,进行异或(^)运算,得 hashCode 值,将 hashCode 值与数组长度n - 1,进行 &(与) 运算,得到索引位置的
JDK 1.8中对hash算法和寻址算法是如何优化的
在以上描述的获取key的Hash值过程中,JDK1.8进行了两处的优化,一是hash算法的优化,二是寻址算法的优化 hash算法的优化体现在key.hashCode() 值与 hashCode无符号右移(>>>)16位后的值,进行异或(异或:参与运算的两个值,如果相应的二进制位相同,则结果为0,否则为1),在他的低16位中,让高低16位进行了异或,这样低16位同时保持了高低16位的特征,然后再和 数组长度n - 1,进行 &(与) 运算时,就尽量避免hash值后续出现的冲突 如果说没有异或这一步,高16位之间的与运算,是可以忽略的,核心点在于低16位的与运算,hash值的高16位没有参与到与运算里来,则计算出来的hash值可能会很相近,甚至是一样的,加大了hash冲突的概率 寻址算法的优化体现在用&(与) 运算替代取模,提升性能,&(与) 运算效果跟 hash对n取模效果是一样的,但是与运算的性能要比hash对n取模要高很多,数学问题,必须保持数组长度一直是2的n次方,所以说HashMap在对容量设值时,一直是2的n次方

HashMap是如何进行扩容的

HashMap底层是一个数组,当这个数组满了之后,他就会自动进行扩容,在数组长度为16的时候,两个hash值的位置是一样时,用链表来处理,hash冲突的问题,如果数组的长度扩容为32后,会重新对每个hash值进行寻址,也就是用每个hash值跟新数组的length - 1进行&(与) 运算,拿到新的存放位置 判断二进制结果中是否多出一个bit的1,如果没多,那么就是原来的index(比如原来是5,还是5),如果多了出来,那么就是index + oldCap(原来的5 + 原数组长度16 = 扩容后的位置21),通过这个方式,就避免了rehash的时候,用每个hash对新数组.length取模,取模性能不高,位运算的性能比较高​
为什么HashMap初始容量必须是2的N次幂
HashMap 容量为2次幂的原因,寻址算法优化 &(与) 运算的必要条件,一是提升性能,二是为了数据的的均匀分布,减少hash冲突,毕竟hash冲突越大,代表数组中一个链的长度越大 如果创建HashMap对象时,输入的数组长度是10,不是2次幂,HashMap 源码中有一个 tableSizeFor 方法,该方法会通过一系列的位移运算找到大于等于10的最小的2的幂值
HashMap中容量的初始化
当HashMap进行扩容时,这个过程涉及到 rehash、复制数据等操作,非常消耗性能。所以开发中尽量减少扩容的次数 按照阿里巴巴手册,在平时的开发中,当我们明确知道 HashMap 中元素的个数的时候,把默认容量设置成 initialCapacity / 0.75F + 1.0F 是一个在性能上相对好的选择 比如说如果我们要存7个元素,并不是直接给7,而是 7/0.75 + 1 = 10 ,初始化时给 10,10经过HashMap源码中初始化处理(找大于等于初始值最小的2的幂值)之后,会被设置成16,根据HashMap 扩容临界值计算:容量16 * 加载因子0.75 = 12,这样当存储达到12时才会进行扩容,这就大大的减少了扩容的几率 如果初始化直接给 7 的话,经过处理则找到的是 8 ,临界值计算:容量8 * 加载因子0.75 = 6, 这样就是有问题的,存储 7 个元素,但是当到达 6 个时就开始进行扩容了,非常影响性能 往大了说,如果需要存储 1024 个元素,由于没有设置初始容量大小,随着元素不断增加,每次容量会扩容数组长度2倍,总共要扩容 7 次,严重影响了性能

HashMap是如何解决hash碰撞问题的

如果有多个key它们算出来的hash值是一样的,会在数组的这个位置上挂一个链表,这个链表里面放入多个元素,get时,如果定位到数组里发现这个位置挂了一个链表,此时遍历链表,从里面找到要找的那个key-value对 假设链表很长,可能会导致遍历链表,性能会比较差,最差的情况可能会将链表从头到尾遍历一次,时间复杂度取决于链表的长度 O(n) 在JDK1.8之后,对数据结构做了进一步的优化,引入了红黑树。当链表长度超过8且当前数组长度 > 64时(小于64只是进行扩容),链表就转换为红黑树,遍历一颗红黑树找一个元素,性能会比链表高一些
为什么链表长度超过8时才转为红黑树
在HashMap源码中有一段注释是说,因为TreeNodes的大小大约是普通Nodes的两倍,所以只有当bucket(桶)包含足够多的节点时才会转成TreeNodes,而 8 是通过定义的成员变量 TREEIFY_THRESHOLD 决定的 反之,当bucket(桶)中节点少于6时,会从红黑树转回链表,这是由成员变量 `UNTREEIFY_THRESHOLD` 决定的 个人想法:如果在链表长度比较小时转为红黑树,反而会降低效率,因为红黑树需要进行左旋,右旋,变色这些操作来保持平衡 。同时数组长度小于64时,搜索时间相对要快些。所以为了提高性能和减少搜索时间,底层在阈值大于8并且数组长度大于64时,链表才转换为红黑树

关于HashMap遍历说明

在HashMap提供的遍历方式中,有 Iterator 和 keySet 方式,根据阿里开发手册,不建议使用这两种方式,因为会迭代两次,降低性能。 JDK8以后 Map 接口中增加了默认方法 forEach(BiConsumer<? super K,? super V> action) ,BiConsumer 为函数式接口,建议使用这种方式

map.forEach((key,value)->{ System.out.println(key+"---"+value); });

为什么HashMap使用红黑树而不是AVL树 红黑树牺牲了一些查找性能,但其本身并不是完全平衡的二叉树,因此插入删除操作效率略高于AVL树 AVL树用于自平衡的计算牺牲了插入删除性能,但是因为最多只有一层的高度差,查询效率会高一些

LinkHashMap的实现原理

image LinkedHashMap继承于HashMap,LinkedHashMap是有序的,且默认为插入顺序 LinkedHashMap是在HashMap的基础上,多了一个双向链表来维持顺序

在LinkedHashMap的get()方法中,我们每次获取元素的时候,都要调用afterNodeAccess(e)将元素移动到尾部​,在插入数据的时候,如果removeEldestEntry(first)返回true,按照LRU策略,那么会删除头节点

什么是双向链表

(1)当某个位置的数据被命中,通过调整该数据的位置,将其移动至尾部 (2)新插入的元素也是直接放入尾部(尾插法) (3)这样一来,最近被命中的元素就向尾部移动,那么链表的头部就是最近最少使用的元素所在的位置

ConcurrentHashMap

JDK1.7的ConcurrentHashMap

在JDK 1.7以前的版本里,ConcurrentHashMap分段锁思想是 [数组1] , [数组2],[数组3] ---> 每个数组都对应一把锁,分段加锁

这时如果多个线程过来,线程1要put的位置是数组1[5],线程2要put的位置是数组2[21],put的数组位置不同,则不会影响,如果put的是同一个数组,此时就是串行处理了,并发性肯定会降低

JDK1.8的ConcurrentHashMap

JDK 1.8以后,做了锁的细粒度优化,对数组中每个元素进行CAS,如果是多线程对数组里同一个位置进行put操作时,采取CAS策略,只有一个线程能成功执行这个CAS 就是说刚开始先获取一下数组[5]这个位置的值,null,然后执行CAS,线程1,比较一下,put进去我的这条数据,其他的线程执行CAS不再是null,都会失败,失败后会对数组[5]这个位置进行 synchronized 加锁,基于链表或者是红黑树来进行处理,在这个位置插进去自己的数据 在CurrentHashMap中实际上加的是读写锁,如果写冲突就会等待,所以对某个数组位置的元素操作CAS失败,就会对这个位置进行 synchronized 加锁,读的时候是没有加锁的 如果是对数组不同位置的元素操作,此时大家可以并发执行的​

ConcurrentHashMap是如何去统计size的

要统计整个ConcurrentHashMap的size,就必须统计所有Segment里元素的大小后求和 Segment里的全局变量count是一个Volatile变量,那么在多线程场景下,并不是直接把所有Segment的count相加就可以得到ConcurrentHashMap的size得,因为在累加前count值可能会发生变化,那么统计结果就不准了 但是在累加count操作过程中,之前累加过的count发生变化的几率非常小,所以ConcurrentHashMap的做法是先尝试2次通过不加锁的方式来统计各个Segment的大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小,因为加锁做法显然效率低,所以不是一来就使用的 ConcurrentHashMap使用modCount变量,在put,remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化

=================================================== (几年面试的总结,有自己的,有网上看各位大佬总结的,归纳到一起,没有一次写完,后续加更,可能有些理解不是很到位,不完整,欢迎大佬指点,指出 错误的地方) 常见JAVA面试题总结<2020 java面试必备>(二)多线程

常见JAVA面试题总结<2020 java面试必备>(三)JVM

常见JAVA面试题总结<2020 java面试必备>(四)设计模式

常见JAVA面试题总结<2020 java面试必备>(五) 网络

最新回复(0)