互联网大厂面试考点————Java基础

博客地址: Coding Lemon’s blog
所有文章会第一时间在博客更新!

后面会花点时间,将前面整理的知识点以提问的形式展现出来。面试时问的问题也与下面的八九不离十,直接背就好了,当然如果能加上自己的理解就更好了!

问题来自:八股文骚套路之Java基础(重构完善版)

通过打星与加粗的方式对下面面试题的重要性进行评级!难度是针对互联网大厂的。

  • ⭐ :面试中不常问到,如果面试官问到尽量能答出来,答不出来也没关系。
  • ⭐⭐ :面试中不常问到,但是如果面试官问到的话,答不出来对你的印象会减分。
  • ⭐⭐⭐:面试中会问到,答不出来面试有点悬。面试官会惊讶为什么你这也不会。
  • ⭐⭐⭐⭐:面试高频考点
  • ⭐⭐⭐⭐⭐:面试超高频考点。四星考点和五星考点是参加十场面试,至少能有五场面试问到这些的。大家在准备面试过程中尽量把这些知识点的回答条理梳理清楚,面试官一问就开背。

1. Java 语言的特点(⭐⭐)

  1. 简单易学
  2. 面向对象(封装,继承,多态)
  3. 平台无关性( Java 虚拟机实现平台无关性)
  4. 支持网络编程并且很方便
  5. 编译与解释并存
  6. 安全性和可靠性有保证

2.比较 JVM 和 JDK 以及 JRE(⭐⭐⭐)

  • JVM:Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。
  • JDK: 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。
  • JRE:JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序

3.为什么说 Java 语言“解释与编译并存”(⭐⭐)

Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(*.class 文件),这种字节码必须由 Java 解释器来解释执行。因此,我们可以认为 Java 语言编译与解释并存。

总结:需要将.java文件专为.class文件,然后由JVM来执行.class文件,此时既有编译型语言的特点(前半部分),又有解释性语言的特点(后半部分)。

4. Java 基本类型有哪几种,各占多少位?(⭐⭐)

基本类型位数字节默认值
int3240
short1620
long6480L
byte810
char162‘u0000’
float3240f
double6480d
boolean11false

另外,对于 boolean,官方文档未明确定义,它依赖于 JVM 厂商的具体实现。逻辑上理解是占用 1 位,但是实际中会考虑计算机高效存储因素。

5. Java 泛型,类型擦除?(⭐⭐⭐)

Java 泛型(generics)是 JDK 5 中引入的一个新特性。Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除

类型擦除:Java 的泛型在编译器有效,在运行期被删除,也就是说所有泛型参数类型在编译后都会被清除掉。(也就是说,不确定的类型在编译成字节码后一定会确定具体的类型)

泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。

常用的通配符为: T,E,K,V,?

  • ? 表示不确定的 java 类型
  • T (type) 表示具体的一个 java 类型
  • K V (key value) 分别代表 java 键值中的 Key Value
  • E (element) 代表 Element

6. == 和 equals() 的区别(⭐⭐)

对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。

类如果没有覆盖equals()方法,则与相同,因为Object的equals()方法内部就是用比较两个对象。

另外String类的equals()方法是被重写过的,他比较的是两个字符串的内容是否相同(将两个字符串拆分为两个字符数组,然后每个字符挨个进行比较)。

7. 为什么重写 equals() 时要重写 hashCode() 方法?(⭐⭐⭐⭐)

因为每个对象都有对应的hashCode。那么如果你用equals()方法比较两个对象的时候,你认为这两个对象是相同的,那么他们的hashCode()的方法返回值一定要相同

如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

重点:如果两个对象用equals()方法比较返回true,那他们的hashCode值一定相同;但是如果他们的hashCode相同,两个对象不一定相同(比如HashMap中的碰撞问题)。

8. 重载和重写的区别?(⭐⭐⭐⭐)

  • 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理(比如get(int num1)和get(int num1,int num2))

  • 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法(例如子类继承父类的方法,并修改该方法的实现)

9. 深拷贝和浅拷贝?(⭐)

  • 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
  • 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

image.png

总结:对于B对象拷贝A对象来说,浅拷贝是将B对象指向的A对象的存储地址,此时A对象和B对象实际上是同一个对象;深拷贝是B对象拷贝了A对象中的所有的属性值,他们是两个完全独立的对象。

10. 面向对象和面向过程的区别(⭐⭐⭐)

通俗来说,“面向过程”是做一件事,“面向对象”是造一堆东西。

比如愤怒的小鸟,前几年流行的时候,没几天就出个新版,什么太空呀,圣诞呀,鸟越来越多,景越来越炫。一只鸟,一块砖,一头猪,还能写个过程实现。但鸟有会变身的,能下蛋的,砖有铁的,木的,冰的,猪有大的,小的,戴头盔的…混在一起,什么事都能发生,写出所有过程就不现实了。

面向对象就是造出不同的鸟,砖,猪,各自怎么飞,怎么撞,有什么特技,都定义好,交给关卡设计师,给猪垒个楼,摆—摆造型,就可以上线收钱了。

面向过程面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展

面向对象面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低

这个并不是根本原因,面向过程也需要分配内存,计算内存偏移量,Java 性能差的主要原因并不是因为它是面向对象语言,而是 Java 是半编译语言,最终的执行代码并不是可以直接被 CPU 执行的二进制机械码。

而面向过程语言大多都是直接编译成机械码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。

11. 成员变量与局部变量的区别(⭐⭐⭐)

  1. 从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。

  2. 从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。

  3. 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。

  4. 从变量是否有默认值来看,成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

总结

  1. 成员变量是属于类(用static修饰)或者实例对象的,局部变量属于代码块或者方法
  2. 成员变量如果没有被赋初值,那么他会有默认值,而局部变量必须赋初值,否则会报错
  3. 成员变量随着对象的创建而存在,局部变量随方法的调用自动消失。

12. 面向对象三大特性是什么?并解释这三大特性

面向对象的三大特性:封装、继承和多态。

封装:封装是指把一个对象的属性隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。(最最常见的就是getter、setter)好处是对属性的统一管理,防止对象属性在未知情况下恶意篡改,所有属性的获取和修改必须通过统一的方法。

继承:将多个相似的类的特性抽离出来,创建一个共同的父类,填入共同的属性。这样可以快速的创建新的类,提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。(implements Runnable实现Runnable接口或者extends Thread,编写多线程程序)

关于继承如下 3 点请记住

  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法(即方法的重写)。

多态

多态是同一个行为具有多个不同表现形式或形态的能力。多态就是同一个接口,使用不同的实例而执行不同操作。

最常见的就是:

        List list = new ArrayList();
        List list1 = new LinkedList();

对于同一个List接口,创建不同类型的List实例。

13. String、StringBuffer 和 StringBuilder 的区别(⭐⭐⭐⭐)

简单的来说:String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以String 对象是不可变的

String 中的对象是不可变的,也就可以理解为常量,线程安全StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。

每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。

总结:String底层是final修饰的char[]数组,线程安全,每次修改创建一个新String对象;StringBuffer对方法加synchronized锁,线程安全,每次修改对对象本身进行操作;StringBuilder未加锁,线程不安全,其他与StringBuffer相同。

14.Java 异常(⭐⭐⭐)

在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception(异常)Error(错误)。Exception 能被程序本身处理(try-catch), Error 是无法处理的(只能尽量避免)。

常见的Exception

  1. NullPointerException
  2. NumberFormatException(字符串转换为数字)
  3. ArrayIndexOutOfBoundsException(数组越界)
  4. ArithmeticException(算术错误)

常见的Error

  1. StackOverflowError(栈溢出错误)

  2. OutOfMemoryError:Java heap space堆内存溢出

  3. OutOfMemoryError:GC overhead limit exceeded:GC回收时间过长时会抛出OutOfMemroyError。过长的定义是,超过98%的时间用来做GC并且回收了不到2%的堆内存,连续多次GC都只回收了不到2%的极端情况下才会抛出。假如不抛出 GC overhead limit 错误会发生什么情况呢?那就是GC清理的这么点内存很快会再次填满,迫使Gc再次执行.这样就形成恶性循环,CPU使用率一直是100%,而GC却没有任何成果。

  4. OutOfMemoryError:unable to create new native thread(重要):高并发请求服务器时,经常出现如下异常:java.lang.OutOfMemoryError: unable to create new native thread,准确的讲该native thread异常与对应的平台有关

导致原因:
1) 你的应用创建了太多线程了,一个应用进程创建多个线程,超过系统承载极限
2) 你的服务器并不允许你的应用程序创建这么多线程, Linux系统默认允许单个进程可以创建的线程数是1024个,你的应用创建超过这个数量,就会报java.Lang.OutOfMNemoryError: unable to create new native thread
解决办法:
1) 想办法降低你应用程序创建线程的数量,分析应用是否真的需要创建这么多线程,如果不是,改代码将线程数降到最低
2) 对于有的应用,确实需要创建很多线程,远超过Linux系统的默认1024个线程的限制,可以通过修改Uinux服务器配置,扩大linux默认限制

  1. OutOfMemoryError:MetaSpaceOutOfMemoryError:MetaSpace:

JVM参数

-XX : Metaspacesize=8m
-XX:MaxMetaspacesize=8m

Java 8及之后的版本使用Metaspace来替代永久代。

Metaspace是方法区在HotSpot中的实现,它与持久代最大的区别在于:Metaspace并不在虚拟机内存中而是使用本地内存也即在java8中, class metadata(the virtual machines internal presentation of Java class),被存储在叫做 Metaspace 的native memory

永久代(java8后被原空间Metaspace取代了)存放了以下信息:

  • 虚拟机加载的类信息
  • 常量池
  • 静态变量
  • 即时编译后的代码
    模拟Metaspace空间溢出,我们不断生成类往元空间灌,类占据的空间总是会超过Metaspace指定的空间大小的。

15. 序列化和反序列化(⭐⭐)

如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。

简单来说,序列化就是将数据结构或对象转换成二进制字节流的过程;而反序列化就是将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程。

序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。

对于不想进行序列化的变量,使用 transient 关键字修饰。transient 只能修饰变量,不能修饰类和方法。

16. 反射(⭐⭐)面试官可能会问你什么是反射,它的优缺点是什么,有哪些应用场景。

通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。

应用场景:像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。这些框架中大量使用了动态代理,而动态代理的实现也依赖反射;另外java中的注解其实也用到了反射。

反射的优缺点

优点 : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利

缺点 :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。

17. List、Set、 Map 的区别。(⭐⭐)

List:存储元素有序、可重复
Set:元素无序,不可重复
Map:存储kv形式数据,key不可重复,value可重复,无序

18. ArrayList 和 LinkedList 的区别(⭐⭐⭐⭐)

ArrayList

  1. 线程不安全
  2. 底层是数组
  3. 插入和删除受到元素位置的影响
  4. 支持元素的随机访问,因为元素位置连续
  5. 空间浪费体现在尾部空间,因为ArrayList底层是数组,存储元素个数肯定小于等于当前数组长度。

LinkedList

  1. 线程不安全
  2. 底层是双向链表
  3. 插入和删除元素不受元素位置影响
  4. 不支持元素随机访问
  5. 空间浪费体现在每个元素都有一个前指针一个后指针

19. 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同(⭐⭐⭐)

  1. HashSet、LinkedHashSet 和 TreeSet 都是 Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。

  2. HashSet、LinkedHashSet 和 TreeSet 的主要区别在于底层数据结构不同。HashSet 的底层数据结构是哈希表(基于 HashMap 实现)。LinkedHashSet 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。TreeSet 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。

  3. HashSet 用于不需要保证元素插入和取出顺序的场景,LinkHashSet 用于保证元素的插入和取出顺序满足 FIFO 的场景,TreeSet 用于支持对元素自定义排序规则的场景。

20. HashMap 多线程操作导致死循环问题(⭐⭐⭐)

此情况发生在JDK1.7之前,因为JDK1.7的HashMap是采用的头插法,那么在数组扩容之后,需要将原来的数据迁移到新数组,会导致数组的顺序进行反转,原来在头部的元素会在数组的尾部。

在插入元素的过程中,如果是多线程的情况下,有可能会存在以下情况:线程1和线程2同时触发了resize操作,同时进行数组扩容的操作。线程1读取了HashMap的数据,即将进行扩容,此时A.next = B;线程2先于线程1将HashMap扩容完成了,因为扩容之后链表会反转,此时的B.next = A;线程1再次执行时,执行A.next = B没问题,但是读取B后,调用B.next会发现B.next指向了A,此时就形成了循环链表。

总结:线程1读取A.next = B,线程1暂停;线程2将HashMap扩容完成,采用的头插法,此时B.next = A;线程1启动,读取B,发现B.next = A,A.next = B,形成了循环链表。

JDK1.8采用尾插法,解决了这个问题。但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap。(数据覆盖是发生在两个线程同时进行Hash运算,线程1计算完Hash后被挂起,然后线程2将值put进去,然后线程1执行,因为线程1已经进行过Hash运算,就直接将线程2设置的值给覆盖

21. HashMap 的长度为什么是 2 的幂次方(⭐⭐⭐)

为了能让 HashMap 存取高效,尽量较少碰撞,要尽量把数据分配均匀。数组下标的计算方法是"(n - 1) & hash",n是数组长度。这里不用取余(%)是因为位运算效率比取余效率高,另外如果n是2的幂次方,n-1的二进制的所有位都是1,这样在位运算时与取余运算的运算结果是一致的。

22. HashMap、HashTable、以及 ConcurrentHashMap 的区别 超级重要,基本必问(⭐⭐⭐⭐⭐)

现在面试的超高频考点。当面试官问到这个问题的时候,展现你背面试八股文能力的机会来了。你可以展开去讲在 Java7 和 Java8 中 HashMap 分别采用什么数据结构,为什么 Java8 把之前的头插法改成了尾插法,怎样实现扩容,为什么负载因子是 0.75,为什么要用红黑树等等一系列的东西。

HashTable

  1. 线程安全,因为内部都用synchronized修饰
  2. 效率没有HashMap高
  3. 不允许有 null 键和 null 值,否则会抛出 NullPointerException
  4. 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1
  5. 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小
  6. 底层数据结构是Entry数组

HashMap

  1. 线程不安全
  2. 效率比HashTable高
  3. 可以有 null 的 key 和 value,但是null key只能有一个。
  4. 默认初始大小为16,这是一个经验值,太小了就有可能频繁发生扩容,影响效率。太大了又浪费空间,不划算。当前元素个数大于数组长度*0.75就会触发扩容,JDK1.7是先扩容,再插入数据,多线程情况下可能存在循环链表和数据丢失的问题;JDK1.8是先插入数据,再扩容,多线程情况下可能存在数据丢失问题。
  5. 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。为什么扩容是原来的2倍…见21。
  6. JDK1.7底层数据结构是数组+链表;JDK1.8底层数据结构是数组+链表+红黑树。数组长度小于64且链表长度大于8时,会先扩容数组;当数组长度大于64且链表长度大于8时会将链表转为红黑树减少搜索时间。链表长度大于等于8时转成红黑树正是遵循泊松分布。根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。
  7. HashMap处理hash碰撞:1.7使用链表,1.8使用链表+红黑树,链表时间复杂度为O(n),红黑树时间复杂度为O(logn),红黑树是不严谨的AVL树。但是查找、插入、删除效率都比较高
  8. Hash计算规则:

return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
将hashcode右移16位相当于将高16位移入到低16位,再与原hashcode做异或计算(位相同为0,不同为1)可以将高低位二进制特征混合起来 => 高16位没有发生变化,但是低16位改变了。拿到的hash值会参与hashmap中数组槽位的计算,计算公式:(n - 1) & hash。
高区的16位很有可能会被数组槽位数的二进制码锁屏蔽,如果我们不做刚才移位异或运算,那么在计算槽位时将丢失高区特征。

ConcurrentHashMap

  1. 什么是快速失败(fail—fast)?什么是安全失败(fail—safe)?

快速失败(fail—fast) 是java集合中的一种机制, 在用迭代器遍历一个集合对象时,如果遍历过程中对集合对象的内容进行了修改(增加、删除、修改),则会抛出Concurrent Modification Exception。

迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。
集合在被遍历期间如果内容发生变化,就会改变modCount的值。
每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。(这里有一点类似于CAS的思想
Tip:这里异常的抛出条件是检测到 modCount!=expectedmodCount 这个条件。如果集合发生变化时修改modCount值刚好又设置为了expectedmodCount值,则异常不会抛出。
因此,不能依赖于这个异常是否抛出而进行并发操作的编程,这个异常只建议用于检测并发修改的bug。
java.util包下的集合类都是快速失败的,不能在多线程下发生并发修改(迭代过程中被修改)算是一种安全机制吧。

安全失败(fail—safe) ,java.util.concurrent包下的容器都是安全失败,可以在多线程下并发使用,并发修改。(采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。因此在开始遍历后也不会对遍历过程中发生修改的元素敏感)

JDK1.7的ConcurrentHashMap底层数据结构
image.png

如图所示,是由 Segment 数组、HashEntry 组成,和 HashMap 一样,仍然是数组加链表。Segment 是 ConcurrentHashMap 的一个内部类,主要的组成如下:

static final class Segment<K,V> extends ReentrantLock implements Serializable {
    private static final long serialVersionUID = 2249069246763182397L;
    // 和 HashMap 中的 HashEntry 作用一样,真正存放数据的桶
    transient volatile HashEntry<K,V>[] table;
    transient int count;
    // 记得快速失败(fail—fast)么?
    transient int modCount;
    // 大小
    transient int threshold;
    // 负载因子
    final float loadFactor;
}

HashEntry跟HashMap差不多的,但是不同点是,他使用volatile去修饰了他的数据Value还有下一个节点next。

原理上来说,ConcurrentHashMap 采用了分段锁技术,其中 Segment 继承于 ReentrantLock。不会像 HashTable 那样不管是 put 还是 get 操作都需要做同步处理,理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。就是说如果容量大小是16他的并发度就是16,可以同时允许16个线程操作16个Segment而且还是线程安全的。

JDK1.7的put:
他先定位到Segment,然后再进行put操作。首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁。如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
总结:先自旋,达到了MAX_SCAN_RETRIES则改为阻塞锁,保证获取成功。

JDK1.7的get:直接获取,因为用了volatile,所以每次获取的都是最新的值。

缺点:因为基本上还是数组加链表的方式,我们去查询的时候,如果链表很长,还得遍历链表,会导致效率很低。

JDK1.8的ConcurrentHashMap底层数据结构
采用了 CAS + synchronized 来保证并发安全性。跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,并且也引入了红黑树,在链表大于一定值的时候会转换(默认是8)。

JDK1.8的put:

  1. 根据 key 计算出 hashcode 。
  2. 判断是否需要进行初始化。
  3. 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。
  4. 如果当前位置的 hashcode == MOVED == -1,则需要进行扩容。
  5. 如果都不满足,则利用 synchronized 锁写入数据。
  6. 如果数量大于 TREEIFY_THRESHOLD 则要转换为红黑树。

JDK1.8的get:

  1. 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  2. 如果是红黑树那就按照树的方式获取值。
  3. 就不满足那就按照链表的方式遍历获取值
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值