1、java的基本数据类型有哪些?
Java的基本数据类型包括以下八种:
boolean
: 用于存储逻辑值,只能存储true和false。byte
: 用于存储字节数据,占用8位(一个字节)内存空间。short
: 用于存储较短整数,占用16位(两个字节)内存空间。int
: 用于存储整数,占用32位(四个字节)内存空间。long
: 用于存储长整数,占用64位(八个字节)内存空间。float
: 用于存储单精度浮点数,占用32位(四个字节)内存空间。double
: 用于存储双精度浮点数,占用64位(八个字节)内存空间。char
: 用于存储单个字符,占用16位(两个字节)内存空间,采用Unicode编码。
2、java为什么要有包装类型?
主要原因包括以下几点:
- 处理基本数据类型的 null 值:基本数据类型(如 int,double 等)不能直接赋值为 null,而包装类型(如 Integer、Double)可以表示 null 值,这对于某些业务逻辑和数据处理来说非常有用。
- 提供额外功能:包装类型提供了一些额外的方法和功能,这些方法可以用于对基本数据类型进行操作和处理。例如,Integer 类提供了
parselnt()方法用于将字符串转换为整数。 - 泛型支持:泛型只能接受对象类型,无法直接使用基本数据类型。因此,使用包装类型可以很方使地在泛型中使用基本数据类型。
- 自动装箱与拆箱: Java 提供了自动装箱(autoboxing)和自动拆箱(unboxing)的功能,使得基本类型与其对应的包装类型之间的转换更加便捷。
- 与对象集合的兼容性: Java的集合类(如ArrayList, HashMap等)只能存储对象,不能直接存储基本数据类型。使用包装类型可以方便地将基本数据类型转换为对象类型,从而与集合类兼容。
3、String a = “123” 和 String a = new String(“123”) 区别?
String a = "123"; // a 是一个变量,对字符串常量池中“123”这个对象的引用
// "123" 是一个字符串字面量(字符串对象),它会被存储在字符串常量池中(String Constant Pool)。
// 当字符串常量池不存在“123”对象会创建一个对象,存在则不会创建
String b = new String("123"); // b 是一个变量,但它引用堆的 String 对象
// 这里,首先 JVM 会检查字符串常量池中是否已经有 "123" 这个字符串。
// 如果有,它会用常量池中的字符串对象作为参数去调用 String 类的构造函数来创建一个新的 String 对象。
// 这个新的 String 对象会被分配在堆内存中,并且它的内容(字符数组)会是常量池中字符串对象的一个拷贝。
// 变量 b 持有对这个新创建的堆上 String 对象的引用,而不是直接引用常量池中的字符串。
// 最多创建2个对象,最少一个
4、String、StringBuilder和StringBuffer的区别?
String、StringBuilder和StringBuffer是Java中用来处理字符串的类,它们之间的区别主要在于性能和线程安全性。
-
String: 一旦创建就不能被修改,任何对String的操作都会产生一个新的String对象。
不可变性使得String在并发环境下是线程安全的,但是频繁的字符串操作会产生大量临时对象,影响性能。 -
StringBuilder: StringBuilder是可变的,可以进行插入、追加、删除等操作而不会产生新的对象。
由于StringBuilder是非线程安全的,所以在单线程环境下比StringBuffer具有更好的性能。 -
StringBuffer: 与StringBuilder类似,也是可变的,但是它是线程安全的,所有的方法都是同步的。
在多线程环境下,为了确保线程安全性,可以使用StringBuffer,但性能相对较差,因为是通过在方法上面加synchronized 关键字来保证同步的。
因此,如果在单线程环境下需要频繁修改字符串,建议使用StringBuilder;如果在多线程环境下需要频繁修改字符串,则应该使用StringBuffer;如果字符串不需要被修改,那么使用String即可。
5、如何理解面向对象和面向过程?
面向过程是一种以过程为中心的编程方法,它将问题分解为一系列步骤或函数。每个函数负责完成一个特定的任务,通过依次调用这些函数来解决问题。
其优点包括:
- 流程清晰:按照步骤进行编程,易于理解和调试。
然而,它也存在一些局限性:
- 代码复用性差:功能封装在函数中,但难以复用和扩展。
- 维护困难:代码结构较为松散,修改可能影响多个部分。
面向对象则是一种以对象为中心的编程方法。它将问题抽象为对象,每个对象具有属性和行为。
其优点包括:
- 代码复用性高:通过继承和多态实现代码的重用和扩展。
- 维护方便:修改一个对象的行为不会影响其他部分。
- 模拟现实世界:更符合人类的思维方式。
面向对象编程的关键概念包括:
- 对象:表示现实世界中的实体。
- 类:对象的抽象描述。
- 封装:隐藏对象的内部实现,只暴露必要的接口。
- 继承:实现代码的重用和扩展。
- 多态:不同对象对同一消息的不同响应。
总之,面向对象编程更加注重代码的封装、复用和可扩展性,使得代码更易于维护和扩展。而面向过程编程则更适合一些简单、流程性强的问题。在实际编程中,可以根据具体情况选择合适的编程方法。
6、面向对象的三大基本特征是什么?如何理解?
面向对象的三大基本特征是封装、继承和多态
。
封装
是将对象的属性和行为封装在一起,对外只提供必要的接口。它的意义在于:
- 信息隐藏:隐藏内部实现细节,提高安全性和可靠性。
- 模块独立性:减少模块之间的耦合,便于模块的独立开发和测试。
继承
允许子类继承父类的属性和方法,从而实现代码的重用和扩展。理解继承可以从以下几个方面考虑:
- 代码复用:避免重复编写相同的代码。
- 扩展功能:子类可以在父类的基础上添加新的功能。
多态
是指同一个方法在不同的对象上有不同的实现。它的好处包括:
- 灵活性:根据具体对象的类型执行相应的操作。
- 可扩展性:方便添加新的子类并实现不同的行为。
7、java是面向对象还是面向过程?
Java 是一种面向对象的编程语言。
8、什么是反射?为什么需要反射?
反射是指在运行时动态地访问和操作类的信息。
需要反射的原因包括:
- 灵活性:可以在运行时获取类的信息、创建对象、调用方法等,提供了更大的灵活性。
- 动态加载:支持在运行时动态加载和使用类,无需在编译时确定。
- 框架和工具开发:便于开发通用的框架和工具,可适应不同的业务需求。
- 插件机制:支持插件式的扩展,方便添加新的功能。
- XML 配置:与配置文件结合,实现基于配置的动态功能。
- 类操作:对类进行各种操作,如修改属性、方法等。
- 跨模块交互:方便不同模块之间的交互和集成。
通过反射,程序可以在运行时动态地了解和操作类的结构和行为,从而实现更灵活和可扩展的系统设计。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
9、为什么不能用浮点数表示金额? 有什么方法解决?
浮点数在计算机内部的表示方法采用的是 IEEE 标准,它兼顾了数据的精度和大小。32 位的浮点数由 1 比特的符号位、8 比特的阶码和 23 比特的尾数组成。浮点数能表示的数据大小范围由阶码决定,而能够表示的精度完全取决于尾数的长度。对于金额,舍去不能表示的部分,就会产生精度丢失
。
十进制的 0.1 在二进制下将是一个无线循环小数,同样,在进行加法、减法、乘法和除法等运算时,这种精度损失可能会累积,导致结果不正确。为了避免这种情况,建议使用 java.math.BigDecimal
类来表示和计算金额。BigDecimal 提供了用于高精度算术运算的方法,能够精确地表示十进制小数,避免浮点数表示和计算中的精度损失问题。
10、为什么不能用字符串来存储金额?
使用字符串来存储金额有一些局限性:
- 计算困难:进行数值计算时,需要额外的处理来转换和解析字符串。
- 效率较低:在涉及大量金额操作时,性能可能受到影响。
- 不支持数学运算:无法直接进行加减乘除等常见的数学运算。
- 易出错:处理字符串转换可能引入错误。
- 数据类型不一致:与其他数值类型的交互可能需要额外的转换。
然而,在某些情况下,可能会选择使用字符串来存储金额:
- 格式灵活:可以方便地表示特定的金额格式。
- 非数值场景:如仅用于显示或存储。
为了更准确和高效地处理金额,通常使用专门的数值类型或类,例如 Java 中的BigDecimal,它提供了高精度的数值计算功能,能够避免常见的数值计算问题。
11、为什么需要克隆?如何实现对象的克隆?深拷贝和浅拷贝的区别?
需要克隆的原因有以下几点:
- 独立性:创建独立的副本,避免对原始对象的修改影响到其他使用该对象的部分。
- 隔离性:在不同的上下文或操作中使用相同对象的副本,以防止冲突。
- 安全性:确保原始对象的完整性和稳定性。
实现对象的克隆有多种方式,以下是一种常见的方法:
- 实现 Cloneable 接口:确保类实现了 Cloneable 接口。
- 重写 clone 方法:在类中重写 clone 方法。
深拷贝和浅拷贝的区别在于:
深拷贝:复制对象及其引用的所有嵌套对象。
- 副本与原始对象完全独立。
- 修改副本不会影响原始对象。
浅拷贝:只复制对象本身,不复制引用的嵌套对象。
- 嵌套对象仍与原始对象共享。
- 修改副本的嵌套对象会影响原始对象。
深拷贝确保了副本的完全独立性,而浅拷贝在处理嵌套对象时可能会出现问题。在需要完全独立的副本时,应使用深拷贝。
12、try-catch-finally中,如果catch中return了,finally还会执行吗?
在 try-catch-finally 语句中,即使在 catch 块中执行了 return 语句,finally 块仍然会执行。
finally 块的作用是确保无论在 try 块中是否发生异常,某些特定的操作都会被执行,例如资源的释放、清理等。
当执行到 catch 块中的 return 语句时,会先将返回值保存起来,然后执行 finally 块中的代码。最后,再返回保存的返回值。
finally 块的执行具有以下特点:
- 无论是否发生异常,finally 块都会执行。
- 即使在 finally 块中抛出异常,也会继续执行。
- finally 块中的返回值不会影响 try 或 catch 块中的返回值。
使用 finally 块可以确保资源的正确释放,以避免资源泄漏等问题。
13、String为什么设计成不可变的?
String 被设计成不可变有以下几个原因:
- 安全性和稳定性:避免在多个线程中同时修改字符串时可能出现的竞态条件。
- 效率:许多操作可以直接利用字符串的不可变性进行优化。
- 缓存友好:相同字符串可以共享,减少内存占用。
- 线程安全:无需进行额外的同步操作。
- 代码简洁:无需处理可变字符串带来的复杂情况。
- 避免误操作:防止意外修改字符串。
- 易于实现和理解:简化字符串的内部实现。
不可变的特性使得 String 在多线程环境中更加可靠,并且提高了性能和内存效率。
14、Error和Exception的区别和联系?以及常见的RuntimeException?
区别:
- Error: Error是指Java虚拟机无法解决的严重问题,通常
由系统内部错误引起,程序无法通过捕获错误来恢复
。一般来说,程序不应该捕获Error类型的异常,而应该在发生Error时让程序终止。常见的Error包括OutOfMemoryError(内存耗尽)和StackOverflowError(栈溢出)等。 - Exception: Exception是
指程序运行时可能发生的问题,它可以通过捕获和处理来使程序继续执行
。Exception又分为受检异常(Checked Exception)和非受检异常(Unchecked Exception)。受检异常是指在程序编译时需要处理的异常,而非受检异常是指在编译时不需要处理的异常。常见的Exception包括IOException(输入输出异常)和SQLException(数据库访问异常)等。
联系:
Error和Exception都继承自Throwable类,因此它们具有一些共同的特性,如堆栈跟踪和异常信息等。
常见的RuntimeException:
- NullPointerException(空指针异常): 当试图在一个空对象上调用方法或访问属性时抛出。
- ArrayIndexOutOfBoundsException(数组下标越界异常): 当试图访问数组中不存在的索引时抛出。
- IllegalArgumentException(非法参数异常): 当传递给方法的参数不符合方法的要求时抛出。
- ArithmeticException(算术异常): 当出现除以零的运算时抛出。
- ClassCastException(类转换异常): 当试图将一个对象转换为它不是的类型时抛出。
15、抽象类和接口的区别是什么?
抽象类和接口是面向对象编程中两种不同的概念,它们在Java中有着明显的区别。
抽象类(Abstract Class):
- 抽象类可以包含抽象方法和非抽象方法。抽象方法是没有实际实现的方法,需要子类去实现。非抽象方法有默认实现,子类可以选择性地重写这些方法。
- 一个类只能继承一个抽象类(单继承性)。
- 抽象类可以包含成员变量,可以有构造函数,可以拥有普通方法。
- 抽象类可以有访问控制修饰符,可以定义成员变量,可以包含构造方法。
接口(Interface):
- 接口中所有的方法都是抽象方法,没有方法体。
- 一个类可以实现多个接口(多继承性)。
- 接口中的成员变量隐式地是static和final的。
- 接口不能包含构造函数。
- 接口中的方法默认是public的,可以省略访问控制符,不能使用其他访问修饰符。
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
16、==和equals的区别
在 Java 中,== 和 equals() 方法的区别主要包括以下几点:
- ==:它是一种基本的数据比较操作符,用于比较两个对象的引用(内存地址)是否相等。
- equals():它是 Object 类中的方法,通常用于定义对象之间的逻辑相等性。
Object默认是判断地址是否相等, Long、Integer、Date、String等都重写了equals方法。
Object类
public boolean equals(Object obj) {
return (this == obj);
}
Long类
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}
17、super和this的区别是什么?
super用于访问父类的成员,this用于引用当前对象本身的成员
18、Java中的集合类有哪些?说说他们的特点?
List(列表)
特点:有序、可重复
- ArrayList:基于数组实现的动态数组,可以根据需要自动增长容量。它提供了快速的随机访问和在末尾添加/删除元素的性能。适合需要频繁随机访问元素,以及需要在末尾进行添加/删除操作的场景。
- LinkedList:基于双向链表实现的列表,可以高效地在任意位置进行添加/删除操作。它提供了快速的插入和删除性能。
- Vector:与ArrayList类似,但它是线程安全的,因此性能通常较低。现在较少使用,更多地被ArrayList和CopyOnWriteArrayList替代。
Set(集合)
特点:无序、不可重复
- HashSet:基于哈希表实现的集合,不允许重复元素。它提供了快速的添加、删除和查找性能。
- TreeSet:基于红黑树实现的有序集合,可以按照自然顺序或自定义顺序对元素进行排序。它提供了快速的查找和有序遍历性能。
- LinkedHashSet:保持元素插入顺序的HashSet,基于LinkedHashMap实现。
Queue(队列)
特点:先进先出
- LinkedList:除了作为列表使用,LinkedList还可以用作队列,实现FIFO(先进先出)的数据结构。
- PriorityQueue:基于优先级堆的无界队列,元素按照其自然顺序或者创建PriorityQueue时所提供的Comparator进行排序。
- ArrayDeque:一个由数组结构提供的双端队列,可以作为栈使用。
Map(映射)
特点:键值对存储、无序
- HashMap:基于哈希表实现的键值对映射,不允许重复键。它提供了快速的添加、删除和查找性能。
- TreeMap:基于红黑树实现的键值对映射,按键的自然顺序或自定义顺序对键值对进行排序。
- ConcurrentHashMap:基于分段锁实现的并发哈希表,支持高并发的读取和部分并发的写入操作。
- Hashtable:与HashMap类似,但它是线程安全的,并且不允许键或值为null。现在较少使用,更多地被ConcurrentHashMap替代。
19、集合的排序方式的实现方案?
List自带的 sort 方法、集合工具类 Collections 下面的sort方法、stream 流中的 sorted方法
区别:
List.sort:
List.sort 是 List 接口的默认方法,可以直接在列表对象上调用。它也是一个对原始列表进行排序的原地排序方法,可以使用自定义的 Comparator 进行排序。
Collections.sort:
Collections.sort 是对实现了 List 接口的集合进行排序的静态方法。它实际上会调用列表对象的 sort 方法来完成排序操作。
list.stream().sorted:
list.stream().sorted 是使用流的排序方法,它会产生一个新的经过排序的流,可以在后续收集为一个列表或执行其他操作。
如果使用 sorted(null),则会使用默认的自然排序进行排序。
性能对比:
在大多数情况下,原地排序的方法(Collections.sort 和 List.sort)的性能会优于使用流进行排序(list.stream().sorted)。
原地排序方法直接在原始列表上进行操作,不需要额外的内存分配,因此速度更快。
流排序方法(list.stream().sorted)通常会引入额外的内存和计算,因此在速度和性能上可能会略逊于原地排序。
20、ArrayList、LinkedList与Vector的区别?
1. 数据结构:
- ArrayList 基于数组实现。
- LinkedList 基于双向链表实现。
- Vector 也基于数组实现。
2. 线程安全性:
-
ArrayList是非线程安全的。
-
LinkedList是非线程安全的。
-
Vector是线程安全的。
3. 性能:
-
随机访问时,ArrayList性能较好。
-
频繁插入和删除时,LinkedList性能较好。
-
Vector在某些情况下性能可能相对较低。
4. 扩容机制:
- ArrayList和Vector都有扩容机制,ArrayList是1.5倍扩容,Vector是两倍扩容。
- LinkedList是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好。
21、HashMap、Hashtable和ConcurrentHashMap的区别?
1. 线程安全性:
- HashMap 是非线程安全的。
- Hashtable 是线程安全的,但效率较低。
- ConcurrentHashMap 是线程安全的,支持高并发环境。
2. 性能:
- 一般情况下,HashMap 的性能较好,Hashtable 由于线程安全的实现,性能相对较差,ConcurrentHashMap
在保证线程安全的同时,性能也比较优秀。
3. null 值:
-
HashMap 允许键和值为 null。
-
Hashtable 不允许键和值为 null。
-
ConcurrentHashMap 不允许键和值为 null。
22、HashMap的初始化,put流程,get流程,扩容流程说明
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
初始化:
- HashMap 在创建时可以指定初始容量和负载因子。
- 如果没有指定,默认初始容量为 16,负载因子为 0.75。
put 流程:
-
计算键的哈希值。
-
根据哈希值确定存储位置(通过取模运算)。
-
如果该位置没有元素,直接插入。
-
如果该位置已有元素,判断是否与要插入的键相等,若相等则更新值;否则形成链表结构。
get 流程:
-
计算键的哈希值。
-
根据哈希值找到对应的存储位置。
-
在该位置遍历链表或直接返回对应的值。
扩容流程:
- 当元素数量超过当前容量与负载因子的乘积时,进行扩容。
- 扩容时,创建一个新的容量为原来两倍的数组,并将原数组中的元素重新哈希并迁移到新数组中。
23、HashMap、HashSet、ArrayList是线程安全的吗?
HashMap、HashSet和ArrayList都不是线程安全的。
24、创建线程的几种方式?
继承Thread类、实现Runnable接口、使用线程池
25、线程同步的方式有哪些方式?
synchronized关键字、加锁、volatile、原子类
26、synchronized如何使用?加在普通方法上和加在静态方法的区别?
当 synchronized 关键字加在普通方法上时,它会锁定对象实例
;而加在静态方法上时,它锁定的是类的Class对象
。让我通过一个简单的Java类来说明这两种情况。
public class SynchronizedExample {
// 用于演示锁定对象实例的普通方法
public synchronized void synchronizedMethod() {
// 同步的操作
}
// 用于演示锁定类的Class对象的静态方法
public static synchronized void synchronizedStaticMethod() {
// 同步的操作
}
}
现在我们创建两个线程来演示这两种情况:
public class Main {
public static void main(String[] args) {
final SynchronizedExample example = new SynchronizedExample();
// 在普通方法上加锁的示例
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
example.synchronizedMethod();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
example.synchronizedMethod();
}
});
// 在静态方法上加锁的示例
Thread thread3 = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedExample.synchronizedStaticMethod();
}
});
Thread thread4 = new Thread(new Runnable() {
@Override
public void run() {
SynchronizedExample.synchronizedStaticMethod();
}
});
// 启动线程
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
}
在这个例子中,thread1和thread2演示了加在普通方法上的 synchronized 关键字,它们都是针对同一个SynchronizedExample对象实例的。
thread3和thread4演示了加在静态方法上的 synchronized 关键字,它们是针对SynchronizedExample类的Class对象的。
在静态方法上使用 synchronized 关键字时,该关键字锁定的是类的 Class 对象,而不是类的实例对象。这种机制保证了无论类的实例有多少个,同一时刻只能有一个线程执行该类的静态 synchronized 方法,从而确保了对静态方法的同步访问。
springMVC中的service是单例的,因此在service的实现类impl中加 synchronized 关键字,也能使controller层的请求排队。
27、java级别的锁都有哪些?你怎么分类?
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
28、什么情况下需要对对象进行序列化?
对象序列化是一个将对象状态转换为字节流的过程,主要用于实现对象的完全保存或网络传输。当需要满足以下场景时,通常需要对对象进行序列化:
- 网络传输:在分布式系统中,对象经常需要在不同的进程或机器之间进行传输。为了在网络中有效地传输对象,需要将对象序列化为字节流,以便在接收端进行反序列化并恢复为原始对象。
- 数据备份和恢复:当需要将对象状态保存在计算机中以备将来使用时,序列化是一种有效的手段。通过序列化,可以方便地将对象保存到文件或数据库中,以便在需要时进行恢复。
- 持久化存储:序列化是实现对象持久化存储的一种常见方法。通过将对象序列化为字节流并保存到文件中,可以确保对象状态的长期保存,即使程序关闭或重启后也能恢复对象状态。
29、什么是AIO、BIO和NIO?
AIO、BIO和NIO都是IO模型,它们在处理输入输出操作时具有不同的特点和适用场景。
BIO(Blocking I/O):
- BIO是同步阻塞I/O模型。在BIO方式中,一个连接对应一个线程。当客户端发起连接请求后,服务端就需要启动一个线程进行处理。如果这个连接不做任何事情就会造成不必要的线程开销。这种方式的缺点在于,如果有大量客户端并发请求,就需要创建大量的线程来处理,这将导致系统资源的大量消耗,严重情况下甚至会导致系统崩溃。BIO方式适用于连接数量少且固定的场景。
NIO(Non-blocking I/O):
- NIO是同步非阻塞IO模型。它引入了通道(Channel)和缓冲区(Buffer)的概念。数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。这种模型允许一个线程要求一个通道将数据读入一个缓冲区,在数据从通道读入缓冲区的期间,这个线程可以进行其他操作,直到数据完成从通道读入到缓冲区时,线程再继续处理它。数据从缓冲区写入通道时也是如此。此外,NIO还引入了选择器的概念,使得一个线程可以监听多个通道的数据传输情况。NIO适用于连接数目多且业务比较轻的场景,如聊天服务器。
AIO(Asynchronous I/O):
- AIO是异步非阻塞IO模型。在JDK1.7之后,Java提供了异步的相关通道实例。AIO的最大特点是具备异步功能,需要借助操作系统,当底层操作系统具有异步IO模型时,AIO可以在对应的read/write/accept/connection等方法上异步执行,完成后会主动调用回调函数,实现一个CompletionHandler对象。AIO消除了用户态和内核态的切换耗时,使多任务的发展更加容易。此外,它的缓冲机制使得不同的文件读写有更少的线程切换和上下文引起的性能损失。AIO适用于连接数目多且连接比较长(业务重操作)的场景,需要操作系统充分参与并发操作。
33、volatile能保证原子性吗?为什么?
不可以,volatile 关键字能够保证变量的可见性,但是不能保证原子性。
在没有使用 volatile 关键字的情况下,当一个线程修改了变量的值,这个修改之后的值会先被保存在线程的工作内存中,并不会立即刷新到主内存中。这是由于 CPU 和内存优化的特性所决定的。
数据最终会从线程的工作内存刷新到主内存中,但具体刷新的时间并没有明确定义。这取决于底层的架构、CPU 的具体行为,以及 JVM 和编译器应用的各种优化。
34、JUC并发包常用的工具类,分别有什么特性,适用场景?
JUC(java.util.concurrent)是Java中的一个重要包,它提供了一系列用于解决并发问题的工具类。主要包括atomic(原子类)、locks(锁)和一些并发类(如线程安全类和线程池相关:ConcurrentHashMap、CopyOnWriteArrayList、CopyOnWriteArraySet、Executors、CompletableFuture等)
以下是JUC并发包中常用的一些工具类及其特性和适用场景:
ReentrantLock:
- 特性:是一个可重入的互斥锁,支持公平锁和非公平锁。通过它,可以灵活地控制多个线程对共享资源的访问。
- 适用场景:适用于需要实现复杂同步控制的场景,比如需要手动控制锁的获取和释放,或者需要实现更细粒度的锁控制。
Semaphore:
- 特性:俗称信号量,用于控制同时访问某个特定资源的线程数量。通过它,可以实现流量控制,防止过多的线程同时访问某个资源。
- 适用场景:适用于需要对资源进行访问控制的场景,比如数据库连接池、线程池等,确保资源不会被过度消耗。
CountDownLatch:
- 特性:一个同步辅助类,它允许一个或多个线程等待其他线程完成操作。当计数器的值减至0时,等待的线程会被唤醒。
- 适用场景:适用于需要等待一组线程完成某项操作后才能继续执行的场景,比如启动多个线程进行并行计算,然后等待所有线程计算完成后进行汇总。
CyclicBarrier:
- 特性:一个同步辅助类,它允许一组线程互相等待,直到所有线程都到达某个公共屏障点(common barrier point)。
- 适用场景:适用于需要将一组线程划分为几个阶段执行,并且每个阶段都需要所有线程都完成后才能进入下一个阶段的场景。
Exchanger:
- 特性:用于两个线程之间交换数据。当一个线程进入交换时,它会等待另一个线程也进入交换,然后这两个线程交换数据。
- 适用场景:适用于两个线程需要互相交换数据的场景,比如生产者消费者模型中,生产者和消费者需要交换数据。
ConcurrentHashMap:
- 特性:一个线程安全的HashMap实现,它支持高并发读写操作。通过分段锁技术,实现了高效的并发性能。
- 适用场景:适用于需要存储大量键值对,并且需要支持高并发读写的场景,比如缓存系统、分布式系统等。
35、ConcurrentHashMap在哪些地方做了并发控制,保证线程安全的?
36、SimpleDateFormat线程安全性?
SimpleDateFormat 类在 Java 中是非线程安全的。这意味着如果多个线程同时共享同一个 SimpleDateFormat 实例,并尝试使用它进行日期格式化或解析,可能会遇到不可预测的结果和并发问题。
SimpleDateFormat 的非线程安全性主要源于其内部状态(如日期字段、数字等)在格式化或解析过程中可能会被修改。如果多个线程同时访问这些内部状态,就可能导致数据竞争和不一致的行为。
为了解决这个问题,有几种常见的做法:
- 每个线程使用自己的 SimpleDateFormat 实例:这是最简单且最直接的方法。通过为每个线程分配一个单独的
SimpleDateFormat 实例,可以避免线程间的数据竞争。然而,这可能会增加内存消耗,特别是在高并发场景下。 - 使用同步块或锁:通过在访问 SimpleDateFormat 实例时添加同步块或锁,可以确保每次只有一个线程能够修改其内部状态。这可以减少内存消耗,但可能会降低性能,因为线程需要等待锁的释放。
- 使用线程安全的替代方案:Java 8 引入了新的日期和时间 API(如 java.time.format.DateTimeFormatter),这些 API 是线程安全的。如果可能的话,考虑使用这些新的 API
来替代 SimpleDateFormat。 - 使用第三方库:有些第三方库提供了线程安全的日期格式化功能。这些库通常经过优化,可以在高并发环境下提供更好的性能。
37、AQS和CAS分别是什么,如何理解?
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取
AQS是一个Java提供的底层同步工具类,用于构建锁和同步器的框架性组件。它是Java并发包中ReentrantLock、Semaphore、ReentrantReadWriteLock等同步器的基础。AQS的主要特点包括支持独占模式和共享模式,并为这些同步器提供了一个统一的基础框架,让开发人员可以基于此进行扩展和定制化。通过使用AQS,开发人员可以避免自己重复实现同步器的底层机制,从而更加专注于业务的实现。此外,AQS还提供了高效的并发性能,适用于实现计数器、累加器、分布式数据同步、并发队列、内存管理以及自旋等待机制等各种场景。
CAS是一种无锁技术,涉及三个操作数——内存位置(V)、预期原值(A)和新值(B)。当且仅当内存位置V的值等于预期原值A时,将内存位置V的值设置为新值B。否则,处理失败,什么都不做。一般情况下是一个自旋操作,即不断地重试。CAS操作具有原子性,它在多线程环境下可以保证对一个变量的操作过程中不会被其他线程干扰。CAS操作常用于实现高性能的并发队列、内存管理和自旋等待机制等。
38、 ConcurrentHashMap为什么1.8取消了使用ReentrantLock锁?
ConcurrentHashMap在Java 1.8中取消了使用ReentrantLock锁,主要基于以下几个原因:
- 减少内存开销:使用ReentrantLock需要节点继承AQS(AbstractQueuedSynchronizer)来获得同步支持,这增加了内存开销。相比之下,Java 1.8中的ConcurrentHashMap采用了更轻量级的同步机制,如CAS(Compare and Swap)和synchronized,减少了节点的内存占用。
- 优化并发性能:在Java 1.8中,ConcurrentHashMap的存储结构改为了Node数组+链表/红黑树。当冲突链表达到一定长度时,链表会转换成红黑树,以进一步优化查询性能。同时,ConcurrentHashMap对每个Node节点进行加锁,这种细粒度的锁控制可以提高并发度,减少线程间的争用。
- 简化和统一并发机制:通过采用CAS和synchronized等轻量级同步机制,Java 1.8中的ConcurrentHashMap简化了并发控制逻辑,使得代码更加简洁易懂。同时,这也使得Java的并发机制更加统一和一致。
39、HashMap是如何解决Hash冲突的?解决hash冲突都有哪些方案?
HashMap 解决哈希冲突的主要方法是通过链地址法(Separate Chaining)。当发生哈希冲突时,即不同的键具有相同的哈希值,HashMap 会在哈希表的每个桶(bucket)中维护一个链表(或者在链表长度较长的情况下,可以转换为红黑树)来存储具有相同哈希值的键值对。
40、HashMap和ArrayLsit区别,以及他们的key是否可重复/为空?
42、线程池的等待队列有哪几种实现方式?
在Java的java.util.concurrent包中,线程池框架提供了几种不同的队列实现方式,以适应不同的应用场景和需求。以下是线程池等待队列的一些常见实现方式:
ArrayBlockingQueue:
- ArrayBlockingQueue是一个基于数组的有界阻塞队列。它按照FIFO(先进先出)的原则对元素进行排序。当试图向一个已满的队列添加元素时,添加操作会被阻塞;当试图从一个空的队列中移除元素时,移除操作也会被阻塞。
- 适用于已知大概任务量的情况,因为可以预先设定队列容量。
LinkedBlockingQueue:
- LinkedBlockingQueue是一个基于链表的无界(或指定容量的)阻塞队列。与ArrayBlockingQueue相比,它的容量可以动态增长,但也可以指定一个最大容量。
- 当队列为空时,获取元素的线程会被阻塞;当队列已满时,尝试添加元素的线程也会被阻塞(如果队列设置了最大容量)。
- 适用于任务量可能会动态变化的情况,因为队列可以动态扩展。
PriorityBlockingQueue:
- PriorityBlockingQueue是一个支持优先级的无界阻塞队列。元素按照它们的自然顺序或者通过提供的Comparator进行排序。
- 适用于任务之间有优先级区分的情况,可以确保优先级高的任务优先得到执行。
SynchronousQueue:
- SynchronousQueue是一个不存储元素的阻塞队列。每一个插入操作必须等待一个相应的删除操作,反之亦然。它支持公平和非公平两种模式。
- 适用于高并发且任务处理速度非常快的情况,因为任务几乎是在提交后立即得到处理,不需要在队列中等待。
DelayQueue:
- DelayQueue是一个支持延时获取元素的无界阻塞队列。队列中的元素必须实现Delayed接口,这允许元素在队列中等待特定的时间才能被取出。
- 适用于需要定时或延时执行任务的情况,例如定时任务调度。
44、为什么说CAS是乐观锁?底层对应那个类?
CAS(Compare-And-Swap)之所以被称为乐观锁,是因为它在数据更新时持有一种乐观的态度,认为在数据被更新的过程中不会有其他线程来修改它。这与悲观锁(如传统的数据库锁)形成对比,悲观锁总是假设最坏的情况,即在数据被处理时总是会有其他线程来修改它,因此需要在整个数据处理过程中锁定数据。
CAS是一种无锁机制,它通过比较内存中的值与期望值是否相等来确定是否执行交换操作。如果内存中的值与期望值相等,则执行交换操作;否则,不执行。这种机制避免了使用传统锁所带来的性能开销和死锁问题,提高了程序的并发性能。
在CAS的底层实现中,Unsafe类起到了核心作用。Unsafe是CAS的核心类,它提供了直接访问底层操作系统的功能,使得CAS能够在底层进行比较和交换操作。通过Unsafe类中的compareAndSwap方法,CAS能够实现无锁的数据结构,保证并发安全。
请注意,CAS并不是严格意义上的锁,而是通过原子性来保证数据的同步。它不会保证线程同步,而是确保在数据更新期间的一致性。此外,CAS也存在一些潜在的问题,如ABA问题(即在CAS操作期间,一个值可能被其他线程多次修改后又改回原来的值,导致CAS操作无法正确感知数据的实际变动)。
综上所述,CAS之所以被称为乐观锁,是因为它在处理并发数据时持有一种乐观的态度,并通过底层的Unsafe类实现无锁操作,从而提高了程序的并发性能。
45、ThreadLocal有使用过吗?底层原理是什么?使用它该注意什么?有什么问题?
是的,我使用过ThreadLocal。ThreadLocal是Java中一个非常有用的类,它提供了线程本地变量。这些变量不同于它们的正常变量,因为每一个访问这个变量的线程都有其自己独立初始化的变量副本。
底层原理:
- ThreadLocal的底层实现主要依赖于ThreadLocalMap,这是一个特殊的Map,其键为ThreadLocal对象,值为线程变量的副本。每个Thread都持有一个ThreadLocalMap的引用,这个map被用来存储该线程的本地变量。当线程首次访问一个ThreadLocal变量时,通过ThreadLocal的set或get方法在ThreadLocalMap中为其创建一个新的条目。
使用注意事项:
- 内存泄漏:由于ThreadLocal的生命周期和线程的生命周期不同,如果不注意及时清理ThreadLocal变量,可能会导致内存泄漏。因此,在不再需要使用ThreadLocal时,应调用remove()方法将其从当前线程中清除,避免线程结束后仍然持有对该变量的引用。
- 共享变量问题:尽管ThreadLocal为每个线程提供了独立的变量副本,但它并不能解决线程间共享变量的同步问题。如果多个线程共享同一个ThreadLocal变量,需要自行处理线程间的同步操作,确保线程安全。
- 内部使用慎重:在一些特定的情况下,如使用线程池或者异步任务执行框架,使用ThreadLocal需要格外小心。因为线程池中的线程可能会被复用,如果不正确地处理ThreadLocal变量,可能会导致数据混乱。
存在的问题:
- 性能问题:虽然ThreadLocal可以提高并发性能,但因为它需要在每个线程中存储变量副本,所以会增加内存消耗。同时,对ThreadLocalMap的访问也需要一定的时间,这可能会影响性能。
- 使用不当导致的错误:如果在使用ThreadLocal时没有正确地处理变量的初始化和清理,可能会导致数据混乱或内存泄漏等问题。
46、JMM内存模型是什么?为什么需要JMM内存模型?
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记【点击此处即可】免费获取