java基础面试题(3)
Java 核心知识点整理(常用类、集合、IO 流)
一、常用类(第九章)
1. 基本数据类型与包装类(以 int 和 Integer 为例)
对比维度 | 基本数据类型(int) | 包装类(Integer) |
---|---|---|
用途 | 仅用于定义常量、局部变量;不能用于泛型 ;方法参数、对象成员变量中极少使用 | 可用于泛型;方法参数、对象成员变量中常用;提供丰富工具方法(如类型转换、常量定义) |
存储方式 | 局部变量存于栈 ;成员变量存于 堆 (随对象) | 引用类型,实例对象存于堆 ,栈中存储对象引用地址 |
占用空间 | 占用空间极小(固定 4 字节) | 占用空间更大(包含对象头、数据等额外信息) |
默认值 | 0 | null |
特殊特性 | 无 | 内部自带缓冲区:通过 Integer.valueOf(int) 创建 -128~127 范围内的对象时,复用缓存对象,不新建实例 |
2. 包装类的设计意义与拆装箱
(1)设计原因
基本数据类型不具备面向对象特性,存在实际开发限制:
- 集合(如 List、Set)仅能存储引用类型,无法直接存储基本数据类型
- 泛型仅支持引用类型,不支持基本数据类型
因此为每个基本数据类型提供对应的包装类,实现 “对象化” 处理。
(2)拆装箱概念
- 装箱 :基本数据类型 → 包装类,调用包装类的
valueOf()
方法(如Integer.valueOf(10)
) - 拆箱 :包装类 → 基本数据类型,调用包装类的
xxxValue()
方法(如integer.intValue()
)
(3)自动拆装箱(JDK5+)
JVM 自动完成拆装箱操作,无需手动调用方法(如 Integer i = 10; int num = i;
),但 频繁拆装箱会严重影响系统性能 ,需避免不必要操作。
3. String、StringBuffer、StringBuilder 的区别与联系
特性 | String | StringBuffer | StringBuilder |
---|---|---|---|
可变性 | 不可变 (字符序列创建后无法修改,修改时生成新对象) | 可变 (字符序列可动态修改,不生成新对象) | 可变 |
线程安全 | 线程安全(不可变特性天然保证) | 线程安全 (方法加 synchronized 锁) | 线程不安全 |
执行效率 | 低(频繁修改会产生大量临时对象) | 中(锁机制导致性能损耗) | 高 (无锁,适合单线程场景) |
适用场景 | 字符串不频繁修改(如常量定义、字符串拼接次数少) | 多线程环境下的字符串频繁修改(如日志拼接) | 单线程环境下的字符串频繁修改(如普通业务逻辑字符串处理) |
4. Object 类(所有类的基类)
(1)常用方法
方法名 | 功能描述 |
---|---|
getClass() | 返回当前对象的运行时类对象(Class 实例),用于反射 |
hashCode() | 返回对象的哈希码(int 值),用于确定对象在哈希表中的存储位置 |
equals(Object obj) | 默认比较两个对象的内存地址 (等同于 == ),可重写为比较对象属性 |
toString() | 默认返回 “类名 @哈希码” 格式字符串(如 java.lang.Object@123456 ),可重写为自定义对象描述 |
wait() | 让当前线程进入等待状态,需配合 synchronized 使用 |
notify() | 唤醒当前对象监视器上等待的一个线程,需配合 synchronized 使用 |
(2)==
与 equals()
的区别
对比维度 | == | equals() |
---|---|---|
适用类型 | 基本数据类型、引用数据类型 | 仅引用数据类型 |
比较逻辑 | 基本数据类型:比较值 ;引用数据类型:比较内存地址 | 默认(未重写):比较内存地址(同 == );重写后:通常比较对象 属性值 (如 String 的 equals() 比较字符序列) |
(3)hashCode()
的作用与原理
- 作用 :获取哈希码,用于快速确定对象在哈希表(如 HashMap、HashSet)中的存储位置,提升查找效率。
- 核心原理(以 HashSet 为例) :
- 向 HashSet 添加对象时,先计算对象的
hashCode()
,确定存储位置 - 若该位置无对象,直接存入;若有对象(哈希冲突),调用
equals()
比较两个对象是否真的相同 - 若
equals()
返回true
,则视为重复对象,不存入;若返回false
,则通过 “拉链法” 存储在该位置的链表 / 红黑树中
- 为什么需同时重写
hashCode()
和equals()
:若仅重写equals()
,可能导致 “两个equals()
相等的对象,hashCode()
不同”,从而在哈希表中被视为不同对象,破坏集合去重逻辑。
5. java.sql.Date 与 java.util.Date 的区别
对比维度 | java.util.Date | java.sql.Date |
---|---|---|
继承关系 | 顶层类(直接继承 Object) | 子类 (继承 java.util.Date) |
功能定位 | 通用日期对象,用于 Java 代码中表示日期时间 | 数据库适配类型,用于与数据库的 DATE 类型交互 |
时间精度 | 可表示年月日时分秒 | 仅表示年月日 (时分秒部分被截断为 0) |
二、集合(第十章)
1. 集合框架分类(核心接口)
Java 集合框架基于两大核心接口:Collection
(存储单一元素)和 Map
(存储键值对),分类如下:
顶层接口 | 子接口 / 实现类 | 核心特性 |
---|---|---|
Collection | List | 元素不唯一、有序 (按插入顺序),支持索引访问(如 ArrayList、LinkedList) |
Set | 元素唯一、无序 (按哈希值排序),不支持索引(如 HashSet、TreeSet) | |
Queue | 元素有序、可重复 ,遵循 “先进先出”(FIFO),仅能在尾部添加、头部获取(如 LinkedList、PriorityQueue) | |
Map | HashMap | 键唯一、无序 ,值可重复;底层哈希表,查询效率高 |
TreeMap | 键唯一、有序 (按键排序),值可重复;底层红黑树,支持排序查询 | |
Hashtable | 键唯一、无序 ,值可重复;线程安全,不支持 null 键 / 值 | |
LinkedHashMap | 键唯一、有序 (按插入顺序或访问顺序);底层哈希表 + 双向链表,保留键值对顺序 |
2. 集合底层数据结构总结
集合类 | 底层数据结构 | JDK 版本差异 |
---|---|---|
List | ||
ArrayList | Object[] 数组 | 无 |
Vector | Object[] 数组 | 无(线程安全,方法加 synchronized ) |
LinkedList | 双向链表 | JDK1.6 前为循环链表,JDK1.7 后取消循环 |
Set | ||
HashSet | 基于 HashMap 实现(存储键,值为固定常量) | 无 |
LinkedHashSet | 基于 LinkedHashMap 实现 | 无 |
TreeSet | 红黑树(按键排序) | 无 |
Queue | ||
PriorityQueue | Object[] 数组(基于堆结构) | 无 |
DelayQueue | 基于 PriorityQueue 实现 | 无 |
ArrayDeque | 可扩容动态双向数组 | 无 |
Map | ||
HashMap | 数组 + 链表(JDK1.8 前);数组 + 链表 / 红黑树(JDK1.8 后,链表长度 > 8 时转红黑树) | JDK1.8 优化哈希冲突解决方式 |
LinkedHashMap | 哈希表 + 双向链表(继承 HashMap,保留顺序) | 无 |
Hashtable | 数组 + 链表 | 无 |
TreeMap | 红黑树(按键排序) | 无 |
3. 线程安全的集合
集合类 | 线程安全实现方式 | 特点 |
---|---|---|
Vector | 方法加 synchronized 锁 | 早期类,性能低,不推荐使用 |
Hashtable | 方法加 synchronized 锁 | 不支持 null 键 / 值,性能低,推荐用 ConcurrentHashMap 替代 |
ConcurrentHashMap | JDK1.7:分段锁(Segment);JDK1.8:CAS+ synchronized(粒度更细) | 高性能,支持并发访问,推荐用于多线程场景 |
4. Collection 与 Collections 的区别
对比维度 | Collection | Collections |
---|---|---|
类型 | 接口 | 工具类 (java.util 包下) |
作用 | 定义集合通用操作(如 add() 、remove() 、isEmpty() ),是 List、Set、Queue 的父接口 | 提供静态工具方法,用于操作集合(如排序、查找、同步控制) |
使用方式 | 作为集合类的父接口,通过实现类(如 ArrayList)使用 | 直接调用静态方法(如 Collections.sort(list) ) |
5. 集合遍历方法
-
普通 for 循环 :仅适用于 List(支持索引访问),如:
java运行
for (int i = 0; i < list.size(); i++) {System.out.println(list.get(i));}
-
for-each 循环 :适用于所有 Collection(List、Set、Queue)和 Map(需遍历键 / 值集),简洁但无法获取索引,如:
java运行
for (String str : list) {System.out.println(str);}
-
迭代器(Iterator) :适用于所有 Collection,支持遍历中删除元素(避免
ConcurrentModificationException
),如:
java运行
Iterator<String> iterator = list.iterator();while (iterator.hasNext()) {String str = iterator.next();if ("delete".equals(str)) {iterator.remove(); // 安全删除}}
6. 核心集合类对比
(1)Vector 与 ArrayList
对比维度 | Vector | ArrayList |
---|---|---|
线程安全 | 安全(方法加 synchronized ) | 不安全 |
扩容机制 | 默认扩容为原容量的2 倍 | 默认扩容为原容量的1.5 倍 |
版本 | 早期 JDK 类(JDK1.0) | 替代 Vector 的新类(JDK1.2) |
性能 | 低(锁开销) | 高(无锁) |
适用场景 | 多线程环境(不推荐,优先用 ConcurrentArrayList) | 单线程环境 |
(2)ArrayList 与 LinkedList
对比维度 | ArrayList | LinkedList |
---|---|---|
底层结构 | Object[] 数组 | 双向链表 |
线程安全 | 不安全 | 不安全 |
访问效率 | 查询快 (索引直接访问,时间复杂度 O (1)) | 查询慢 (需遍历链表,时间复杂度 O (n)) |
增删效率 | 增删慢 (数组扩容、元素移位,时间复杂度 O (n)) | 增删快 (仅修改指针,时间复杂度 O (1),需先找到位置时 O (n)) |
空间占用 | 需分配连续内存,可能存在内存浪费(扩容预留空间) | 每个节点存储元素 + 前后指针,空间占用更灵活 |
适用场景 | 频繁随机访问、尾部增删 | 频繁中间增删、无需随机访问 |
(3)HashMap 与 Hashtable
对比维度 | HashMap | Hashtable |
---|---|---|
线程安全 | 不安全 | 安全(方法加 synchronized ) |
null 支持 | 允许 null 键和 null 值 | 不允许 null 键和 null 值 |
继承关系 | 实现 Map 接口 | 继承 Dictionary 类,实现 Map 接口 |
版本 | JDK1.2(新类) | JDK1.0(早期类) |
性能 | 高(无锁) | 低(锁开销) |
扩容机制 | 默认初始容量 16,扩容为原容量 2 倍 | 默认初始容量 11,扩容为原容量 2 倍 + 1 |
(4)HashMap 与 HashSet
对比维度 | HashMap | HashSet |
---|---|---|
存储结构 | 键值对(key-value) | 单一元素(仅存储 key) |
底层实现 | 哈希表(数组 + 链表 / 红黑树) | 基于 HashMap 实现 (元素作为 key,value 为固定常量 PRESENT ) |
去重逻辑 | 基于 key 的 hashCode() 和 equals() | 基于元素的 hashCode() 和 equals() (同 HashMap 的 key 去重) |
核心方法 | put(key, value) | add(element) (本质是 hashMap.put(element, PRESENT) ) |
7. Comparable 与 Comparator 的区别(排序接口)
对比维度 | Comparable | Comparator |
---|---|---|
所在包 | java.lang | java.util |
核心方法 | compareTo(Object obj) (自身与参数比较) | compare(Object obj1, Object obj2) (两个参数比较) |
实现方式 | 需被比较对象的类实现(如 class User implements Comparable<User> ) | 需单独定义比较器类 (无需修改被比较对象的源码) |
排序灵活性 | 仅支持单一排序规则 (类中固定) | 支持多排序规则 (可定义多个比较器) |
使用场景 | 固定排序逻辑(如 User 类默认按 ID 升序) | 动态排序逻辑(如有时按年龄升序,有时按姓名降序) |
8. 集合关键概念
(1)无序性与不可重复性(以 Set 为例)
- 无序性 :非 “随机性”,指元素在底层数组中并非按插入顺序存储,而是根据元素的
hashCode()
计算存储位置。 - 不可重复性 :指添加的元素通过
equals()
判断为true
时,视为重复元素,不允许存入(如 HashSet 的去重逻辑)。
(2)集合判空
判断集合是否为空, 优先使用 isEmpty()
方法 ,而非 size() == 0
:
isEmpty()
:直接判断内部元素是否为空,效率更高(部分集合底层直接存储空状态标识)size() == 0
:需计算元素个数,再与 0 比较,效率略低- 可读性:
isEmpty()
语义更清晰(“是否为空” vs “大小是否为 0”)
(3)Collection 转 Map(Stream 流)
使用 Collectors.toMap()
转换时,需注意:
- 若 value 为
null
,会抛出NullPointerException
(Map 接口的部分实现类不支持 null 值,如 HashMap 支持,但toMap()
内部逻辑会校验) - 若存在重复 key,会抛出
IllegalStateException
,需通过(k1, k2) -> k1
指定冲突解决策略(如保留第一个 key)
9. Collections 工具类常用方法
(1)排序操作
void reverse(List list)
:反转 List 中元素的顺序void shuffle(List list)
:随机打乱 List 中元素的顺序void sort(List list)
:按自然顺序(Comparable)对 List 升序排序void sort(List list, Comparator c)
:按自定义比较器(Comparator)对 List 排序
(2)查找与替换
int binarySearch(List list, Object key)
:对有序 List进行二分查找,返回元素索引(未找到返回负数)Object max(Collection coll)
:返回 Collection 中按自然顺序排序的最大元素Object min(Collection coll)
:返回 Collection 中按自然顺序排序的最小元素boolean replaceAll(List list, Object oldVal, Object newVal)
:将 List 中所有 oldVal 替换为 newVal
(3)同步控制(不推荐)
提供 Collections.synchronizedXXX()
方法,将普通集合包装为线程同步集合(如 Collections.synchronizedList(new ArrayList<>())
),但 性能极低 (全局锁),推荐使用 JUC 包下的并发集合(如 CopyOnWriteArrayList、ConcurrentHashMap)。
三、IO流(第十一章)
1. IO流的概念与分类
(1)基本概念
IO(Input/Output,输入/输出)指数据在内存与外部存储(如文件、数据库、网络) 之间的传输过程。因数据传输具有“连续性”,类似水流,故称为“IO流”。
- 输入流:数据从外部存储→内存(读操作,如读取本地文件到程序);
- 输出流:数据从内存→外部存储(写操作,如将程序数据写入本地文件)。
(2)核心分类
IO流按不同维度可分为3类,具体如下:
分类维度 | 具体类型 | 说明 |
---|---|---|
传输方向 | 输入流(InputStream/Reader) | 读数据,数据源→内存,如 FileInputStream (读文件)、BufferedReader (缓冲读) |
输出流(OutputStream/Writer) | 写数据,内存→数据源,如 FileOutputStream (写文件)、BufferedWriter (缓冲写) | |
处理单位 | 字节流(InputStream/OutputStream) | 以“字节”为单位传输(1字节=8位),可处理所有类型文件(如图片、视频、文本) |
字符流(Reader/Writer) | 以“字符”为单位传输(依赖编码,如UTF-8、GBK),仅用于处理文本文件,避免乱码 | |
功能(角色) | 节点流(直接操作数据源) | 直接连接数据源/目标,如 FileInputStream (直接读文件)、FileWriter (直接写文件) |
处理流(包装节点流) | 基于节点流扩展功能(如缓冲、编码转换),如 BufferedInputStream (缓冲字节流)、InputStreamReader (字节转字符) |
2. 字节流与字符流的区别(为什么分两类)
对比维度 | 字节流(InputStream/OutputStream) | 字符流(Reader/Writer) |
---|---|---|
处理单位 | 字节(8位) | 字符(如UTF-8中1个汉字=3字节) |
适用场景 | 所有文件(图片、视频、文本等) | 仅文本文件(.txt、.java等) |
编码依赖 | 不依赖编码,直接操作二进制数据 | 依赖编码(由JVM自动完成“字节→字符”转换),避免乱码 |
核心问题 | 处理文本时易乱码(如未指定编码) | 无法处理非文本文件(如图片会损坏) |
核心原因:文本文件需考虑“编码格式”(如UTF-8、GBK),字节流直接传输二进制数据,若未手动处理编码,易出现乱码;字符流内置编码转换逻辑,可自动适配编码,更适合文本处理。
3. 序列化与反序列化
(1)基本概念
当需要持久化Java对象(如存到文件)或网络传输Java对象(如RPC调用)时,需将“内存中的对象”转换为“可传输/存储的二进制数据”,此过程即序列化与反序列化:
- 序列化:内存中的Java对象 → 平台无关的二进制数据(如字节数组),便于存储/传输;
- 反序列化:二进制数据 → 内存中的Java对象,恢复对象的属性和状态。
(2)实现步骤与注意事项
实现序列化需满足以下条件,否则会抛出 NotSerializableException
:
- 实现
Serializable
接口:该接口是“标记接口”(无任何抽象方法),仅用于告诉JVM“该类可序列化”; - 所有成员属性需可序列化:若对象的某个成员是自定义对象类型,该自定义类也必须实现
Serializable
; - 排除无需序列化的属性:用
transient
关键字修饰(而非static
,static
是类属性,本身不参与对象序列化); - 指定序列化ID(
serialVersionUID
):- 作用:保证序列化与反序列化时“对象版本一致”。JVM会对比二进制数据中的
serialVersionUID
与本地类的serialVersionUID
,不一致则抛出InvalidClassException
; - 示例:
private static final long serialVersionUID = 1L;
(建议显式定义,避免JVM自动生成导致版本冲突)。
- 作用:保证序列化与反序列化时“对象版本一致”。JVM会对比二进制数据中的
(3)排除属性的方式
- 用
transient
关键字修饰属性:被修饰的属性不会参与序列化,反序列化后值为默认值(如int
为0,String
为null
); - 示例:
private transient String password;
(密码无需序列化,避免泄露)。
(4)自定义实现方案(加分)
Java默认序列化存在缺陷:安全漏洞(易被反序列化攻击)、不跨语言(仅Java可解析)、性能差。实际开发中常用主流序列化框架:
- FastJson:阿里开源,JSON格式序列化,跨语言、性能高;
- Protobuf:Google开源,二进制格式,压缩率高、性能极强,适合网络传输;
- Hessian:轻量级二进制序列化,支持跨语言(Java、Python等)。
4. 常用IO流(5对核心流)
IO流的命名有规律(如 FileXXX
表示节点流,BufferedXXX
表示缓冲处理流),常用核心流如下:
流类型 | 输入流(读) | 输出流(写) | 适用场景 |
---|---|---|---|
字节节点流 | FileInputStream | FileOutputStream | 读/写任意文件(图片、视频等) |
字节缓冲流 | BufferedInputStream | BufferedOutputStream | 包装字节节点流,提升读写效率 |
字符节点流 | FileReader | FileWriter | 读/写文本文件(默认编码) |
字符缓冲流 | BufferedReader (含 readLine() ) | BufferedWriter (含 newLine() ) | 包装字符节点流,支持按行读写 |
字节转字符流 | InputStreamReader (指定编码) | OutputStreamWriter (指定编码) | 字节流→字符流,解决编码问题 |
5. 缓冲流的原理与优点
缓冲流(BufferedInputStream
/BufferedOutputStream
、BufferedReader
/BufferedWriter
)是典型的“处理流”,核心作用是减少IO次数,提升性能。
(1)原理对比
- 无缓冲流(如
FileInputStream
):读1个字节/字符 → 立即写入目标(如文件),“读1次写1次”,频繁操作硬盘,效率低; - 缓冲流:内置一个8KB(8*1024字节)的缓冲区(字节缓冲流)或字符缓冲区,读数据时先存入缓冲区,当缓冲区满/手动刷新时,一次性写入目标,大幅减少硬盘IO次数。
(2)核心优点
- 提升性能:减少硬盘读写次数(硬盘IO是“慢操作”,内存缓冲是“快操作”);
- 简化操作:字符缓冲流提供便捷方法(如
BufferedReader.readLine()
按行读,BufferedWriter.newLine()
换行); - 保护硬件:减少硬盘频繁读写,降低硬件损耗。
四、多线程(第十二章)
1. 程序、进程、线程的区别
三者是“包含关系”,核心区别在于“资源占用”和“执行粒度”:
概念 | 定义 | 资源占用 | 核心特点 |
---|---|---|---|
程序 | 用某种语言编写的“静态指令集合”(如 Hello.java ) | 不占用资源(未运行) | 静态、无生命周期 |
进程 | 系统运行程序的“动态实例”(如双击运行 Hello.exe ),是资源分配的基本单位 | 占用独立内存、CPU等资源 | 动态、有生命周期(启动→运行→消亡),进程间资源隔离 |
线程 | 进程内的“执行路径”,是资源调度的基本单位(一个进程可包含多个线程) | 共享进程的内存、资源(如堆) | 轻量级、切换成本低(比进程小1-2个数量级) |
示例:打开Chrome浏览器是1个进程,浏览器中的“标签页”“下载任务”是该进程下的多个线程。
2. 创建线程的4种方式
(1)继承 Thread
类
- 步骤:继承
Thread
→ 重写run()
(定义线程任务) → 创建实例→调用start()
(启动线程,JVM调用run()
); - 示例:
class MyThread extends Thread {@Overridepublic void run() {System.out.println("线程任务:继承Thread");} } // 启动 new MyThread().start();
- 缺点:Java单继承,继承
Thread
后无法再继承其他类。
(2)实现 Runnable
接口
- 步骤:实现
Runnable
→ 重写run()
→ 把Runnable
实例传给Thread
构造器→调用start()
; - 示例:
class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("线程任务:实现Runnable");} } // 启动 new Thread(new MyRunnable()).start();
- 优点:支持多实现,可继承其他类,解耦“任务”与“线程”。
(3)实现 Callable
接口(带返回值)
- 特点:
Runnable
的增强版,call()
方法可返回值、抛出异常;需配合FutureTask
(实现Runnable
)使用; - 步骤:实现
Callable<T>
→ 重写call()
→ 封装为FutureTask
→ 传给Thread
→调用start()
→用get()
获取返回值; - 示例:
class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {return 100; // 返回值} } // 启动与获取结果 FutureTask<Integer> task = new FutureTask<>(new MyCallable()); new Thread(task).start(); Integer result = task.get(); // 阻塞获取返回值
(4)使用线程池(推荐,JDK1.5+)
- 核心类:
ThreadPoolExecutor
(自定义线程池)、Executors
(工具类,快速创建线程池,如Executors.newFixedThreadPool(5)
); - 优点:
- 降低资源消耗(复用线程,避免频繁创建/销毁线程);
- 提高响应速度(线程已创建,无需等待初始化);
- 便于管理(控制线程数量、任务队列等);
- 示例:
// 创建固定线程数的线程池 ExecutorService pool = Executors.newFixedThreadPool(3); // 提交任务 pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("线程池任务");} }); // 关闭线程池 pool.shutdown();
3. 线程的启动与停止
(1)启动线程
- 必须调用
Thread
的start()
方法,而非直接调用run()
; - 原因:
start()
会向JVM注册线程,由JVM调度CPU时间片后执行run()
;直接调用run()
只是“普通方法调用”,不会启动新线程。
(2)停止线程(安全方式)
- 不推荐的方式:
stop()
(已废弃)、suspend()
/resume()
(易导致死锁),会强制终止线程,释放锁资源,导致数据不一致; - 推荐的方式:用“中断标记”控制线程退出(
interrupt()
+isInterrupted()
),或通过volatile
变量标记; - 示例(中断标记):
class StopThread implements Runnable {@Overridepublic void run() {// 检查线程是否被中断while (!Thread.currentThread().isInterrupted()) {try {Thread.sleep(1000); // 阻塞时,interrupt()会抛出异常System.out.println("线程运行中");} catch (InterruptedException e) {// 捕获异常后,需手动重置中断标记(否则isInterrupted()会返回false)Thread.currentThread().interrupt();break; // 退出循环,停止线程}}} } // 停止线程 Thread t = new Thread(new StopThread()); t.start(); Thread.sleep(3000); t.interrupt(); // 标记线程中断
4. 线程的生命周期(7种状态,JDK官方定义)
线程从创建到消亡的完整状态流转,由 Thread.State
枚举定义,共7种状态:
状态 | 说明 | 触发条件(状态转换) |
---|---|---|
新建(New) | 线程对象已创建(new Thread() ),但未调用 start() | 调用 start() → 就绪(Runnable) |
就绪(Runnable) | 线程已注册到JVM,等待CPU时间片(具备执行条件,但未执行) | CPU分配时间片 → 运行(Running) |
运行(Running) | 线程获得CPU时间片,执行 run() 方法中的任务 | 时间片用完 → 就绪;调用 wait() → 等待;竞争锁失败 → 阻塞 |
阻塞(Blocked) | 线程因“竞争同步锁失败”(如 synchronized )或“I/O操作”暂停执行 | 锁释放 → 就绪;I/O完成 → 就绪 |
等待(Waiting) | 线程主动释放CPU(如 wait() 、join() ),需被其他线程唤醒 | 其他线程调用 notify() /notifyAll() → 就绪 |
超时等待(Timed Waiting) | 线程主动释放CPU,但有超时时间(如 sleep(1000) 、wait(1000) ) | 超时时间到 → 就绪;其他线程唤醒 → 就绪 |
终止(Terminated) | 线程执行完毕(run() 结束)或异常终止(抛出未捕获异常) | 无(状态不可逆,无法重启) |
简化版5种状态:将“等待、超时等待”归为“阻塞”,即:新建 → 就绪 → 运行 → 阻塞 → 终止。
5. 线程阻塞(Blocked)与等待(Waiting)的区别
对比维度 | 阻塞(Blocked) | 等待(Waiting) |
---|---|---|
触发条件 | 竞争同步锁失败(如 synchronized )、I/O操作 | 调用 wait() (无超时)、join() (无超时) |
资源竞争 | 会竞争锁资源,等待锁释放 | 不竞争锁资源,主动放弃CPU |
唤醒机制 | 锁释放后,JVM自动唤醒(无需显式调用) | 需其他线程显式调用 notify() /notifyAll() |
典型场景 | 多线程竞争同一 synchronized 锁 | 线程A等待线程B执行完毕(B.join() ) |
6. wait()
与 sleep()
的区别(高频)
对比维度 | wait() | sleep(long millis) |
---|---|---|
所属类 | Object 类(所有对象都有) | Thread 类(静态方法) |
锁释放 | 调用后立即释放持有的对象锁 | 不释放任何锁(即使持有锁) |
使用场景 | 多线程间通信(如生产者-消费者模型) | 线程休眠(如模拟延迟) |
调用条件 | 必须在 synchronized 块/方法中(需持有对象锁) | 可在任意代码中调用(无需锁) |
唤醒方式 | 其他线程调用 notify() /notifyAll() ,或超时(wait(long) ) | 超时自动唤醒,或 interrupt() 中断 |
7. 多线程的意义与问题
(1)为什么用多线程?
多线程的核心价值在于提升资源利用率和满足业务场景需求,具体可从底层原理和实际开发两方面分析:
1. 底层:充分利用硬件资源,降低执行成本
- CPU利用率最大化:现代计算机多为多核CPU,若采用单线程,仅能使用一个核心,其他核心处于空闲状态(如单线程程序执行IO操作时,CPU会闲置等待)。多线程可将任务分配到多个核心,让CPU“并行工作”,避免资源浪费。示例:文件下载时,单线程需等待“读取网络数据→写入本地文件”完成后再继续;多线程可拆分文件块,同时从网络读取不同块并写入,CPU和IO设备并行运作。
- 线程切换成本低于进程:线程是“轻量级进程”,共享所属进程的内存空间(如堆、方法区),切换时无需重新分配内存和资源;而进程切换需切换整个地址空间,成本是线程的1-2个数量级。多线程可在同一进程内快速切换,提升系统响应速度。
2. 业务:满足高并发、异步化、复杂场景需求
- 高并发处理:互联网系统需同时应对大量请求(如电商秒杀、直播弹幕),单线程无法及时处理所有请求,会导致响应超时。多线程可并行处理多个请求,提升系统吞吐量(单位时间处理的请求数)。示例:Tomcat服务器通过“线程池”管理线程,每个请求分配一个线程处理,支持同时处理上千个客户端连接。
- 异步化解耦:避免“同步阻塞”导致的流程卡顿,将耗时操作(如IO、数据库查询)异步执行,主线程可继续处理其他任务,提升用户体验。示例:用户注册时,主线程完成“数据校验→写入数据库”后立即返回“注册成功”,同时启动子线程异步发送“欢迎短信”,无需用户等待短信发送完成。
- 复杂任务拆分:将大型任务拆分为多个子任务,用多线程并行执行,缩短总耗时。
示例:Excel数据导入时,单线程需逐行读取并处理;多线程可按行号拆分数据块,每个线程处理一块数据,处理时间从“10分钟”缩短至“2分钟”。
(2)线程并发可能带来的问题(高频)
多线程虽能提升效率,但因“线程共享进程资源”且“CPU调度无序”,会引入线程安全和资源竞争相关问题,常见如下:
1. 线程不安全:数据一致性问题
当多个线程同时操作“共享变量”(如静态变量、堆中对象属性)时,若未加同步控制,会导致“脏读”“幻读”“数据覆盖”等问题。
示例:两个线程同时对“count=0”执行 count++
(实际分3步:读取count→加1→写入count),可能出现“线程1读count=0,线程2也读count=0,最终两者都写入1,count=1而非2”的结果。
2. 死锁:线程永久阻塞
- 定义:多个线程互相持有对方需要的资源(如锁),且都不释放,导致所有线程永久阻塞,程序无法继续执行。
- 死锁产生的4个必要条件(缺一不可):
- 互斥条件:资源只能被一个线程占用(如
synchronized
锁); - 请求与保持条件:线程持有一个资源后,又请求其他资源,且不释放已持有资源;
- 不剥夺条件:线程已持有的资源,不能被其他线程强制剥夺;
- 循环等待条件:多个线程形成“资源请求循环”(如线程A等线程B的锁,线程B等线程A的锁)。
- 互斥条件:资源只能被一个线程占用(如
示例:线程1持有锁A,请求锁B;线程2持有锁B,请求锁A,两者互相等待,形成死锁。
3. 内存泄漏:资源无法回收
线程未正确关闭或资源未释放,导致JVM无法回收内存,长期积累会导致内存溢出(OOM)。
示例:线程中创建的 ThreadLocal
变量未调用 remove()
,线程结束后 ThreadLocal
实例仍被线程的 ThreadLocalMap
引用,若线程是线程池中的复用线程,内存会持续泄漏。
4. 上下文切换开销
CPU调度线程时,需保存当前线程的“上下文信息”(如程序计数器、寄存器值),切换到新线程时再恢复其上下文。频繁的上下文切换会消耗CPU资源,反而降低程序效率。
示例:线程数远超CPU核心数(如1000个线程跑在8核CPU上),CPU会频繁切换线程,大部分时间用于保存/恢复上下文,而非执行任务。