Android 高频面试必问之 Java 基础,安卓 framework 开发
多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。即同一消息可以根据发送对象的不同而采取不同的行为方式。
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 在发生异常时,会自动释放线程占有的锁,因此不会导致死锁现象发生。
不能响应中断。
同一时刻不管是读还是写都只能有一个线程对共享资源操作,其他线程只能等待,性能不高。
评论