关于java:Android-高频面试之必考Java基础

10次阅读

共计 16524 个字符,预计需要花费 42 分钟才能阅读完成。

如果大家去面 Android 客户端岗位,那么必问 Java 根底和 Kotlin 根底,所以,我打算花 3,4 篇文章的样子来给大家总结下 Android 面试中会问到的一些 Java 基础知识。

1,面向对象和面向过程的区别

面向过程 :面向过程性能比面向对象高。因为对象调用须要实例化,开销比拟大,较耗费资源,所以当性能是最重要的考量因素的时候,比方单片机、嵌入式开发、Linux/Unix 等,个别采纳面向过程开发。然而,面向过程没有面向对象易保护、易复用、易扩大。
面向对象:面向对象易保护、易复用、易扩大。因为面向对象有封装、继承、多态性的个性,所以可设计出低耦合的零碎,使得零碎更加灵便、更加易于保护。

那为什么,面向过程性能比面向对象高呢?
面向过程也须要分配内存,计算内存偏移量,Java 性能差的次要起因并不是因为它是面向对象语言,而是因为 Java 是半编译语言,最终的执行代码并不是能够间接被 CPU 执行的二进制机器码。而面向过程语言大多都是间接编译成机器码在电脑上执行,并且其它一些面向过程的脚本语言性能也并不一定比 Java 好。

2,面向对象的特色有哪些

  • 封装:通常认为封装是把数据和操作数据的办法绑定起来,对数据的拜访只能通过已定义的接口。
  • 继承:继承是从已有类失去继承信息创立新类的过程。提供继承信息的类被称为父类(超类、基类);失去继承信息的类被称为子类(派生类)。
  • 形象:形象是将一类对象的独特特色总结进去结构类的过程,包含数据抽象和行为形象两方面。形象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
  • 多态性:多态性是指容许不同子类型的对象对同一音讯作出不同的响应。即同一音讯能够依据发送对象的不同而采取不同的行为形式。

3,解释下 Java 的编译与解释并存的景象

当 .class 字节码文件通过 JVM 转为机器能够执行的二进制机器码时,JVM 类加载器首先加载字节码文件,而后通过解释器逐行进行解释执行,这种形式的执行速度绝对比较慢。而且有些办法和代码块是重复被调用的(也就是所谓的热点代码),所以前面引进了 JIT 编译器,而 JIT 属于运行时编译。当 JIT 编译器实现一次编译后,会将字节码对应的机器码保留下来,下次能够间接调用。这也解释了咱们为什么常常会说 Java 是编译与解释共存的语言。

4,简略介绍下 JVM 的内存模型

Java 虚拟机所治理的内存蕴含程序计数器、Java 虚拟机栈、本地办法栈、Java 堆和办法区 5 个局部,模型图如下图所示。

4.1 程序计数器

因为 Java 虚拟机的多线程是通过线程轮流切换、调配处理器执行工夫的形式来实现的,在任何一个确定的时刻,一个处理器只会执行一条线程中的指令。为了线程切换后能复原到正确的执行地位,每条线程都须要有一个独立的程序计数器,各个线程之间的计数器互不影响,独立存储,这类内存区域为【线程公有】的内存。

程序计数器具备如下的特点:

  • 是一块较小的内存空间。
  • 线程公有,每条线程都有本人的程序计数器。
  • 生命周期方面,随着线程的创立而创立,随着线程的完结而销毁。
  • 是惟一一个不会呈现 OutOfMemoryError 的内存区域。

4.2 Java 虚拟机栈

Java 虚拟机栈也是线程公有的,它的生命周期与线程的生命周期同步,虚拟机栈形容的是 Java 办法执行的线程内存模型。每个办法被执行的时候,Java 虚拟机都会同步创立一个内存块,用于存储在该办法运行过程中的信息,每个办法被调用的过程都对应着一个栈帧在虚拟机中从入栈到出栈的过程。

Java 虚拟机栈有如下的特点:

  • 局部变量表所需的内存空间在编译期间实现调配,进入一个办法时,这个办法须要在栈帧中调配的局部变量空间是齐全确定的,在办法运行期间不会扭转局部变量表的大小。
  • Java 虚拟机栈会呈现两种异样:StackOverflowError 和 OutOfMemoryError。

4.3 本地办法栈

本地办法栈与虚拟机所施展的作用很类似,区别在于虚拟机栈为虚拟机执行 Java 办法服务,而本地办法栈则是为虚拟机应用到的本地办法服务。

4.4 Java 堆

Java 堆是虚拟机所治理的内存中最大的一块,Java 堆是被所有线程共享的一块内存区域,在虚拟机启动时创立。
此内存区域的惟一目标就是寄存对象实例,java 中“简直”所有的对象实例都在这里分配内存。这里应用“简直”是因为 java 语言的倒退,及时编译的技术倒退,逃逸剖析技术的日渐弱小,栈上调配、标量替换等优化伎俩,使 java 对象实例都调配在堆上变得不那么相对。
Java 堆是垃圾收集器治理的次要区域,因而很多时候也被称做“GC 堆”。从内存回收的角度来看,因为当初收集器根本都采纳分代收集算法(G1 之后开始变得不一样,引入了 region,然而仍旧采纳了分代思维),Java 堆中还能够细分为:新生代和老年代。再粗疏一点的有 Eden 空间、From Survivor 空间、ToSurvivor 空间等。从内存调配的角度来看,线程共享的 Java 堆中可能划分出多个线程公有的调配缓冲区(Thread Local Allocation Buffer,简写 TLAB)。

OOM 异样
Java 堆的大小既能够固定也能够扩大,然而支流的虚拟机,堆的大小都是反对扩大的。如果须要线程申请分配内存,但堆已满且内存已无奈再扩大时,就抛出 OutOfMemoryError 异样。比方:

/**
 * VM Args:-Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError
 */
public class HeapOOMTest {

    public static final int _1MB = 1024 * 1024;

    public static void main(String[] args) {List<Integer[]> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {Integer[] ints = new Integer[2 * _1MB];
            list.add(ints);
        }
    }
}

4.5 办法区

办法区和 Java 堆一样,是各个线程共享的内存区域,他用于存储已被虚拟机加载的类信息、常量、动态变量、即时编译器编译后的代码缓存等数据。

在 HotSpot JVM 中,永恒代(永恒代实现办法区)中用于寄存类和办法的元数据以及常量池,比方 Class 和 Method。每当一个类首次被加载的时候,它的元数据都会放到永恒代中。永恒代是有大小限度的,因而如果加载的类太多,很有可能导致永恒代内存溢出,为此咱们不得不对虚拟机做调优。

起初 HotSpot 放弃永恒代(PermGen),jdk1.7 版本中,HotSpot 曾经把本来放在永恒代的字符串常量池、动态变量等移出,到了 jdk1.8,齐全废除了永恒代,办法区移至元空间(Metaspace)。比方类元信息、字段、动态属性、办法、常量等都挪动到元空间区。元空间的实质和永恒代相似,都是对 JVM 标准中办法区的实现。不过元空间与永恒代之间最大的区别在于:元空间并不在虚拟机中,而是应用本地内存。因而,默认状况下,元空间的大小仅受本地内存限度。

罕用的 JVM 调参如下表:

参数 作用形容
-XX:MetaspaceSize 调配给 Metaspace(以字节计)的初始大小。如果不设置的话,默认是 20.79M,这个初始大小是触发首次 Metaspace Full GC 的阈值,例如 -XX:MetaspaceSize=256M
-XX:MaxMetaspaceSize 调配给 Metaspace 的最大值,超过此值就会触发 Full GC,此值默认没有限度,但应取决于零碎内存的大小。JVM 会动静地扭转此值。然而线上环境倡议设置,例如 -XX:MaxMetaspaceSize=256M
-XX:MinMetaspaceFreeRatio 最小闲暇比,当 Metaspace 产生 GC 后,会计算 Metaspace 的闲暇比,如果闲暇比 (闲暇空间 / 以后 Metaspace 大小) 小于此值,就会触发 Metaspace 扩容。默认值是 40,也就是 40%,例如 -XX:MinMetaspaceFreeRatio=40
-XX:MaxMetaspaceFreeRatio 最大闲暇比,当 Metaspace 产生 GC 后,会计算 Metaspace 的闲暇比,如果闲暇比 (闲暇空间 / 以后 Metaspace 大小) 大于此值,就会触发 Metaspace 开释空间。默认值是 70,也就是 70%,例如 -XX:MaxMetaspaceFreeRatio=70

运行时常量池
运行时常量池是办法区的一部分,Class 文件中除了有类的版本、字段、办法、接口等形容信息外,还有一项信息是常量池表,用于寄存编译期间生成的各种字面量与符号援用,这部分内容将在类加载后寄存到办法区的运行时常量池中。
办法区中寄存:类信息、常量、动态变量、即时编译器编译后的代码。常量就寄存在运行时常量池中。
当类被 Java 虚拟机加载后,.class 文件中的常量就寄存在办法区的运行时常量池中。而且在运行期间,能够向常量池中增加新的常量。如 String 类的 intern()办法就能在运行期间向常量池中增加字符串常量。

4.6 间接内存

间接内存并不是虚拟机运行时数据区的组成部分,在 NIO 中引入了一种基于通道和缓冲的 IO 形式。它能够通过调用本地办法间接调配 Java 虚拟机之外的内存,而后通过一个存储在堆中的 DirectByteBuffer 对象间接操作该内存,而无须先将内部内存中的数据复制到堆中再进行操作,从而进步了数据操作的效率。

因为间接内存并非 Java 虚拟机的组成部分,因而间接内存的大小不受 Java 虚拟机管制,但既然是内存,如果内存不足时还是会抛出 OutOfMemoryError 异样。

上面是间接内存与堆内存的一些异同点:

  • 间接内存申请空间消耗更高的性能;
  • 间接内存读取 IO 的性能要优于一般的堆内存。
  • 间接内存作用链:本地 IO -> 间接内存 -> 本地 IO
  • 堆内存作用链:本地 IO -> 间接内存 -> 非间接内存 -> 间接内存 -> 本地 IO

服务器管理员在配置虚拟机参数时,会依据理论内存设置 -Xmx 等参数信息,但常常疏忽间接内存,使得各个内存区域总和大于物理内存限度,从而导致动静扩大时呈现 OutOfMemoryError 异样。

5,简略介绍下 Java 的类加载器

Java 的类加载器能够分为 BootstrapClassLoader、ExtClassLoader 和 AppClassLoader,它们的作用如下。

  • BootstrapClassLoader:Bootstrap 类加载器负责加载 rt.jar 中的 JDK 类文件,它是所有类加载器的父加载器。Bootstrap 类加载器没有任何父类加载器,如果调用 String.class.getClassLoader(),会返回 null,任何基于此的代码会抛出 NUllPointerException 异样,因而 Bootstrap 加载器又被称为初始类加载器。
  • ExtClassLoader:Extension 将加载类的申请先委托给它的父加载器,也就是 Bootstrap,如果没有胜利加载的话,再从 jre/lib/ext 目录下或者 java.ext.dirs 零碎属性定义的目录下加载类。Extension 加载器由 sun.misc.Launcher$ExtClassLoader 实现。
  • AppClassLoader:Java 默认的加载器就是 System 类加载器,又叫作 Application 类加载器。它负责从 classpath 环境变量中加载某些利用相干的类,classpath 环境变量通常由 -classpath 或 -cp 命令行选项来定义,或者是 JAR 中的 Manifest 的 classpath 属性,Application 类加载器是 Extension 类加载器的子加载器。

类加载会波及一些加载机制。

  • 委托机制:加载工作委托交给父类加载器,如果不行就向下传递委托工作,由其子类加载器加载,保障 Java 外围库的安全性。
  • 可见性机制:子类加载器能够看到父类加载器加载的类,而反之则不行。
  • 单一性准则:父加载器加载过的类不能被子加载器加载第二次。

6,谈一下 Java 的垃圾回收,以及罕用的垃圾回收算法。

Java 的内存治理次要波及三个局部:堆 (Java 代码可及的 Java 堆 和 JVM 本身应用的办法区)、栈 (服务 Java 办法的虚拟机栈 和 服务 Native 办法的本地办法栈) 和 保障程序在多线程环境下可能间断执行的程序计数器。
Java 堆是进行垃圾回收的次要区域,故其也被称为 GC 堆;而办法区的垃圾回收次要针对的是新生代和中生代。总的来说,堆 (包含 Java 堆 和 办法区)是 垃圾回收的次要对象,特地是 Java 堆。

6.1 垃圾回收算法

6.1.1 对象存活判断

援用计数

每个对象有一个援用计数属性,新增一个援用时计数加 1,援用开释时计数减 1,计数为 0 时能够回收。此办法尽管简略,但无奈解决对象互相循环援用的问题。

可达性剖析

从 GC Roots 开始向下搜寻,搜寻所走过的门路称为援用链,当一个对象到 GC Roots 没有任何援用链相连时,则证实此对象是不可用的。在 Java 中,GC Roots 包含:

  • 虚拟机栈中援用的对象。
  • 办法区中类动态属性实体援用的对象。
  • 办法区中常量援用的对象。
  • 本地办法栈中 JNI 援用的对象。

    6.2 垃圾收集算法

    标记革除法

如它的名字一样,算法分为“标记”和“革除”两个阶段:首先标记出所有须要回收的对象,在标记实现后对立回收掉所有被标记的对象。之所以说它是最根底的收集算法,是因为后续的收集算法都是基于这种思路并对其毛病进行改良而失去的。
标记简单算法有两个次要的毛病:一个是效率问题,标记和革除过程的效率都不高;另外一个是空间问题,标记革除之后会产生大量不间断的内存碎片,空间碎片太多可能会导致,当程序在当前的运行过程中须要调配较大对象时无奈找到足够的间断内存而不得不提前触发另一次垃圾收集动作。

复制算法

复制的收集算法,它将可用内存按容量划分为大小相等的两块,每次只应用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块下面,而后再把已应用过的内存空间一次清理掉。
它的长处是每次只须要对其中的一块进行内存回收,内存调配时也就不必思考内存碎片等简单状况,只有挪动堆顶指针,按程序分配内存即可,实现简略,运行高效。而毛病也是不言而喻的,内存放大为原来的一半,继续复制长生存期的对象则导致效率升高。

标记整顿法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更要害的是,如果不想节约 50% 的空间,就须要有额定的空间进行调配担保,以应答被应用的内存中所有对象都 100% 存活的极其状况,所以在老年代个别不能间接选用这种算法。
依据老年代的特点,有人提出了另外一种“标记 - 整顿”(Mark-Compact)算法,标记过程依然与“标记 - 革除”算法一样,但后续步骤不是间接对可回收对象进行清理,而是让所有存活的对象都向一端挪动,而后间接清理掉端边界以外的内存。

分代收集算法
分代收集算法,就是把 Java 堆分为新生代和老年代,这样就能够依据各个年代的特点采纳最适当的收集算法。在新生代中,每次垃圾收集时都发现有少量对象死去,只有大量存活,那就选用复制算法,只须要付出大量存活对象的复制老本就能够实现收集。而老年代中因为对象存活率高、没有额定空间对它进行调配担保,就必须应用“标记 - 清理”或“标记 - 整顿”算法来进行回收。

7,成员变量和局部变量的区别

  • 从语法模式上看:成员变量是属于类的,而局部变量是在办法中定义的变量或是办法的参数;成员变量能够被 public、private、static 等修饰符所润饰,而局部变量不能被这些修饰符所润饰;然而它们都能够被 final 所润饰。
  • 从变量在内存中的存储形式来看:如果成员变量被 static 所润饰,那么这个成员变量属于类,如果没有被 static 润饰,则该成员变量属于对象实例。对象存在于堆内存,局部变量存在于栈内存(具体是 Java 虚拟机栈)。
  • 从变量在内存中的生存工夫来看:成员变量是对象的一部分,它随着对象的创立而存在,而局部变量随着办法的调用完结而主动隐没。
  • 成员变量如果没有赋初始值,则会主动以类型的默认值而赋值(例外:被 final 润饰的成员变量必须在初始化时赋值),局部变量则不会主动赋值。

8,Java 中的办法重写 (Overriding) 和办法重载 (Overload) 的含意

办法重写
在 Java 程序中,类的继承关系能够产生一个子类,子类继承父类,它具备了父类所有的特色,继承了父类所有的办法和变量。子类能够定义新的特色,当子类须要批改父类的一些办法进行扩大,增大性能,程序设计者经常把这样的一种操作方法称为重写,也叫称为覆写或笼罩。

办法重写有如下一些特点:

  • 办法名,参数列表必须雷同,返回类型能够雷同也能够是原类型的子类型
  • 重写办法不能比原办法拜访性差(即拜访权限不容许放大)。
  • 重写办法不能比原办法抛出更多的异样。
  • 重写产生在子类和父类之间。
  • 重写实现运行时的多态性。

办法重载
办法重载是让类以对立的形式解决不同类型数据的一种伎俩。调用办法时通过传递给它们的不同个数和类型的参数来决定具体应用哪个办法,这就是多态性。所谓办法重载是指在一个类中,多个办法的办法名雷同,然而参数列表不同。参数列表不同指的是参数个数、参数类型或者参数的程序不同。

  • 办法名必须雷同,参数列表必须不同(个数不同、或类型不同、参数类型排列程序不同等)。
  • 办法的返回类型能够雷同也能够不雷同。
  • 重载产生在同一类中。
  • 重载实现编译时的多态性。

9,简略介绍下传递和援用传递

按值传递:值传递是指在调用函数时将理论参数复制一份传递到函数中,这样在函数中如果对参数进行批改,将不会影响到理论参数。简略来说就是间接复制了一份数据过来,因为是间接复制,所以这种形式在传递时如果数据量十分大的话,运行效率天然就变低了,所以 Java 在传递数据量很小的数据是值传递,比方 Java 中的各种根本类型:int、float、double、boolean 等类型。

援用传递:援用传递其实就补救了下面说的有余,如果每次传参数的时候都复制一份的话,如果这个参数占用的内存空间太大的话,运行效率会很底下,所以援用传递就是间接把内存地址传过来,也就是说援用传递时,操作的其实都是源数据,这样的话批改有时候会抵触,记得用逻辑补救下就好了,具体的数据类型就比拟多了,比方 Object,二维数组,List,Map 等除了根本类型的参数都是援用传递。

10,为什么重写 equals 时必须重写 hashCode 办法

上面是应用 hashCode()与 equals()的相干规定:

  • 如果两个对象相等(即用 equals 比拟返回 true),则 hashcode 肯定也是雷同的;
  • 两个对象有雷同的 hashcode 值,它们也不肯定是相等的(不同的对象也可能产生雷同的 hashcode,概率性问题);
  • equals 办法被笼罩过,则 hashCode 办法也必须被笼罩。

为什么必须要重写 hashcode 办法?其实就是为了保障同一个对象,保障在 equals 雷同的状况下 hashcode 值必然雷同,如果重写了 equals 而未重写 hashcode 办法,可能就会呈现两个没有关系的对象 equals 雷同的(因为 equals 都是依据对象的特色进行重写的),但 hashcode 的确不雷同的。

11,接口和抽象类的区别和相同点是什么

相同点

  • 接口是相对形象的,不能够被实例化,抽象类也不能够被实例化。
  • 类能够不实现抽象类和接口申明的所有办法,当然,在这种状况下,类也必须得申明成是形象的。

异同点:

  • 从设计层面来说,形象是对类的形象,是一种模板设计,接口是行为的形象,是一种行为的标准。
  • 定义接口的关键字是 interface,抽象类的关键字是 abstract class
  • 接口中所有的办法隐含的都是形象的。而抽象类则能够同时蕴含形象和非形象的办法。
  • 类能够实现很多个接口,然而只能继承一个抽象类,接口能够继承多个接口
  • Java 接口中申明的变量默认都是 public static final 的。抽象类能够蕴含非 final 的变量。
  • 在 JDK1.8 之前,接口中不能有静态方法,抽象类中能够有一般办法和静态方法;在 JDK1.8 后,接口中能够有默认办法和静态方法,并且有办法体。
  • 抽象类能够有构造方法,然而不能间接被 new 关键字实例化。
  • 在 JDK1.8 前,抽象类的形象办法默认拜访权限为 protected,1.8 默认拜访权限为 default,共有 default,protected、public 三种修饰符,非形象办法能够应用四种修饰符;在 JDK1.8 前,接口办法默认为 public,1.8 时默认为 public,此时能够应用 public 和 default,1.9 时接口办法还反对 private。

12,简述下 HashMap

HashMap 底层采纳了数组 + 链表的数据结构,数组是 HashMap 的主体,链表则是次要为了解决哈希抵触而存在的。

如果定位到的数组地位不含链表,那么执行查找、增加等操作很快,仅需一次寻址即可;如果定位到的数组蕴含链表,对于增加操作,其工夫复杂度为 O(n),首先遍历链表,存在即笼罩,否则新增;对于查找操作来讲,仍需遍历链表,而后通过 key 对象的 equals 办法逐个比对查找。所以,性能思考,HashMap 中的链表呈现越少,性能才会越好。

HashMap 有 4 个结构器,其余结构器如果用户没有传入 initialCapacity 和 loadFactor 这两个参数,会应用默认值 initialCapacity 默认为 16,loadFactory 默认为 0.75。

public HashMap(int initialCapacity, float loadFactor) {// 此处对传入的初始容量进行校验,最大不能超过 MAXIMUM_CAPACITY = 1<<30(230)
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity:" +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor:" +
                                               loadFactor);

        this.loadFactor = loadFactor;
        threshold = initialCapacity;
     
        init();//init 办法在 HashMap 中没有理论实现,不过在其子类如 linkedHashMap 中就会有对应实现}

加载因子存在的起因,还是因为减缓哈希抵触,如果初始桶为 16,等到满 16 个元素才扩容,某些桶里可能就有不止一个元素了。所以加载因子默认为 0.75,也就是说大小为 16 的 HashMap,到了第 13 个元素,就会扩容成 32。

Put 过程

  • 判断以后数组是否要初始化。
  • 如果 key 为空,则 put 一个空值进去。
  • 依据 key 计算出 hashcode。
  • 依据 hsahcode 定位出在桶内的地位。
  • 如果桶是链表,则须要遍历判断 hashcode,如果 key 和原来的 key 是否相等,相等则进行笼罩,返回原来的值。
  • 如果桶是空的,阐明以后地位没有数据存入,新增一个 Entry 对象写入以后地位. 当调用 addEntry 写入 Entry 时须要判断是否须要扩容。如果须要就进行两倍裁减,并将以后的 key 从新 hash 并定位。而在 createEntry 中会将以后地位的桶传入到新建的桶中,如果以后桶有值就会在地位造成链表。

Get 过程

  • 依据 key 计算出 hashcode,并定位到桶内的地位。
  • 判断是不是链表,如果是则须要依据遍历直到 key 及 hashcode 相等时候就返回值,如果不是就依据 key、key 的 hashcode 是否相等来返回值。
  • 如果啥也没取到就返回 null。

JDK 1.8 的 HashMap 底层采纳的是链表 + 红黑树,减少一个阈值进行判断是否将链表转红黑树,HashEntry 批改为 Node,目标是解决 hash 抵触造成的链表越来越长、查问慢的问题。

Get 过程

  • 判断以后桶是不是空, 空就须要初始化;
  • 依据 key,计算出 hashcode,依据 hashcode,定位到具体的桶中,并判断以后桶是不是为空, 为空表明没有 hsah 抵触创立一个新桶即可;
  • 如果有 hash 抵触, 那么就要比拟以后桶中的 key、key 的 hashcode 与写入的 key 是否相等,相等就赋值给 e, 在第 8 步的时候会对立进行赋值及返回;
  • 如果以后地位是红黑树,就依照红黑树的形式写入数据;
  • 如果以后地位是链表, 则须要把 key,value 封装一个新的节点, 增加到以后的桶前面(尾插法), 造成链表;
  • 接着判断以后链表的大小是否大于预设的阈值,大于时就要转换为红黑树;
  • 如果在遍历过程中找到 key 雷同时间接退出遍历;
  • 如果 e != null 就相当于存在雷同的 key, 那就须要将值笼罩;
  • 最初判断是否须要进行扩容;

Get 过程

  • 首先将 key hash 之后获得所定位的桶。
  • 如果桶为空则间接返回 null。
  • 否则判断桶的第一个地位 (有可能是链表、红黑树) 的 key 是否为查问的 key,是就间接返回 value。
  • 如果第一个不匹配,则判断它的下一个是红黑树还是链表。
  • 红黑树就依照树的查找形式返回值。
  • 不然就依照链表的形式遍历匹配返回值。

13,CurrentHashMap

JDK8 中 ConcurrentHashMap 参考了 JDK8 HashMap 的实现,采纳了数组 + 链表 + 红黑树的实现形式来设计,外部大量采纳 CAS 操作,那什么是 CAS。

CAS 是 compare and swap 的缩写,中文称为【比拟替换】。CAS 是一种基于锁的操作,而且是乐观锁。在 Java 中锁分为乐观锁和乐观锁。乐观锁是将资源锁住,等一个之前取得锁的线程开释锁之后,下一个线程才能够拜访。而乐观锁采取了一种宽泛的态度,通过某种形式不加锁来解决资源,性能较乐观锁有很大的进步。

CAS 操作蕴含三个操作数 —— 内存地位(V)、预期原值(A)和新值(B)。如果内存地址外面的值和 A 的值是一样的,那么就将内存外面的值更新成 B。CAS 是通过有限循环来获取数据的,如果在第一轮循环中,a 线程获取地址外面的值被 b 线程批改了,那么 a 线程须要自旋,到下次循环才有可能机会执行。

14,介绍下什么是乐观锁、乐观锁

Java 依照锁的实现分为乐观锁和乐观锁,乐观锁和乐观锁并不是一种实在存在的锁,而是一种设计思维。

乐观锁
乐观锁是一种乐观思维,它总认为最坏的状况可能会呈现,它认为数据很可能会被其他人所批改,所以乐观锁在持有数据的时候总会把资源 或者 数据 锁住,这样其余线程想要申请这个资源的时候就会阻塞,直到等到乐观锁把资源开释为止。传统的关系型数据库里边就用到了很多这种锁机制,比方行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。乐观锁的实现往往依附数据库自身的锁性能实现。

Java 中的 Synchronized 和 ReentrantLock 等独占锁 (排他锁) 也是一种乐观锁思维的实现,因为 Synchronzied 和 ReetrantLock 不论是否持有资源,它都会尝试去加锁,惟恐本人可爱的宝贝被他人拿走。

乐观锁
乐观锁的思维与乐观锁的思维相同,它总认为资源和数据不会被他人所批改,所以读取不会上锁,然而乐观锁在进行写入操作的时候会判断以后数据是否被批改过(具体如何判断咱们上面再说)。乐观锁的实现计划一般来说有两种:版本号机制 和 CAS 实现。乐观锁多实用于多度的利用类型,这样能够进步吞吐量。

在 Java 中 java.util.concurrent.atomic 包上面的原子变量类就是应用了乐观锁的一种实现形式 CAS 实现的。

15,谈谈对 Java 线程的了解

线程是过程中可独立执行的最小单位,也是 CPU 资源(工夫片)调配的根本单位,同一个过程中的线程能够共享过程中的资源,如内存空间和文件句柄。线程有一些根本的属性,如 id、name、以及 priority。
id:线程 id 用于标识不同的线程,编号可能被后续创立的线程应用,编号是只读属性,不能批改。
name:线程的名称,默认值是 Thread-(id)
daemon:分为守护线程和用户线程,咱们能够通过 setDaemon(true) 把线程设置为守护线程。守护线程通常用于执行不重要的工作,比方监控其余线程的运行状况,GC 线程就是一个守护线程。setDaemon() 要在线程启动前设置,否则 JVM 会抛出非法线程状态异样,可被继承。
priority:线程调度器会依据这个值来决定优先运行哪个线程(不保障),优先级的取值范畴为 1~10,默认值是 5,可被继承。Thread 中定义了上面三个优先级常量:

  • 最低优先级:MIN_PRIORITY = 1
  • 默认优先级:NORM_PRIORITY = 5
  • 最高优先级:MAX_PRIORITY = 10

一个线程被创立后,会经验从创立到沦亡的状态,下图是线程状态的变更过程。

下表是展现了线程的生命周期状态变动:

状态 阐明
New 新创建了一个线程对象,但还没有调用 start()办法。
Runnable Ready 状态 线程对象创立后,其余线程(比方 main 线程)调用了该对象的 start() 办法。该状态的线程位于可运行线程池中,期待被线程调度选中 获取 cpu 的使用权。Running 绪状态的线程在取得 CPU 工夫片后变为运行中状态(running)。
Blocked 线程因为某种原因放弃了 cpu 使用权(期待锁),临时进行运行。
Waiting 线程进入期待状态因为以下几个办法:Object#wait()、Thread#join()、LockSupport#park()
Terminated 该线程曾经执行结束。

16,Synchronized、volatile、Lock 并发

线程同步和并发通常会问到 Synchronized、volatile、Lock 的作用。其中,Lock 是一个类,而其余两个则是 Java 关键字。

Synchronized

Synchronized 是 Java 的关键字,也是 Java 的内置个性,在 JVM 层面实现了对临界资源的同步互斥拜访,通过对对象的头文件来操作,从而达到加锁和开释锁的目标。应用 Synchronized 润饰的代码或办法,通常有如下个性:

  • Synchronized 在产生异样时,会主动开释线程占有的锁,因而不会导致死锁景象产生。
  • 不能响应中断。
  • 同一时刻不论是读还是写都只能有一个线程对共享资源操作,其余线程只能期待,性能不高。

正是因为下面的个性,所以 Synchronized 的毛病也是不言而喻的:即如果一个代码块被 synchronized 润饰了,当一个线程获取了对应的锁,并执行该代码块时,其余线程便只能始终期待,因而效率很低。

volatile
保障了不同线程对这个变量进行操作时的可见性,即一个线程批改了某个变量的值,这新值对其余线程来说是立刻可见的。并且 volatile 是禁止进行指令重排序。

所谓指令重排序,指的是处理器为了进步程序运行效率,可能会对输出代码进行优化,它不保障程序中各个语句的执行先后顺序同代码中的程序统一,然而它会保障程序最终执行后果和代码程序执行的后果是统一的。

volatile 为了保障原子性,必须具备以下条件:

  • 对变量的写操作不依赖于以后值
  • 该变量没有蕴含在具备其余变量的不变式中

17,锁

依照作用的不同,Java 的锁能够分为如下:

乐观锁、乐观锁

乐观锁认为本人在应用数据的时候肯定有别的线程来批改数据,因而在获取数据的时候会先加锁,确保数据不会被别的线程批改。Java 中,synchronized 关键字和 Lock 的实现类都是乐观锁。乐观锁适宜写操作多的场景,先加锁能够保障写操作时数据正确。

而乐观锁认为本人在应用数据时不会有别的线程批改数据,所以不会增加锁,只是在更新数据的时候去判断之前有没有别的线程更新了这个数据。如果这个数据没有被更新,以后线程将本人批改的数据胜利写入。如果数据曾经被其余线程更新,则依据不同的实现形式执行不同的操作(例如报错或者主动重试)。乐观锁在 Java 中是通过应用无锁编程来实现,最常采纳的是 CAS 算法,Java 原子类中的递增操作就通过 CAS 自旋实现。乐观锁适宜读操作多的场景,不加锁的特点可能使其读操作的性能大幅晋升。

这里说到了 CAS 算法,那么什么是 CAS 算法呢?

CAS 算法

一个线程失败或挂起并不会导致其余线程也失败或挂起,那么这种算法就被称为非阻塞算法。而 CAS 就是一种非阻塞算法实现,也是一种乐观锁技术,它能在不应用锁的状况下实现多线程平安,因而是一种无锁算法。

CAS 算法的定义:CAS 的次要作用是不应用加锁就能够实现线程平安,CAS 算法又称为比拟替换算法,是一种实现并发算法时罕用到的技术,Java 并发包中的很多类都应用了 CAS 技术。CAS 具体包含三个参数:以后内存值 V、旧的预期值 A、行将更新的值 B,当且仅当预期值 A 和内存值 V 雷同时,将内存值批改为 B 并返回 true,否则什么都不做,并返回 false。

原子更新的基本操作包含:

  • AtomicBoolean:原子更新布尔变量;
  • AtomicInteger:原子更新整型变量;
  • AtomicLong:原子更新长整型变量;

以 AtomicInteger 为例,代码如下:

public class AtomicInteger extends Number implements java.io.Serializable {
     // 返回以后的值
     public final int get() {return value;}
     // 原子更新为新值并返回旧值
     public final int getAndSet(int newValue) {return unsafe.getAndSetInt(this, valueOffset, newValue);
     }
     // 最终会设置成新值
     public final void lazySet(int newValue) {unsafe.putOrderedInt(this, valueOffset, newValue);
     }
     // 如果输出的值等于预期值,则以原子形式更新为新值
     public final boolean compareAndSet(int expect, int update) {return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
     }
     // 原子自增
     public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);
     }
     // 原子形式将以后值与输出值相加并返回后果
     public final int getAndAdd(int delta) {return unsafe.getAndAddInt(this, valueOffset, delta);
     }
 }

再如,上面是应用多线程对一个 int 值进行自增操作的代码,如下所示。

public class AtomicIntegerDemo {private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args){for (int i = 0; i < 5; i++){new Thread(new Runnable() {public void run() {
                    // 调用 AtomicInteger 的 getAndIncement 返回的是减少之前的值
                     System.out.println(atomicInteger.getAndIncrement());
                }
            }).start();}
        System.out.println(atomicInteger.get());
    }
}

自旋锁、适应性自旋锁

阻塞或唤醒一个 Java 线程须要操作系统切换 CPU 状态来实现,这种状态转换须要消耗处理器工夫。在许多场景中,同步资源的锁定工夫很短,为了这一小段时间去切换线程,线程挂起和复原现场的破费可能会让零碎得失相当。如果物理机器有多个处理器,可能让两个或以上的线程同时并行执行,咱们就能够让前面那个申请锁的线程不放弃 CPU 的执行工夫,看看持有锁的线程是否很快就会开释锁。

而为了让以后线程【稍等一下】,咱们需让以后线程进行自旋,如果在自旋实现后后面锁定同步资源的线程曾经开释了锁,那么以后线程就能够不用阻塞而是间接获取同步资源,从而防止切换线程的开销,这就是自旋锁。

死锁

以后线程领有其余线程须要的资源,以后线程期待其余线程已领有的资源,都不放弃本人领有的资源。

18,谈谈你对 Java 反射的了解

所谓反射,指的是在运行状态中,对于任意一个类,都可能获取这个类的所有属性和办法;对于任意一个对象,都可能调用它的任意一个办法和属性,而这种动静获取的信息以及动静调用对象的办法的性能就被称为 Java 语言的反射机制。

应用反射前须要当时获取到的字节码,在 Java 中,获取字节码的形式有三种:

  1. Class.forName(className)
  2. 类名.class
  3. this.getClass()

19,注解

Java 语言中的类、办法、变量、参数和包等都能够被标注。和 Javadoc 不同,Java 标注能够通过反射获取标注内容。依据作用机会的不同,Java 的注解能够分为三种:

  • SOURCE:注解将被编译器抛弃(该类型的注解信息只会保留在源码里,源码通过编译后,注解信息会被抛弃,不会保留在编译好的 class 文件里),如 @Override。
  • CLASS:注解在 class 文件中可用,但会被 VM 抛弃(该类型的注解信息会保留在源码里和 class 文件里,在执行的时候,不会加载到虚拟机中),请留神,当注解未定义 Retention 值时,默认值是 CLASS。
  • RUNTIME:注解信息将在运行期 (JVM) 也保留,因而能够通过反射机制读取注解的信息(源码、class 文件和执行的时候都有注解的信息),如 @Deprecated。

20,单例

为了保障只有一个对象存在,能够应用单例模式,网上有,单例模式的七种写法。咱们介绍一下常见的几种:

懒汉式
懒汉式应用的是 static 关键字,因而是线程不平安的。

public class Singleton {  
     private static Singleton instance;  
     private Singleton (){}   
     public static Singleton getInstance() {if (instance == null) {instance = new Singleton();  
     }  
    return instance;  
     }  
 }  

如果要线程平安,那么须要应用 synchronized 关键字。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}
    public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();  
     }  
   return instance;  
     }  
 }   

不过,应用 synchronized 锁住之后,运行效率明显降低。

动态外部类
动态外部类利用了 classloder 的机制来保障初始化 instance 时只有一个线程。

public class Singleton {  
   private static class SingletonHolder {private static final Singleton INSTANCE = new Singleton();  
    }  
   private Singleton (){}
   public static final Singleton getInstance() {return SingletonHolder.INSTANCE;}  
 }   

双重校验锁

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}   
    public static Singleton getSingleton() {if (singleton == null) {synchronized (Singleton.class) {if (singleton == null) {singleton = new Singleton();  
        }  
       }  
     }  
    return singleton;  
    }  
 }  

双重查看锁定是 synchronized 的降级的写法,那为什么要应用 volatile 关键字呢,是为了禁止初始化实例时的重排序。咱们晓得,初始化一个实例在 java 字节码中会有 4 个步骤:

  1. 申请内存空间
  2. 初始化默认值(区别于结构器办法的初始化)
  3. 执行结构器办法
  4. 连贯援用和实例

而后两步是有可能会重排序,而应用 volatile 能够禁止指令重排序。

正文完
 0