第四天面试题
文章目录
- 1.什么叫 Java 的内存泄露与内存溢出?
- **1. 内存泄露(Memory Leak)**
- **内存泄露的常见原因:**
- **如何避免内存泄露:**
- **2. 内存溢出(Out Of Memory, OOM)**
- **内存溢出的常见原因:**
- **如何避免内存溢出:**
- **内存泄露与内存溢出的关系**
- **示例代码**
- **内存泄露示例:**
- **内存溢出示例:**
- **总结**
- 2.java 中数据在内存中是如何存储的?
- **1. 栈(Stack)**
- **栈中存储的内容:**
- **示例:**
- **2. 堆(Heap)**
- **堆中存储的内容:**
- **示例:**
- **3. 方法区(Method Area)**
- **示例:**
- **4. 本地方法栈(Native Method Stack)**
- **5. 程序计数器(Program Counter Register)**
- **数据存储的总结**
- **示例代码分析**
- **总结**
- 3.Error 和 Exception 的区别?
- **1. Error**
- **Error 的特点:**
- **常见的 Error 类型:**
- **示例:**
- **2. Exception**
- **Exception 的特点:**
- **常见的 Exception 类型:**
- **示例:**
- **Error 和 Exception 的区别**
- **总结**
- 4.常见的异常有哪些?
- 5.请说一下递归的概念?
- 6.try...catch...finally的作用?
- 1. **`try` 块**
- 2. **`catch` 块**
- 3. **`finally` 块**
- 执行顺序
- 示例代码
- 使用场景
- 注意事项
- 7.try...catch...finally执行顺序问题?
- 1. **正常情况(无异常)**
- 2. **发生异常**
- 3. **`try` 或 `catch` 中有 `return`**
- 4. **`finally` 中有 `return`**
- 5. **`try` 或 `catch` 中有 `System.exit()`**
- 总结
- 8.throws 和 throw 的区别?
- 1. **`throw`**
- 2. **`throws`**
- 3. **`throw` 和 `throws` 的区别**
- 4. **结合使用的示例**
- 5. **总结**
- 9.final,finally 和 finalize 的比较?
- 1. `final`
- 2. `finally`
- 3. `finalize`
- 总结
- 10.进程和线程的区别?
- 1. **定义**
- 2. **资源分配**
- 3. **创建和销毁的开销**
- 4. **独立性**
- 5. **并发性**
- 6. **应用场景**
- 7. **示例**
- 总结对比表
- 选择进程还是线程?
- 11.并行和并发?
- 1. **定义**
- 2. **核心区别**
- 3. **实现方式**
- 4. **适用场景**
- 5. **示例**
- 6. **类比**
- 7. **总结对比表**
- 8. **并发与并行的关系**
- 9. **编程模型**
- 总结
- 12.JVM 的启动是多线程的吗
- 主要后台线程
- 总结
- 13.多线程的创建方式?
- 1. **继承 `Thread` 类**
- 2. **实现 `Runnable` 接口**
- 3. **实现 `Callable` 接口**
- 4. **使用线程池**
- 5. **使用 Lambda 表达式(Java 8+)**
- 总结
- 14.继承Thread类和实现Runnable接口的对比?
- 1. **继承 `Thread` 类**
- 实现方式
- 优点
- 缺点
- 2. **实现 `Runnable` 接口**
- 实现方式
- 优点
- 缺点
- 对比总结
- 推荐使用场景
- 示例:资源共享场景
- 15.什么是守护线程?
- 守护线程的特点
- 守护线程的创建
- 守护线程 vs 非守护线程
- 注意事项
- 示例:守护线程的应用
- 总结
- 16.线程的生命周期?
- 线程的 5 种状态
- 1. **新建状态(NEW)**
- 2. **就绪状态(RUNNABLE)**
- 3. **运行状态(RUNNING)**
- 4. **阻塞状态(BLOCKED)**
- 5. **等待状态(WAITING)**
- 6. **超时等待状态(TIMED_WAITING)**
- 7. **终止状态(TERMINATED)**
- 线程状态转换图
- 示例代码:观察线程状态
- 总结
- 17.阻塞状态的分类?
- 1. **等待阻塞(Waiting)**
- 2. **阻塞阻塞(Blocked)**
- 3. **定时阻塞(Timed Waiting)**
- 总结
- 18.什么线程安全问题?
- 19.什么是线程同步?
1.什么叫 Java 的内存泄露与内存溢出?
内存泄漏(memoryleak):就是存在一些被分配的对象但是这些对象不会再被使用,仍存在该内存对象的引用,导致无法释放内存空间。一次内存泄露危害可以忽略,但是任其发展最终会导致内存溢出,如读取文件后流要及时关闭、数据库连接要及时释放。
内存溢出(outofmemory):就是指应用程序在申请内存时,没有足够的内存空间供其使用。如我们在项目中对于大批量数据的导入,尽量采用分批提交的方式
Java 中的 内存泄露(Memory Leak) 和 内存溢出(Out Of Memory, OOM) 是两个不同的概念,但它们都与内存管理相关。
1. 内存泄露(Memory Leak)
内存泄露是指程序在运行过程中,由于某些原因未能释放不再使用的对象,导致这些对象占用的内存无法被回收,从而造成内存的浪费。随着时间的推移,内存泄露会逐渐累积,最终可能导致内存溢出。
内存泄露的常见原因:
- 静态集合类:静态集合(如
static List
、static Map
)会一直持有对象的引用,导致对象无法被回收。 - 未关闭的资源:如数据库连接、文件流、网络连接等未显式关闭。
- 监听器或回调未移除:注册了监听器或回调但未及时移除,导致对象无法被回收。
- 内部类持有外部类引用:非静态内部类会隐式持有外部类的引用,如果内部类对象未释放,外部类对象也无法释放。
- 缓存未清理:缓存中的对象未被及时清理,导致内存占用不断增加。
如何避免内存泄露:
- 及时释放不再使用的资源。
- 使用弱引用(
WeakReference
)或软引用(SoftReference
)来管理缓存。 - 避免滥用静态变量。
- 使用工具(如
Java VisualVM
、Eclipse MAT
)检测内存泄露。
2. 内存溢出(Out Of Memory, OOM)
内存溢出是指程序在申请内存时,没有足够的内存空间供其使用。通常是因为内存泄露累积、程序本身需要的内存超过了 JVM 分配的最大内存限制,或者内存分配不合理。
内存溢出的常见原因:
- 内存泄露:内存泄露累积到一定程度会导致内存溢出。
- 大对象或大数组:一次性申请过大的内存空间(如加载大文件到内存)。
- JVM 内存设置过小:JVM 堆内存(
-Xmx
)设置过小,无法满足程序需求。 - 过多的线程:每个线程都会占用一定的栈空间,线程过多可能导致栈内存溢出。
如何避免内存溢出:
- 合理设置 JVM 内存参数(如
-Xmx
和-Xms
)。 - 优化代码,避免一次性加载过多数据到内存。
- 使用缓存时设置合理的缓存大小和过期策略。
- 定期检查并修复内存泄露问题。
内存泄露与内存溢出的关系
- 内存泄露是内存溢出的一个可能原因,但不是唯一原因。
- 内存泄露会导致可用内存逐渐减少,最终可能引发内存溢出。
- 内存溢出可能是由于内存泄露,也可能是由于程序本身需要的内存超过了 JVM 的限制。
示例代码
内存泄露示例:
public class MemoryLeakExample {
private static List<Object> list = new ArrayList<>();
public void addToCache(Object obj) {
list.add(obj); // 对象被添加到静态集合中,无法被回收
}
}
内存溢出示例:
public class OutOfMemoryExample {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
while (true) {
list.add(new byte[1024 * 1024]); // 不断申请内存,最终导致 OOM
}
}
}
总结
- 内存泄露:对象不再使用但未被回收,导致内存浪费。
- 内存溢出:程序申请的内存超过了可用内存,导致程序崩溃。
- 解决内存问题的关键是合理管理内存,避免内存泄露,并优化内存使用。
维度 | 内存泄漏 | 内存溢出 |
---|---|---|
本质 | 对象无法回收,内存被无效占用。 | 内存不足,无法分配新对象。 |
触发条件 | 长期运行后逐渐积累。 | 可能由内存泄漏引发,也可能瞬间发生(如加载超大文件)。 |
表现 | 程序性能逐渐下降,最终可能触发内存溢出。 | 直接抛出OutOfMemoryError ,程序崩溃。 |
解决方案 | 修复代码逻辑(如及时释放资源、清除集合)。 | 增加JVM内存或优化程序(如分页查询、减少对象创建)。 |
2.java 中数据在内存中是如何存储的?
在Java中,数据在内存中的存储方式取决于数据类型和变量的声明方式。基本数据类型(如int、double、boolean等)直接存储在栈内存(Stack Memory)中,而对象的引用则存储在栈内存中,而对象本身存储在堆内存(Heap Memory)中
在 Java 中,数据在内存中的存储方式主要分为以下几个部分:栈(Stack)、堆(Heap)、方法区(Method Area) 和 本地方法栈(Native Method Stack)。不同的数据类型和对象在内存中的存储位置和方式有所不同。
1. 栈(Stack)
栈内存用于存储局部变量、方法参数和方法调用的上下文。它的特点是:
- 速度快:栈内存的分配和回收速度非常快。
- 生命周期短:栈内存中的数据随着方法的调用而创建,随着方法的结束而销毁。
- 线程私有:每个线程都有自己的栈内存。
栈中存储的内容:
- 基本数据类型:如
int
、float
、boolean
等,直接存储值。 - 对象引用:对象的引用(地址)存储在栈中,而对象本身存储在堆中。
- 方法调用栈帧:每个方法调用会创建一个栈帧,存储局部变量、操作数栈、动态链接和方法返回地址。
示例:
public void exampleMethod() {
int a = 10; // 基本数据类型,存储在栈中
String str = "Hello"; // 对象引用存储在栈中,对象本身在堆中
}
2. 堆(Heap)
堆内存用于存储对象实例和数组。它的特点是:
- 生命周期长:堆中的对象由垃圾回收器(GC)管理,不需要手动释放。
- 线程共享:堆内存是所有线程共享的。
- 速度较慢:堆内存的分配和回收速度相对较慢。
堆中存储的内容:
- 对象实例:通过
new
关键字创建的对象。 - 数组:无论是基本数据类型数组还是对象数组。
- 字符串常量池:字符串常量(如
String str = "Hello"
)存储在堆中的字符串常量池。
示例:
public void exampleMethod() {
Object obj = new Object(); // 对象实例存储在堆中
int[] arr = new int[10]; // 数组存储在堆中
}
3. 方法区(Method Area)
方法区是堆的一个逻辑部分,用于存储:
- 类信息:类的元数据(如类名、方法信息、字段信息等)。
- 静态变量:
static
修饰的变量。 - 常量池:编译期生成的字面量和符号引用。
- 运行时常量池:类加载后,常量池中的内容会被加载到运行时常量池。
示例:
public class Example {
public static int staticVar = 10; // 静态变量存储在方法区
public final String constant = "Constant"; // 常量存储在方法区的常量池
}
4. 本地方法栈(Native Method Stack)
本地方法栈用于支持 Native 方法(用 native
关键字修饰的方法,通常是用 C/C++ 实现的)。它的特点是:
- 与栈类似,但专门用于 Native 方法的调用。
5. 程序计数器(Program Counter Register)
程序计数器是一个很小的内存区域,用于存储当前线程执行的字节码指令地址。每个线程都有自己的程序计数器。
数据存储的总结
数据类型 | 存储位置 | 说明 |
---|---|---|
基本数据类型 | 栈 | 直接存储值。 |
对象引用 | 栈 | 存储对象的地址,对象本身在堆中。 |
对象实例 | 堆 | 通过 new 创建的对象。 |
数组 | 堆 | 无论是基本数据类型数组还是对象数组。 |
静态变量 | 方法区 | static 修饰的变量。 |
常量 | 方法区的常量池 | final 修饰的常量。 |
类信息 | 方法区 | 类的元数据(如方法、字段信息等)。 |
本地方法 | 本地方法栈 | native 方法的相关数据。 |
示例代码分析
public class MemoryExample {
private static int staticVar = 10; // 静态变量,存储在方法区
private int instanceVar = 20; // 实例变量,存储在堆中
public void method() {
int localVar = 30; // 局部变量,存储在栈中
Object obj = new Object(); // 对象引用在栈中,对象实例在堆中
}
}
总结
- 栈:存储局部变量、方法参数和对象引用。
- 堆:存储对象实例和数组。
- 方法区:存储类信息、静态变量和常量池。
- 本地方法栈:存储 Native 方法的调用信息。
理解 Java 内存模型对于编写高效、稳定的程序非常重要,尤其是在处理内存泄露和性能优化时。
3.Error 和 Exception 的区别?
Error 和 Exception 都有一个共同的根类是 Throwable 类。
Error 是系统中的错误,程序员是不能改变的和处理的,一般是指与虚拟机相关的问题,对于这类错误的导致的应用程序中断,仅靠程序本身无法恢复和和预防,遇到这样的错误,建议让程序终止。因此我们编写程序时不需要关心这类错误。
Exception,也就是我们经常见到的一些异常情况,表示程序可以处理的异常,可以捕获且可能恢复。遇到这类异常,应该尽可能处理异常,使程序恢复运行,而不应该随意终止异常。
在 Java 中,Error 和 Exception 都是 Throwable
类的子类,但它们代表了不同类型的异常情况,用途和处理方式也有所不同。
1. Error
Error
表示程序无法处理的严重问题,通常与虚拟机(JVM)或系统资源相关。Error
是不可检查的异常(Unchecked Exception),程序通常无法恢复或处理这些问题。
Error 的特点:
- 严重性:表示系统级别的错误,通常是致命的。
- 不可恢复:程序无法从
Error
中恢复。 - 不需要捕获:通常不需要在代码中显式捕获或处理
Error
。
常见的 Error 类型:
OutOfMemoryError
:内存不足错误,通常是由于堆内存耗尽。StackOverflowError
:栈溢出错误,通常是由于递归调用过深。NoClassDefFoundError
:类定义未找到错误,通常是由于类加载失败。VirtualMachineError
:虚拟机错误,表示 JVM 出现了严重问题。
示例:
public class ErrorExample {
public static void main(String[] args) {
recursiveMethod(); // 递归调用导致栈溢出
}
public static void recursiveMethod() {
recursiveMethod(); // 无限递归,最终抛出 StackOverflowError
}
}
2. Exception
Exception
表示程序可以处理的异常情况,通常是程序逻辑或外部环境导致的。Exception
分为两类:
- 检查型异常(Checked Exception):必须在代码中显式处理(捕获或抛出)。
- 非检查型异常(Unchecked Exception):通常是程序逻辑错误,不需要显式处理。
Exception 的特点:
- 可恢复性:程序通常可以从
Exception
中恢复。 - 需要处理:检查型异常必须在代码中显式处理。
- 分类:
- 检查型异常:如
IOException
、SQLException
。 - 非检查型异常:如
NullPointerException
、ArithmeticException
。
- 检查型异常:如
常见的 Exception 类型:
- 检查型异常:
IOException
:输入输出异常。SQLException
:数据库操作异常。ClassNotFoundException
:类未找到异常。
- 非检查型异常:
NullPointerException
:空指针异常。ArithmeticException
:算术异常(如除以零)。ArrayIndexOutOfBoundsException
:数组越界异常。
示例:
public class ExceptionExample {
public static void main(String[] args) {
try {
int result = 10 / 0; // 抛出 ArithmeticException
} catch (ArithmeticException e) {
System.out.println("捕获到算术异常: " + e.getMessage());
}
}
}
Error 和 Exception 的区别
特性 | Error | Exception |
---|---|---|
类型 | 不可检查的异常(Unchecked) | 检查型异常(Checked)和非检查型异常(Unchecked) |
严重性 | 严重,通常是致命的 | 可恢复,通常是程序逻辑或外部环境问题 |
处理方式 | 通常不需要捕获或处理 | 检查型异常必须显式处理,非检查型异常可选处理 |
来源 | 通常与 JVM 或系统资源相关 | 通常与程序逻辑或外部环境相关 |
示例 | OutOfMemoryError 、StackOverflowError | IOException 、NullPointerException |
总结
- Error:表示系统级别的严重问题,程序通常无法恢复,不需要显式处理。
- Exception:表示程序可以处理的异常情况,分为检查型异常和非检查型异常,通常需要显式处理。
在实际开发中,应重点关注 Exception
的处理,而对于 Error
,通常需要从系统或 JVM 层面进行优化和调整。
4.常见的异常有哪些?
java.lang.RuntimeException: 运行时异常
ClassCastException: 类类型转换异常,当试图将对象强制转换为不是实例的子类时,抛出该异常;
ArrayIndexOutOfBoundsException: 数组下标越界异常,当你使用不合法的索引访问数组时会抛出该异常;
NullPointerException: 空指针异常,通过 null 进行方法和属性调用会抛出该异常;
ArithmeticException: 算术运算异常,除数为 0,抛出该异常;
NumberFormatException: 数字转换异常,当试图将一个 String 转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常;
InputMismatchException: 输入不匹配异常,输入的值数据类型与设置的值数据类型不能匹配。
java 编译时异常:
SQLException: SQL 异常,提供有关数据库访问错误或其他错误的信息的异常;
IOExeption: 输入输出异常,表示发送了某种 I/O 异常的信号。FileNotFoundException: 文件找不到异常,通常是两种情况:1、系统找不到指定的路径 2、拒绝访问(指定的是目录时,就会报拒绝访问异常)
EOFException: 文件已结束异常,抛出 EOFException 一定是因为连接断了还在继续read;
java.lang.ClassNotFoundException: 类找不到异常,当我们通过配置文件去查找一个类的时候,如果配置路径写错,就会抛出该异常,比如:web.xml 文件中根本就不存在该类的配置或者配置的路径写错;
在编程和系统运行中,常见的异常类型包括:
-
空指针异常(NullPointerException)
- 尝试访问或操作一个空对象。
-
数组越界异常(ArrayIndexOutOfBoundsException)
- 访问数组时超出其范围。
-
类型转换异常(ClassCastException)
- 试图将对象强制转换为不兼容的类型。
-
算术异常(ArithmeticException)
- 如除零错误。
-
输入输出异常(IOException)
- 文件读写或网络通信失败。
-
文件未找到异常(FileNotFoundException)
- 尝试访问不存在的文件。
-
非法参数异常(IllegalArgumentException)
- 方法接收到非法参数。
-
非法状态异常(IllegalStateException)
- 对象处于不适合执行操作的状态。
-
并发修改异常(ConcurrentModificationException)
- 在迭代集合时修改集合内容。
-
栈溢出异常(StackOverflowError)
- 递归调用过深导致栈空间耗尽。
-
内存溢出异常(OutOfMemoryError)
- 内存不足,无法分配对象。
-
类未找到异常(ClassNotFoundException)
- 尝试加载不存在的类。
-
未实现异常(UnsupportedOperationException)
- 调用未实现的操作。
-
安全异常(SecurityException)
- 违反安全限制的操作。
-
超时异常(TimeoutException)
- 操作在规定时间内未完成。
-
中断异常(InterruptedException)
- 线程在等待或睡眠时被中断。
-
格式异常(NumberFormatException)
- 字符串转换为数字时格式不正确。
-
未找到资源异常(NoSuchElementException)
- 尝试访问不存在的元素。
-
断言错误(AssertionError)
- 断言条件不成立。
-
虚拟机错误(VirtualMachineError)
- 虚拟机出现严重问题。
这些异常通常通过try-catch
块捕获和处理,以确保程序稳定运行。
5.请说一下递归的概念?
递归是计算机的重要概念,就是在过程或函数里调用自身,以达到能使程序更加简洁、清晰
递归是一种在函数定义中使用函数自身的方法。在计算机科学中,递归算法是一种直接或者间接调用自身函数或者方法的算法。递归通常用于解决可以分解为相似的子问题的问题。递归方法的核心在于将问题分解成更小的、更容易解决的子问题,直到这些子问题足够小以至于可以直接解决。
递归的两个关键要素是:
-
基准情形(Base Case):这是递归的终止条件。在基准情形下,递归不再进行,而是返回一个确定的值。没有基准情形或者基准情形设置不当,递归将无限进行下去,导致栈溢出错误。
-
递归情形(Recursive Case):这是递归的主体部分,它将问题分解成更小的子问题,并调用自身来解决这些子问题。每一次递归调用都应该使问题向基准情形靠近。
递归的一个经典例子是计算阶乘。阶乘函数定义为:
- 基准情形:0! = 1
- 递归情形:n! = n * (n-1)!
用伪代码表示递归计算阶乘的函数如下:
function factorial(n):
if n == 0:
return 1
else:
return n * factorial(n - 1)
在这个例子中,factorial
函数在n
不为0时调用自身,每次调用时n
的值减1,直到n
为0时递归停止,开始返回计算结果。
递归的优点是代码简洁且易于理解,尤其适用于处理如树形结构、分治算法、动态规划等问题。然而,递归也有缺点,比如可能会导致大量的函数调用,消耗栈空间,以及可能引起栈溢出。在某些情况下,递归可以被迭代(循环)所替代,以减少资源消耗。
6.try…catch…finally的作用?
try 块中放可能发生异常的代码。
catch 块用于捕获并处理一个特定的异常,catch 块可以有多个;
finally 块无论异常是否发生,异常是否匹配被处理,都会执行,主要做一些清理工作, 比如释放资源;
try 必须有,catch 和 finally 都是可选的;但是 try 不能单独存在,至少要和 catch 或finally 其中一个结合使用;
try...catch...finally
是许多编程语言中用于异常处理的结构。它的作用是在程序中捕获和处理异常,确保代码的健壮性和稳定性。以下是每个部分的作用:
1. try
块
- 作用:包裹可能会抛出异常的代码。
- 说明:在
try
块中,程序会正常执行代码。如果代码执行过程中发生异常,程序会立即跳转到catch
块处理异常;如果没有异常,catch
块会被跳过。
2. catch
块
- 作用:捕获并处理
try
块中抛出的异常。 - 说明:
catch
块可以捕获特定类型的异常(如NullPointerException
、IOException
等),并定义处理逻辑。可以有一个或多个catch
块,用于处理不同类型的异常。 - 注意:如果
try
块中没有抛出异常,catch
块不会执行。
3. finally
块
- 作用:无论是否发生异常,
finally
块中的代码都会执行。 - 说明:
finally
块通常用于释放资源、关闭文件、断开连接等清理操作,确保这些操作无论程序是否发生异常都会执行。 - 注意:即使
try
或catch
块中有return
语句,finally
块仍然会执行。
执行顺序
- 执行
try
块中的代码。 - 如果
try
块中发生异常,跳转到匹配的catch
块处理异常。 - 无论是否发生异常,最终都会执行
finally
块。
示例代码
try {
// 可能会抛出异常的代码
int result = 10 / 0; // 这里会抛出 ArithmeticException
} catch (ArithmeticException e) {
// 捕获并处理异常
System.out.println("捕获到异常: " + e.getMessage());
} finally {
// 无论是否发生异常,都会执行的代码
System.out.println("finally 块执行");
}
使用场景
- 资源释放:如关闭文件、数据库连接、网络连接等。
- 异常处理:捕获并处理特定异常,避免程序崩溃。
- 清理操作:确保某些操作(如日志记录、状态重置)一定会执行。
注意事项
finally
块中的代码一定会执行,除非程序被强制终止(如System.exit()
)。- 如果
catch
块中抛出了新的异常,finally
块仍然会执行,但新异常会覆盖原来的异常。 - 尽量避免在
finally
块中使用return
,因为它会覆盖try
或catch
块中的返回值。
通过 try...catch...finally
,可以有效地管理异常,确保程序的稳定性和资源的正确释放。
7.try…catch…finally执行顺序问题?
try-catch-finally的执行顺序是:
1、不管有没有出现异常,finally块中的代码都会执行;
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return后面的表达式运算后执行的。
try...catch...finally
的执行顺序是固定的,理解它的执行顺序对于正确处理异常和资源管理非常重要。以下是详细的执行顺序规则:
1. 正常情况(无异常)
- 执行顺序:
try
→finally
- 说明:
- 程序首先执行
try
块中的代码。 - 如果
try
块中没有抛出异常,则跳过catch
块。 - 最后执行
finally
块中的代码。
- 程序首先执行
示例:
try {
System.out.println("try 块执行");
} catch (Exception e) {
System.out.println("catch 块执行");
} finally {
System.out.println("finally 块执行");
}
输出:
try 块执行
finally 块执行
2. 发生异常
- 执行顺序:
try
→catch
→finally
- 说明:
- 程序首先执行
try
块中的代码。 - 如果
try
块中抛出异常,程序立即跳转到匹配的catch
块处理异常。 - 无论是否捕获到异常,最后都会执行
finally
块中的代码。
- 程序首先执行
示例:
try {
System.out.println("try 块执行");
int result = 10 / 0; // 抛出 ArithmeticException
} catch (ArithmeticException e) {
System.out.println("catch 块执行");
} finally {
System.out.println("finally 块执行");
}
输出:
try 块执行
catch 块执行
finally 块执行
3. try
或 catch
中有 return
- 执行顺序:
try
→catch
→finally
→return
- 说明:
- 如果
try
或catch
块中有return
语句,程序会先计算return
的值并暂存。 - 然后执行
finally
块中的代码。 - 最后返回之前暂存的
return
值。
- 如果
示例:
public static int test() {
try {
System.out.println("try 块执行");
return 1; // 返回值暂存
} catch (Exception e) {
System.out.println("catch 块执行");
return 2; // 返回值暂存
} finally {
System.out.println("finally 块执行");
}
}
public static void main(String[] args) {
System.out.println("返回值: " + test());
}
输出:
try 块执行
finally 块执行
返回值: 1
4. finally
中有 return
- 执行顺序:
try
→catch
→finally
(覆盖返回值) - 说明:
- 如果
finally
块中有return
语句,它会覆盖try
或catch
块中的返回值。 - 这种写法不推荐,因为它会掩盖异常和返回值。
- 如果
示例:
public static int test() {
try {
System.out.println("try 块执行");
return 1;
} catch (Exception e) {
System.out.println("catch 块执行");
return 2;
} finally {
System.out.println("finally 块执行");
return 3; // 覆盖返回值
}
}
public static void main(String[] args) {
System.out.println("返回值: " + test());
}
输出:
try 块执行
finally 块执行
返回值: 3
5. try
或 catch
中有 System.exit()
- 执行顺序:
try
→System.exit()
(程序终止) - 说明:
- 如果
try
或catch
块中调用了System.exit()
,程序会立即终止,finally
块不会执行。
- 如果
示例:
try {
System.out.println("try 块执行");
System.exit(0); // 程序终止
} catch (Exception e) {
System.out.println("catch 块执行");
} finally {
System.out.println("finally 块执行");
}
输出:
try 块执行
总结
finally
块一定会执行(除非程序被强制终止,如System.exit()
)。return
和异常不会阻止finally
的执行,但finally
中的return
会覆盖之前的返回值。- 避免在
finally
中使用return
,以免掩盖异常或返回值。
理解这些规则可以帮助你更好地编写健壮的异常处理代码!
8.throws 和 throw 的区别?
throws 定义在方法的声明中,表示调用该方法可能出现一个或多个异常,由该方法的调
用者来处理;throws 后面跟的是一个或多个异常类型,让它的使用者知道需要捕获的异常的
类型; throws 表示出现异常的一种可能性,并不一定会发生这种异常。
throw 定义在方法内部,表示方法体内,表示抛出异常,由方法体内的语句处理;throw 是
具体向外抛出异常的动作,该语句的后面必须是一个异常对象; 执行 throw 一定抛出了某种
异常。
两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由函数去处理异
常,真正的处理异常由函数的上层调用处理。
throws
和 throw
是 Java 中用于异常处理的两个关键字,它们的作用和使用场景完全不同。以下是它们的区别和具体用法:
1. throw
- 作用:用于在代码中显式抛出一个异常。
- 使用场景:当程序逻辑中检测到某种错误或异常情况时,可以使用
throw
手动抛出异常。 - 语法:
throw new ExceptionType("异常信息");
- 特点:
throw
是一个语句,必须写在方法体内。- 抛出的异常可以是 Java 内置的异常类(如
NullPointerException
、IllegalArgumentException
等),也可以是自定义的异常类。 - 抛出异常后,程序会立即停止当前方法的执行,并将异常传递给调用者。
示例:
public void checkAge(int age) {
if (age < 18) {
throw new IllegalArgumentException("年龄不能小于18岁");
}
System.out.println("年龄合法");
}
2. throws
- 作用:用于在方法声明中指定该方法可能抛出的异常类型。
- 使用场景:当一个方法内部可能会抛出某种异常,但不想在当前方法中处理时,可以使用
throws
将异常抛给调用者处理。 - 语法:
返回类型 方法名(参数列表) throws 异常类型1, 异常类型2, ... { // 方法体 }
- 特点:
throws
是方法声明的一部分,用于告知调用者该方法可能抛出的异常。- 调用者必须处理这些异常(使用
try-catch
或继续向上抛出)。 - 可以声明多个异常类型,用逗号分隔。
示例:
public void readFile(String filePath) throws IOException {
FileReader fileReader = new FileReader(filePath);
// 读取文件操作
}
3. throw
和 throws
的区别
特性 | throw | throws |
---|---|---|
作用 | 显式抛出一个异常对象。 | 声明方法可能抛出的异常类型。 |
使用位置 | 方法体内。 | 方法声明中。 |
异常处理 | 抛出异常后,程序立即停止当前方法执行。 | 仅声明异常,不处理异常。 |
异常类型 | 必须是一个具体的异常对象。 | 可以是多个异常类型,用逗号分隔。 |
是否必须 | 可选,根据需要手动抛出异常。 | 如果方法可能抛出受检异常,则必须声明。 |
4. 结合使用的示例
public class Example {
// 方法声明可能抛出 IOException
public void readFile(String filePath) throws IOException {
if (filePath == null) {
// 手动抛出异常
throw new IllegalArgumentException("文件路径不能为空");
}
FileReader fileReader = new FileReader(filePath);
// 读取文件操作
}
public static void main(String[] args) {
Example example = new Example();
try {
example.readFile(null); // 调用可能抛出异常的方法
} catch (IOException e) {
System.out.println("捕获到IO异常: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println("捕获到非法参数异常: " + e.getMessage());
}
}
}
5. 总结
throw
:用于在代码中手动抛出异常。throws
:用于在方法声明中指定可能抛出的异常类型。- 关系:
throw
是抛出异常的具体操作,而throws
是对异常的一种声明和传递。
理解它们的区别和用法,可以帮助你更好地设计异常处理逻辑!
9.final,finally 和 finalize 的比较?
final 修饰类,类不能被继承 final 修饰方法,方法不能被重写,final 修饰变量,变量不能被修改。
finally 是异常语句块的一部分,无论是否有异常,都会被执行。
finalize是Object类的一个方法,在垃圾收集器执行的时候会调用被回收对象的此方法,供垃圾收集时的其他资源回收,例如关闭文件等。
final
、finally
和 finalize
是 Java 中的三个不同概念,尽管它们在名称上相似,但用途和功能完全不同。以下是它们的比较:
1. final
- 用途:
final
是一个关键字,用于修饰类、方法或变量。 - 作用:
- 修饰类: 表示该类不能被继承。
- 修饰方法: 表示该方法不能被子类重写(override)。
- 修饰变量: 表示该变量是一个常量,一旦赋值后不能被修改。
- 示例:
final class MyClass { // 不能被继承 final int myVar = 10; // 常量,不能修改 final void myMethod() { // 不能被子类重写 System.out.println("This is a final method."); } }
2. finally
- 用途:
finally
是一个关键字,用于异常处理中的try-catch
块。 - 作用:
finally
块中的代码无论是否发生异常都会执行,通常用于释放资源或执行清理操作。 - 示例:
try { int result = 10 / 0; // 可能抛出异常 } catch (ArithmeticException e) { System.out.println("Exception caught: " + e.getMessage()); } finally { System.out.println("This will always be executed."); }
3. finalize
- 用途:
finalize
是Object
类中的一个方法,用于垃圾回收。 - 作用: 在对象被垃圾回收器回收之前,
finalize
方法会被调用,通常用于释放非内存资源(如文件句柄、网络连接等)。 - 注意:
finalize
方法不保证一定会被执行,且不推荐依赖它来释放资源,因为垃圾回收的时间是不确定的。 - 示例:
class MyClass { @Override protected void finalize() throws Throwable { try { System.out.println("Finalize method called."); } finally { super.finalize(); } } }
总结
final
: 用于修饰类、方法或变量,表示不可继承、不可重写或不可修改。finally
: 用于异常处理,确保某段代码无论是否发生异常都会执行。finalize
: 用于垃圾回收,在对象被回收前执行清理操作,但不推荐依赖。
这三者在 Java 中扮演不同的角色,理解它们的区别有助于编写更健壮和高效的代码。
10.进程和线程的区别?
1、线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
2、一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
3、进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段,数据集,堆等)及一些进程级的资源(如打开文件和信号等),某进程内的线程在其他进程不可见;
4、调度和切换:线程上下文切换比进程上下文切换要快得多;
进程(Process)和线程(Thread)是操作系统中的两个核心概念,它们都是程序执行的基本单位,但在资源管理、调度和通信等方面存在显著区别。以下是它们的详细比较:
1. 定义
-
进程:
- 进程是操作系统分配资源的基本单位,是程序的一次执行过程。
- 每个进程都有独立的内存空间、文件描述符、系统资源等。
- 进程之间相互隔离,一个进程崩溃不会直接影响其他进程。
-
线程:
- 线程是进程中的一个执行单元,是 CPU 调度的基本单位。
- 线程共享进程的内存空间和资源,但每个线程有自己的栈和程序计数器。
- 线程之间可以直接通信,但需要同步机制来避免资源竞争。
2. 资源分配
-
进程:
- 每个进程都有独立的内存空间(代码段、数据段、堆栈等)。
- 进程之间的资源是隔离的,通信需要通过进程间通信(IPC)机制,如管道、消息队列、共享内存等。
-
线程:
- 线程共享进程的内存空间和资源(如堆、全局变量等)。
- 线程之间的通信更高效,因为它们可以直接读写共享数据。
3. 创建和销毁的开销
-
进程:
- 创建和销毁进程的开销较大,因为需要分配和回收独立的内存空间和资源。
- 进程切换(上下文切换)的开销也较大,因为需要保存和恢复整个进程的状态。
-
线程:
- 创建和销毁线程的开销较小,因为线程共享进程的资源。
- 线程切换的开销也较小,因为只需要保存和恢复线程的栈和程序计数器。
4. 独立性
-
进程:
- 进程之间相互独立,一个进程崩溃不会影响其他进程。
- 进程之间的数据共享需要通过 IPC 机制。
-
线程:
- 线程之间共享进程的资源,一个线程崩溃可能导致整个进程崩溃。
- 线程之间的数据共享更直接,但需要同步机制(如锁、信号量)来避免竞争条件。
5. 并发性
-
进程:
- 进程之间可以并发执行,但进程切换的开销较大。
- 适合需要高隔离性和安全性的场景。
-
线程:
- 线程之间可以更高效地并发执行,因为线程切换的开销较小。
- 适合需要高效资源共享和通信的场景。
6. 应用场景
-
进程:
- 适合需要高隔离性和安全性的任务,如浏览器中的多个标签页、操作系统的多个应用程序。
- 适合分布式计算和多机协作的场景。
-
线程:
- 适合需要高效资源共享和通信的任务,如 Web 服务器处理多个客户端请求、图形界面程序中的多任务处理。
- 适合单机多核 CPU 的并行计算。
7. 示例
-
进程:
- 在操作系统中,打开多个应用程序(如浏览器、文本编辑器)会创建多个进程。
- 每个进程有独立的内存空间,互不干扰。
-
线程:
- 在一个浏览器进程中,可能有多个线程分别负责渲染页面、处理网络请求、运行 JavaScript 等。
- 这些线程共享浏览器的内存空间,可以高效通信。
总结对比表
特性 | 进程 | 线程 |
---|---|---|
定义 | 资源分配的基本单位 | CPU 调度的基本单位 |
资源分配 | 独立的内存空间和资源 | 共享进程的内存空间和资源 |
创建/销毁开销 | 较大 | 较小 |
切换开销 | 较大 | 较小 |
独立性 | 进程之间相互独立 | 线程之间共享资源 |
通信机制 | 进程间通信(IPC) | 直接共享内存 |
崩溃影响 | 一个进程崩溃不影响其他进程 | 一个线程崩溃可能导致整个进程崩溃 |
适用场景 | 高隔离性、安全性要求高的任务 | 高效资源共享、通信要求高的任务 |
选择进程还是线程?
- 如果需要高隔离性和安全性,选择进程。
- 如果需要高效资源共享和通信,选择线程。
- 在现代多核 CPU 系统中,通常结合使用进程和线程(如多进程 + 多线程)以实现更高的并发性和性能。
11.并行和并发?
并行就是两个任务同时运行,就是甲任务进行的同时,乙任务也在进行。
并发是指两个任务都请求运行,而处理器只能按受一个任务,就把这两个任务安排轮流
进行,由于时间间隔较短,使人感觉两个任务都在运行
并行(Parallelism) 和 并发(Concurrency) 是两个与多任务处理相关的概念,尽管它们经常被混用,但它们在含义和实现方式上有本质区别。以下是它们的详细比较:
1. 定义
-
并发(Concurrency):
- 并发是指多个任务在同一时间段内交替执行,从宏观上看似乎是同时进行的,但实际上可能是通过快速切换任务来实现的。
- 并发更关注任务的分解和调度,适用于单核或多核系统。
-
并行(Parallelism):
- 并行是指多个任务在同一时刻真正同时执行,通常需要多核或多处理器的硬件支持。
- 并行更关注任务的执行效率,适用于多核系统。
2. 核心区别
-
并发:
- 强调任务的交替执行,通过时间片轮转或任务调度实现。
- 适用于任务之间存在依赖或需要共享资源的场景。
- 目标是提高系统的响应性和资源利用率。
-
并行:
- 强调任务的同时执行,通过多核或多处理器实现。
- 适用于任务之间独立且可以同时执行的场景。
- 目标是提高系统的计算效率和吞吐量。
3. 实现方式
-
并发:
- 在单核 CPU 上,通过操作系统的任务调度机制(如时间片轮转)实现并发。
- 在多核 CPU 上,可以结合并行实现更高效的并发。
- 常见的并发模型:多线程、事件驱动、协程等。
-
并行:
- 需要多核 CPU 或多处理器的硬件支持。
- 常见的并行模型:多进程、多线程、GPU 并行计算等。
4. 适用场景
-
并发:
- I/O 密集型任务(如文件读写、网络请求)。
- 需要快速响应的任务(如用户界面、Web 服务器)。
- 任务之间存在依赖或需要共享资源。
-
并行:
- CPU 密集型任务(如科学计算、图像处理)。
- 任务之间独立且可以同时执行。
- 需要提高计算效率和吞吐量。
5. 示例
-
并发:
- 单核 CPU 上运行多个应用程序(如浏览器、音乐播放器),通过快速切换任务实现并发。
- Web 服务器同时处理多个客户端请求,通过多线程或事件驱动实现并发。
-
并行:
- 多核 CPU 上同时运行多个线程或进程,每个核心执行一个任务。
- 使用 GPU 进行大规模并行计算(如深度学习训练)。
6. 类比
-
并发:
- 类似于一个厨师同时处理多个订单,通过快速切换任务(切菜、煮饭、炒菜)来完成任务。
-
并行:
- 类似于多个厨师同时工作,每个厨师负责一个订单,真正同时完成任务。
7. 总结对比表
特性 | 并发(Concurrency) | 并行(Parallelism) |
---|---|---|
定义 | 多个任务交替执行,宏观上同时 | 多个任务真正同时执行 |
核心目标 | 提高响应性和资源利用率 | 提高计算效率和吞吐量 |
实现方式 | 任务调度、时间片轮转 | 多核 CPU、多处理器 |
硬件要求 | 单核或多核均可 | 需要多核或多处理器 |
适用场景 | I/O 密集型任务、需要快速响应的任务 | CPU 密集型任务、独立任务 |
示例 | 单核 CPU 运行多个应用程序 | 多核 CPU 同时运行多个线程 |
8. 并发与并行的关系
- 并发和并行可以结合使用:
- 在多核 CPU 上,可以通过多线程实现并发,同时利用多核实现并行。
- 例如,一个 Web 服务器可以使用多线程(并发)处理多个请求,同时利用多核 CPU 实现并行计算。
9. 编程模型
-
并发编程:
- 多线程(如 Java 的
Thread
、Python 的threading
)。 - 事件驱动(如 Node.js 的异步 I/O)。
- 协程(如 Python 的
asyncio
、Go 的goroutine
)。
- 多线程(如 Java 的
-
并行编程:
- 多进程(如 Python 的
multiprocessing
)。 - 多线程(如 Java 的
Thread
、C++ 的std::thread
)。 - GPU 并行计算(如 CUDA、OpenCL)。
- 多进程(如 Python 的
总结
- 并发 是逻辑上的同时执行,适用于任务调度和资源共享。
- 并行 是物理上的同时执行,适用于提高计算效率。
- 在实际应用中,通常需要结合并发和并行来实现高效的多任务处理。
12.JVM 的启动是多线程的吗
多线程的。JVM 启动其实至少有三个线程:main 主线程,gc 垃圾回收线程,异常处理线程。当然如果发生异常,会影响主线程
是的,JVM 的启动是多线程的。以下是几个关键点:
- 主线程:负责执行
main
方法,启动应用程序。 - 后台线程:JVM 启动时会创建多个后台线程,用于垃圾回收、JIT 编译等任务。
- 系统线程:包括处理信号、终结器线程等,用于支持 JVM 运行。
主要后台线程
- GC 线程:负责垃圾回收。
- JIT 编译线程:将热点代码编译为本地机器码。
- Finalizer 线程:调用对象的
finalize
方法。 - Reference Handler 线程:处理引用对象(如软引用、弱引用)。
总结
JVM 启动时是多线程的,除了主线程外,还有多个后台线程支持 JVM 的运行和管理。
13.多线程的创建方式?
JDK5 之前:
继承 Thread 类
实现 Runnable 接口
JDK5 之后:
使用 Callable 接口创建线程
通过线程池创建线程
在 Java 中,创建多线程的方式主要有以下几种:
1. 继承 Thread
类
通过继承 Thread
类并重写 run()
方法来实现多线程。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
特点:
- 简单易用。
- 由于 Java 是单继承,继承
Thread
类后会限制扩展其他类。
2. 实现 Runnable
接口
通过实现 Runnable
接口并重写 run()
方法,然后将 Runnable
实例传递给 Thread
对象。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 启动线程
}
}
特点:
- 更灵活,因为可以实现多个接口。
- 适合资源共享的场景(多个线程可以共享同一个
Runnable
实例)。
3. 实现 Callable
接口
通过实现 Callable
接口,可以创建有返回值的线程,通常与 FutureTask
或线程池一起使用。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
return "Thread is running: " + Thread.currentThread().getName();
}
}
public class Main {
public static void main(String[] args) throws Exception {
FutureTask<String> futureTask = new FutureTask<>(new MyCallable());
Thread thread = new Thread(futureTask);
thread.start(); // 启动线程
System.out.println(futureTask.get()); // 获取线程返回值
}
}
特点:
- 可以返回结果。
- 可以抛出异常。
4. 使用线程池
通过线程池(如 ExecutorService
)管理线程,避免频繁创建和销毁线程的开销。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> {
System.out.println("Thread is running: " + Thread.currentThread().getName());
});
executor.shutdown(); // 关闭线程池
}
}
特点:
- 高效管理线程资源。
- 适合需要大量线程的场景。
5. 使用 Lambda 表达式(Java 8+)
结合 Runnable
或 Callable
,可以使用 Lambda 表达式简化代码。
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
System.out.println("Thread is running: " + Thread.currentThread().getName());
});
thread.start();
}
}
特点:
- 代码简洁。
- 适合简单的线程任务。
总结
方式 | 特点 |
---|---|
继承 Thread 类 | 简单,但单继承限制扩展性。 |
实现 Runnable 接口 | 灵活,适合资源共享。 |
实现 Callable 接口 | 支持返回值和异常处理。 |
使用线程池 | 高效管理线程,适合高并发场景。 |
使用 Lambda 表达式 | 代码简洁,适合简单任务。 |
根据具体需求选择合适的方式即可。
14.继承Thread类和实现Runnable接口的对比?
查看源码的区别:
a、继承 Thread : 由于子类重写了 Thread 类的 run(), 当调用 start()时, 直接
找子类的 run()方法。
b、实现 Runnable : 构造函数中传入了 Runnable 的引用, 成员变量记住了它,
start()调用 run()方法时内部判断成员变量 Runnable 的引用是否为空, 不为空编译时看的
是 Runnable 的 run(),运行时执行的是子类的 run()方法(也就是实现了 Runnable 接口并重
写了 run()方法的类中的 run()方法)。
继承 Thread:
好处是:可以直接使用 Thread 类中的方法,代码简单。
弊端是:如果已经有了父类,就不能用这种方法。
实现 Runnable 接口(推荐):
好处是:即使自己定义的线程类有了父类也没关系,因为有了父类也可以实现接口, 而且接口是可以多实现的,避免了单继承的局限性。多个线程可以共享同一个接口实现类的
对象,非常适合多个相同线程来处理同一份资源。
弊端是:不能直接使用 Thread 中的方法需要先获取到线程对象后,才能得到 Thread
的方法,代码复杂
继承 Thread
类和实现 Runnable
接口是 Java 中创建多线程的两种主要方式,它们各有优缺点。以下是它们的详细对比:
1. 继承 Thread
类
实现方式
通过继承 Thread
类并重写 run()
方法。
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start(); // 启动线程
}
}
优点
- 简单易用:直接继承
Thread
类,代码直观。 - 直接调用线程方法:可以直接调用
Thread
类的方法,如start()
、sleep()
等。
缺点
- 单继承限制:Java 是单继承语言,继承
Thread
类后无法再继承其他类,限制了类的扩展性。 - 耦合性高:线程逻辑与线程类绑定,不利于代码复用。
2. 实现 Runnable
接口
实现方式
通过实现 Runnable
接口并重写 run()
方法,然后将 Runnable
实例传递给 Thread
对象。
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread is running: " + Thread.currentThread().getName());
}
}
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 启动线程
}
}
优点
- 灵活性高:可以实现多个接口,扩展性更强。
- 资源共享:多个线程可以共享同一个
Runnable
实例,适合资源共享的场景。 - 解耦:线程逻辑与线程类分离,代码更清晰,易于维护。
缺点
- 稍复杂:需要额外创建
Thread
对象来启动线程。
对比总结
特性 | 继承 Thread 类 | 实现 Runnable 接口 |
---|---|---|
继承限制 | 单继承,无法继承其他类 | 可以实现多个接口,扩展性强 |
资源共享 | 不适合资源共享 | 适合资源共享(多个线程共享一个实例) |
代码复用 | 线程逻辑与线程类绑定,复用性差 | 线程逻辑与线程类分离,复用性强 |
耦合性 | 高耦合 | 低耦合 |
使用场景 | 简单任务,不需要扩展其他类 | 复杂任务,需要资源共享或扩展其他功能 |
代码简洁性 | 直接调用 Thread 方法,代码简洁 | 需要额外创建 Thread 对象,稍复杂 |
推荐使用场景
- 继承
Thread
类:适合简单的线程任务,且不需要扩展其他类。 - 实现
Runnable
接口:适合复杂的任务,需要资源共享或扩展其他功能,推荐优先使用。
示例:资源共享场景
实现 Runnable
接口更适合资源共享的场景,例如多个线程操作同一个计数器:
class Counter implements Runnable {
private int count = 0;
@Override
public void run() {
for (int i = 0; i < 5; i++) {
count++;
System.out.println(Thread.currentThread().getName() + ": " + count);
}
}
}
public class Main {
public static void main(String[] args) {
Counter counter = new Counter();
Thread t1 = new Thread(counter);
Thread t2 = new Thread(counter);
t1.start();
t2.start();
}
15.什么是守护线程?
java 线程中有两种线程,一种是用户线程(非守护线程),一种是守护线程。
守护线程是一种特殊的线程,它具有陪伴的含义。当进程中不存在非守护线程了,则守护线程自动销毁。
典型的就是垃圾回收线程。当进程中没有非守护线程了,则垃圾回收线程没有存在的必要,自动销毁。
守护线程(Daemon Thread)是 Java 中的一种特殊线程,它的生命周期依赖于非守护线程(用户线程)。当所有非守护线程结束时,守护线程会自动终止,无论它是否执行完毕。
守护线程的特点
-
依赖非守护线程:
- 守护线程是为其他线程(非守护线程)提供服务的线程。
- 当所有非守护线程结束时,JVM 会强制终止所有守护线程。
-
不会阻止 JVM 退出:
- 如果只剩下守护线程在运行,JVM 会直接退出,不会等待守护线程完成任务。
-
适合后台任务:
- 守护线程通常用于执行后台任务,如垃圾回收、监控、日志记录等。
守护线程的创建
通过 setDaemon(true)
方法将一个线程设置为守护线程。必须在启动线程之前调用,否则会抛出 IllegalThreadStateException
。
public class DaemonThreadExample {
public static void main(String[] args) {
Thread daemonThread = new Thread(() -> {
while (true) {
System.out.println("Daemon thread is running...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
daemonThread.setDaemon(true); // 设置为守护线程
daemonThread.start();
System.out.println("Main thread is done.");
}
}
输出:
Main thread is done.
Daemon thread is running...
Daemon thread is running...
...
当主线程(非守护线程)结束时,守护线程也会自动终止。
守护线程 vs 非守护线程
特性 | 守护线程 | 非守护线程(用户线程) |
---|---|---|
生命周期 | 依赖非守护线程,随非守护线程结束而终止 | 独立运行,不会因其他线程结束而终止 |
阻止 JVM 退出 | 不会阻止 JVM 退出 | 会阻止 JVM 退出,直到所有非守护线程结束 |
适用场景 | 后台任务(如垃圾回收、监控) | 主要业务逻辑 |
默认类型 | 新创建的线程默认是非守护线程 | 新创建的线程默认是非守护线程 |
设置方法 | setDaemon(true) | 无需设置,默认是非守护线程 |
注意事项
-
不能将正在运行的线程设置为守护线程:
- 必须在调用
start()
方法之前调用setDaemon(true)
,否则会抛出IllegalThreadStateException
。
- 必须在调用
-
守护线程中的
finally
块不一定会执行:- 当 JVM 退出时,守护线程会立即终止,
finally
块中的代码可能不会执行。
- 当 JVM 退出时,守护线程会立即终止,
-
谨慎使用守护线程:
- 守护线程适合执行不重要的任务,如果任务涉及关键操作(如文件写入、数据保存),应使用非守护线程。
示例:守护线程的应用
守护线程常用于执行后台任务,例如定期清理缓存或监控系统状态:
public class CacheCleaner implements Runnable {
@Override
public void run() {
while (true) {
System.out.println("Cleaning cache...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
Thread cleanerThread = new Thread(new CacheCleaner());
cleanerThread.setDaemon(true); // 设置为守护线程
cleanerThread.start();
System.out.println("Main thread is running...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Main thread is done.");
}
}
输出:
Main thread is running...
Cleaning cache...
Cleaning cache...
Main thread is done.
当主线程结束后,守护线程也会自动终止。
总结
- 守护线程是为非守护线程提供服务的后台线程。
- 当所有非守护线程结束时,守护线程会自动终止。
- 适合执行不重要的后台任务,但不能用于关键操作。
16.线程的生命周期?
新建状态(New):当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
就绪状态(Runnable):处于新建状态的线程对象被 start()后,线程即进入就绪状态。
运行状态(Running):当就绪状态的线程被操作系统的任务调度机制调度到,此时线程才得以真正执行,即进入到运行状态。
阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对 CPU 的使用权,停止执行,此时进入阻塞状态,直到其再次进入到就绪状态,才有机会再次被 CPU 调用以进入到运行状态。
死亡状态(Dead):线程完成了它的全部工作或线程被提前强制性地中止或出现异常导致结束。
线程的生命周期是指线程从创建到销毁的整个过程。在 Java 中,线程的生命周期可以分为 5 种状态,这些状态定义在 Thread.State
枚举中。以下是线程生命周期的详细说明:
线程的 5 种状态
- 新建(NEW)
- 就绪(RUNNABLE)
- 运行(RUNNING)
- 阻塞(BLOCKED)
- 等待(WAITING)
- 超时等待(TIMED_WAITING)
- 终止(TERMINATED)
1. 新建状态(NEW)
- 线程被创建,但尚未启动。
- 调用
new Thread()
后,线程处于新建状态。 - 此时线程还未分配系统资源。
Thread thread = new Thread();
System.out.println(thread.getState()); // 输出: NEW
2. 就绪状态(RUNNABLE)
- 线程已经启动,等待 CPU 调度执行。
- 调用
start()
方法后,线程进入就绪状态。 - 此时线程已经分配了系统资源,但还未开始执行。
thread.start();
System.out.println(thread.getState()); // 输出: RUNNABLE
3. 运行状态(RUNNING)
- 线程获得 CPU 时间片,开始执行
run()
方法。 - 运行状态是
RUNNABLE
状态的一个子状态,JVM 没有明确区分就绪和运行状态。
4. 阻塞状态(BLOCKED)
- 线程因为某些原因暂时停止执行,但不会释放锁。
- 常见场景:
- 等待进入
synchronized
代码块或方法。 - 等待 I/O 操作完成。
- 等待进入
synchronized (lock) {
// 其他线程尝试进入时会被阻塞
}
5. 等待状态(WAITING)
- 线程无限期等待,直到被其他线程显式唤醒。
- 常见方法:
Object.wait()
:等待其他线程调用notify()
或notifyAll()
。Thread.join()
:等待目标线程执行完毕。
synchronized (lock) {
lock.wait(); // 线程进入等待状态
}
6. 超时等待状态(TIMED_WAITING)
- 线程等待一段时间,超时后自动唤醒。
- 常见方法:
Thread.sleep(long millis)
:线程休眠指定时间。Object.wait(long timeout)
:等待指定时间。Thread.join(long millis)
:等待目标线程指定时间。
Thread.sleep(1000); // 线程进入超时等待状态
7. 终止状态(TERMINATED)
- 线程执行完毕或异常退出。
- 线程进入终止状态后,无法再次启动。
thread.start();
thread.join(); // 等待线程执行完毕
System.out.println(thread.getState()); // 输出: TERMINATED
线程状态转换图
以下是线程状态之间的转换关系:
NEW → RUNNABLE → RUNNING → TERMINATED
↓
BLOCKED
↓
WAITING
↓
TIMED_WAITING
示例代码:观察线程状态
public class ThreadLifecycleExample {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000); // 进入 TIMED_WAITING 状态
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("After creation: " + thread.getState()); // NEW
thread.start();
System.out.println("After start: " + thread.getState()); // RUNNABLE
Thread.sleep(500);
System.out.println("During sleep: " + thread.getState()); // TIMED_WAITING
thread.join();
System.out.println("After completion: " + thread.getState()); // TERMINATED
}
}
输出:
After creation: NEW
After start: RUNNABLE
During sleep: TIMED_WAITING
After completion: TERMINATED
总结
状态 | 描述 |
---|---|
NEW | 线程被创建,但未启动。 |
RUNNABLE | 线程已启动,等待 CPU 调度。 |
BLOCKED | 线程被阻塞,等待获取锁。 |
WAITING | 线程无限期等待,直到被唤醒。 |
TIMED_WAITING | 线程等待指定时间,超时后自动唤醒。 |
TERMINATED | 线程执行完毕或异常退出。 |
理解线程的生命周期对于编写高效、稳定的多线程程序至关重要。
17.阻塞状态的分类?
根据阻塞产生的原因不同,阻塞状态又可以分为三种:
1.等待阻塞:运行状态中的线程执行 wait()方法,使本线程进入到等待阻塞状态;
2.同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
3.其他阻塞:通过调用线程的 sleep()或 join()或发出了 I/O 请求时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。
在 Java 中,线程的阻塞状态主要可以分为以下几种类型:
1. 等待阻塞(Waiting)
线程在等待某个条件发生时会进入等待阻塞状态。这种状态下的线程不会占用 CPU 资源,直到被其他线程唤醒。常见的等待阻塞情况包括:
Object.wait()
:线程调用对象的wait()
方法后,进入等待状态,直到其他线程调用同一对象的notify()
或notifyAll()
方法。Thread.join()
:线程调用另一个线程的join()
方法,等待该线程完成后再继续执行。LockSupport.park()
:使用LockSupport
类的park()
方法使线程进入等待状态。
2. 阻塞阻塞(Blocked)
当线程试图获取一个已经被其他线程持有的锁时,会进入阻塞阻塞状态。这种状态下的线程会被挂起,直到能够获取到锁。常见的情况包括:
- 同步方法:当一个线程进入一个同步方法时,其他线程如果也想进入这个方法,就会被阻塞,直到第一个线程释放锁。
- 同步块:在
synchronized
块中,如果一个线程已经持有了锁,其他线程尝试进入同一块代码时会被阻塞。
3. 定时阻塞(Timed Waiting)
线程在等待某个条件发生时,可以设置一个超时时间。如果在超时时间内条件没有满足,线程会自动返回。常见的定时阻塞情况包括:
Thread.sleep(milliseconds)
:线程进入睡眠状态,指定的时间后自动唤醒。Object.wait(milliseconds)
:线程在等待状态中,指定的时间后自动返回。Thread.join(milliseconds)
:线程等待另一个线程完成,指定的时间后自动返回。LockSupport.parkNanos(nanos)
和LockSupport.parkUntil(Deadline)
:使用LockSupport
类的定时等待方法。
总结
在 Java 中,线程的阻塞状态可以分为等待阻塞、阻塞阻塞和定时阻塞。每种状态都有其特定的场景和使用方法,理解这些状态有助于更好地管理和优化多线程程序的性能。
18.什么线程安全问题?
多线程并发操作同一数据时,会造成操作的不完整性,会破坏数据;
问题的原因:当多条语句在操作同一个线程共享数据时,一个线程对多条语句只执行了一部分,还没有执行完,另一个线程参与进来执行。导致共享数据的错误。
多个线程操作同一数据,如果当前线程 a 没有操作完该数据,其他线程参与进来执行,那么就会导致数据的错误,这就是线程安全问题;一定要明确的是:同一数据
解决办法:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。
使用同步技术可以解决这种问题, 把操作数据的代码进行同步, 不要多个线程一起操作;
线程安全问题主要指在多线程环境中,多个线程同时访问共享资源(如变量、数据结构等)时,可能导致数据不一致或程序行为异常的情况。以下是一些常见的线程安全问题:
-
竞态条件(Race Condition):
当两个或多个线程同时访问和修改共享数据时,最终的结果依赖于线程执行的顺序。如果没有适当的同步机制,可能会导致数据不一致。 -
死锁(Deadlock):
当两个或多个线程相互等待对方释放资源时,导致所有线程都无法继续执行。死锁通常发生在多个线程需要获取多个资源时。 -
活锁(Livelock):
线程在不断地改变状态以响应其他线程的状态变化,但没有实际进展。虽然线程没有被阻塞,但它们仍然无法完成任务。 -
饥饿(Starvation):
某些线程无法获得所需的资源,导致它们无法执行。饥饿通常发生在资源分配不均的情况下。 -
数据不一致(Data Inconsistency):
当多个线程同时读取和写入共享数据时,可能会导致数据处于不一致的状态。例如,一个线程可能在另一个线程更新数据时读取了旧值。
为了解决这些问题,通常会使用同步机制,如互斥锁(Mutex)、信号量(Semaphore)、读写锁(Read-Write Lock)等,来确保在同一时刻只有一个线程可以访问共享资源。
19.什么是线程同步?
线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下个线程在使用。
每个对象都有一把锁;队列 + 锁 就是用来保证多线程安全性;
线程同步是指在多线程环境中,确保多个线程在访问共享资源时不会发生冲突或数据不一致的机制。由于多个线程可以同时执行,若不加以控制,可能会导致数据竞争、死锁等问题,从而影响程序的正确性和稳定性。
线程同步的常见方法包括:
- 互斥锁(Mutex):通过锁机制,确保同一时间只有一个线程可以访问共享资源。
- 信号量(Semaphore):允许多个线程访问共享资源,但限制同时访问的线程数量。
- 条件变量(Condition Variable):允许线程在某些条件下等待,并在条件满足时被唤醒。
- 读写锁(Read-Write Lock):允许多个线程同时读取共享资源,但在写入时会阻止其他线程的读取和写入。
通过这些机制,线程同步可以有效地避免数据竞争和不一致性,确保程序的正确执行。