Android 高频面试之必考Java基础

若是你们去面Android客户端岗位,那么必问Java基础和Kotlin基础,因此,我打算花3,4篇文章的样子来给你们总结下Android面试中会问到的一些Java基础知识。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能够禁止指令重排序。

相关文章
相关标签/搜索