Java基础知识点汇总(六)
一、volatile是什么?
在Java中,volatile
是一个关键字,用于修饰变量,主要作用是保证变量的可见性和禁止指令重排序,但不保证原子性。
1. volatile的核心作用
- 可见性:当一个变量被声明为
volatile
时,它的修改会被立即同步到主内存,且其他线程读取该变量时会直接从主内存获取最新值(而非线程本地缓存)。 - 禁止指令重排序:编译器和CPU不会对
volatile
变量的读写操作进行重排序优化,确保代码执行顺序与预期一致。
2. 原理:内存屏障(Memory Barrier)
volatile
的实现依赖于CPU的内存屏障指令,JVM通过插入内存屏障来保证其语义:
- 写操作后:插入StoreStore屏障(禁止之前的普通写操作被重排序到volatile写之后)和StoreLoad屏障(防止volatile写与后面可能的读操作重排序)。
- 读操作前:插入LoadLoad屏障(禁止volatile读与后面的普通读重排序)和LoadStore屏障(禁止volatile读与后面的普通写重排序)。
这些屏障确保了:
- volatile变量的写操作对其他线程可见。
- 代码执行顺序符合程序的逻辑顺序。
3. 使用场景
- 状态标记(如
boolean isRunning
)。 - 双重检查锁定(DCL)中的单例对象(如
instance = new Singleton()
)。
注意:volatile
不适合依赖前值的操作(如i++
),这类场景需要 synchronized
或原子类(AtomicInteger
)保证原子性。
例如,一个简单的volatile使用示例:
public class VolatileExample {private volatile boolean flag = false;public void setFlag(boolean value) {flag = value; // volatile写操作}public void checkFlag() {while (!flag) { // volatile读操作,始终获取最新值// 执行逻辑}}
}
这里flag
的修改会被立即同步,确保checkFlag()
能及时感知状态变化。
二、如何保证线程安全?
在Java中,保证线程安全的核心是解决多线程并发访问共享资源时可能出现的竞态条件(Race Condition) 和内存可见性问题。常见的线程安全保障手段可以分为以下几类:
1. 原子性保障
确保操作的不可分割性,即一个操作要么完全执行,要么完全不执行,中间不会被其他线程打断。
-
使用
synchronized
关键字
最经典的线程安全保障方式,通过锁机制实现:- 修饰方法:锁住当前对象实例(非静态方法)或类对象(静态方法)。
- 修饰代码块:显式指定锁对象(如
synchronized (lockObj) { ... }
)。
public class SynchronizedExample {private int count = 0;// 同步方法(锁为当前对象)public synchronized void increment() {count++; // 原子化执行} }
-
使用原子类(
java.util.concurrent.atomic
)
基于CPU的CAS(Compare-And-Swap) 指令实现,无锁且高效,适合简单的数值操作:import java.util.concurrent.atomic.AtomicInteger;public class AtomicExample {private AtomicInteger count = new AtomicInteger(0);public void increment() {count.incrementAndGet(); // 原子自增} }
2. 可见性保障
确保一个线程对共享变量的修改能被其他线程立即感知。
volatile
关键字
如前文所述,通过内存屏障保证变量的读写可见性和禁止指令重排序,但不保证原子性,适合作为状态标记:public class VolatileExample {private volatile boolean isRunning = true;public void stop() {isRunning = false; // 修改立即同步到主内存}public void run() {while (isRunning) { // 直接从主内存读取最新值// 执行逻辑}} }
3. 有序性保障
防止编译器或CPU对指令进行重排序,确保代码执行顺序与预期一致。
-
synchronized
和volatile
的隐式保障synchronized
:同步块内的代码视为一个整体,禁止重排序。volatile
:通过内存屏障禁止其前后指令的重排序(如DCL单例模式中对实例的修饰)。
-
显式锁(
java.util.concurrent.locks
)
ReentrantLock
等锁机制不仅提供与synchronized
类似的原子性保障,还支持更灵活的功能(如中断、超时、公平锁):import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock;public class LockExample {private final Lock lock = new ReentrantLock();private int count = 0;public void increment() {lock.lock();try {count++; // 受锁保护的原子操作} finally {lock.unlock(); // 必须在finally中释放锁}} }
4. 避免共享资源(无状态设计)
最彻底的线程安全方式:如果多个线程不共享资源,自然不会有线程安全问题。
- 局部变量:线程私有,无需同步。
- 不可变对象(
final
):对象创建后状态不可修改,天然线程安全(如String
、Integer
)。public class ImmutableExample {private final int value; // 不可变变量public ImmutableExample(int value) {this.value = value;}public int getValue() {return value; // 只读操作,线程安全} }
5. 并发工具类
JUC(java.util.concurrent
)提供了多种线程安全的容器和工具:
- 线程安全容器:
ConcurrentHashMap
、CopyOnWriteArrayList
等,通过分段锁、写时复制等机制实现高效并发。 - 线程池:
ExecutorService
管理线程生命周期,避免频繁创建线程的开销。 - 同步工具:
CountDownLatch
、Semaphore
、CyclicBarrier
等,协调多线程执行顺序。
总结:
选择哪种方式取决于具体场景:
- 简单场景:优先使用
synchronized
(JVM优化成熟)或原子类。 - 复杂场景(如中断、超时):使用
ReentrantLock
。 - 状态标记:使用
volatile
。 - 高性能并发容器:使用JUC提供的线程安全集合。
核心原则:最小化同步范围,在保证线程安全的同时减少性能损耗。
三、IO 模型有哪五种?
在计算机网络和操作系统中,IO模型主要用于描述程序如何处理输入输出操作,尤其是网络IO。常见的五种IO模型如下:
1. 阻塞IO(Blocking IO)
- 特点:应用程序发起IO操作后,会一直阻塞等待,直到数据准备完成并拷贝到用户空间才返回。
- 过程:
- 调用
recvfrom
等IO函数,内核开始准备数据(如等待网络数据到达)。 - 数据未准备好时,进程进入阻塞状态,释放CPU。
- 数据准备完成后,内核将数据从内核空间拷贝到用户空间。
- 拷贝完成,IO函数返回,进程恢复运行。
- 调用
- 典型场景:传统的Socket编程(如Java BIO)。
2. 非阻塞IO(Non-blocking IO)
- 特点:应用程序发起IO操作后,若数据未准备好,会立即返回错误(而非阻塞),进程可继续执行其他任务,需通过轮询检查数据是否就绪。
- 过程:
- 调用
recvfrom
,若数据未准备好,返回EWOULDBLOCK
错误。 - 进程不断轮询调用IO函数,直到数据准备好。
- 数据就绪后,内核将数据拷贝到用户空间,函数返回成功。
- 调用
- 缺点:轮询会消耗大量CPU资源。
3. IO多路复用(IO Multiplexing)
- 特点:通过一个内核对象(如
select
/poll
/epoll
)同时监听多个IO事件,当某个IO就绪时,通知进程处理,避免了阻塞单个IO或无效轮询。 - 过程:
- 进程将多个IO描述符注册到多路复用器(如
epoll
)。 - 调用
epoll_wait
等函数阻塞等待,内核监控所有注册的IO。 - 当任一IO数据就绪,内核通知进程,进程再发起IO操作(如
recvfrom
)拷贝数据。
- 进程将多个IO描述符注册到多路复用器(如
- 优势:单进程可高效管理多个连接,是高并发服务器的核心模型(如Java NIO的
Selector
)。
4. 信号驱动IO(Signal-driven IO)
- 特点:进程通过
sigaction
注册信号处理函数,发起IO操作后不阻塞,当数据就绪时,内核发送SIGIO
信号通知进程处理。 - 过程:
- 进程注册信号处理函数,调用
fcntl
开启信号驱动IO。 - 内核准备数据,完成后发送信号给进程。
- 进程捕获信号,在信号处理函数中调用IO函数拷贝数据。
- 进程注册信号处理函数,调用
- 缺点:信号处理逻辑复杂,实际应用较少。
5. 异步IO(Asynchronous IO)
- 特点:进程发起IO操作后立即返回,内核完成数据准备和拷贝到用户空间的全流程后,再通知进程操作完成(“通知结果”而非“通知就绪”)。
- 过程:
- 调用
aio_read
等异步IO函数,传入回调通知方式。 - 进程继续执行,内核后台完成数据准备和拷贝。
- 操作全部完成后,内核通过信号或回调通知进程。
- 调用
- 优势:全程无阻塞,是最理想的IO模型(如Java AIO、Linux的
io_uring
)。
核心区别总结:
模型 | 阻塞阶段 | 适用场景 |
---|---|---|
阻塞IO | 数据准备 + 数据拷贝 | 简单场景,低并发 |
非阻塞IO | 仅数据拷贝(轮询消耗CPU) | 实时性要求高的场景 |
IO多路复用 | 仅等待就绪(单线程管多连接) | 高并发服务器(如Nginx) |
信号驱动IO | 仅数据拷贝 | 较少使用 |
异步IO | 无阻塞(内核全程处理) | 高性能、低延迟场景 |
其中,IO多路复用和异步IO是高性能网络编程的主流选择。
四、深拷贝、浅拷贝是什么?
在编程中,深拷贝(Deep Copy)和浅拷贝(Shallow Copy)是针对对象复制的两种方式,核心区别在于是否复制对象内部的引用类型成员。
1. 浅拷贝(Shallow Copy)
- 定义:只复制对象本身(基本类型字段直接复制值),但对于对象内部的引用类型字段(如数组、其他对象),仅复制其引用地址(即新对象与原对象共享同一份引用类型数据)。
- 特点:
- 基本类型字段(int、float、boolean等)会被真正复制,修改新对象的基本类型字段不影响原对象。
- 引用类型字段(如对象、数组)仅复制引用,新对象和原对象的引用字段指向同一块内存,修改其中一个会影响另一个。
- 示例(Java):
class Person {String name; // 引用类型int age; // 基本类型public Person(String name, int age) {this.name = name;this.age = age;} }// 浅拷贝实现(通过构造方法示例) Person original = new Person("Alice", 20); Person shallowCopy = new Person(original.name, original.age);// 修改引用类型字段:会影响原对象 shallowCopy.name = "Bob"; System.out.println(original.name); // 输出 "Bob"(因为共享同一份String引用)// 修改基本类型字段:不影响原对象 shallowCopy.age = 30; System.out.println(original.age); // 输出 "20"
2. 深拷贝(Deep Copy)
- 定义:不仅复制对象本身和基本类型字段,还会对对象内部的所有引用类型字段递归复制,即新对象与原对象的引用类型字段指向完全独立的内存空间。
- 特点:
- 新对象和原对象彻底独立,修改任何字段(包括引用类型)都不会相互影响。
- 实现成本较高,需要递归复制所有嵌套的引用类型。
- 示例(Java):
class Person {String name;int age;// 深拷贝构造方法(对引用类型字段重新创建对象)public Person(Person other) {this.name = new String(other.name); // 复制字符串内容(深拷贝)this.age = other.age;} }Person original = new Person("Alice", 20); Person deepCopy = new Person(original); // 深拷贝// 修改引用类型字段:不影响原对象 deepCopy.name = "Bob"; System.out.println(original.name); // 输出 "Alice"(各自独立)// 修改基本类型字段:不影响原对象 deepCopy.age = 30; System.out.println(original.age); // 输出 "20"
3. 常见实现方式
- 浅拷贝:
- Java中
Object.clone()
默认是浅拷贝(需实现Cloneable
接口)。 - 多数语言的对象复制默认行为(如Python的
copy.copy()
)。
- Java中
- 深拷贝:
- 手动递归复制所有引用类型字段。
- 序列化与反序列化(如Java的
ObjectInputStream
/ObjectOutputStream
)。 - 工具类(如Apache Commons的
SerializationUtils.clone()
)。
总结:
类型 | 复制范围 | 独立性 | 性能 | 适用场景 |
---|---|---|---|---|
浅拷贝 | 基本类型+引用地址 | 不完全独立 | 高效 | 引用类型字段无需修改的场景 |
深拷贝 | 基本类型+所有引用类型内容 | 完全独立 | 较低 | 需要彻底隔离原对象的场景 |
选择哪种拷贝方式,取决于是否需要新对象与原对象完全隔离。
五、什么是单例模式懒汉式?
单例模式的懒汉式(Lazy Initialization)是一种延迟创建单例实例的实现方式,其核心特点是:只有在第一次使用时才会创建实例,而非程序启动时就初始化,从而达到节省资源的目的。
1. 懒汉式的基本实现
懒汉式的典型代码结构如下(以Java为例):
public class Singleton {// 1. 私有静态变量,保存唯一实例(初始为null,延迟初始化)private static Singleton instance;// 2. 私有构造方法,禁止外部直接实例化private Singleton() {}// 3. 公开静态方法,第一次调用时创建实例public static Singleton getInstance() {if (instance == null) { // 判断是否已创建实例instance = new Singleton(); // 第一次调用时创建}return instance;}
}
特点:
- 延迟初始化:实例在第一次调用
getInstance()
时才被创建。 - 节省资源:如果实例始终未被使用,则不会被创建,避免了不必要的内存占用。
2. 线程安全问题与改进
上述基础实现存在线程安全隐患:当多个线程同时调用getInstance()
时,可能会创建多个实例(例如,线程A和线程B同时判断instance == null
,都进入创建逻辑)。
解决线程安全问题的常见方式:
-
同步方法(简单但性能较差)
给getInstance()
方法加synchronized
关键字,确保同一时间只有一个线程能执行该方法:public static synchronized Singleton getInstance() {if (instance == null) {instance = new Singleton();}return instance; }
缺点:每次调用方法都需要加锁,即使实例已创建,仍会有锁开销,影响性能。
-
双重检查锁定(Double-Checked Locking,DCL)
仅在实例未创建时加锁,既保证线程安全,又减少锁开销:public class Singleton {// 注意:需用volatile修饰,禁止指令重排序private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {// 第一次检查:未加锁,快速判断(多数情况直接返回)if (instance == null) {synchronized (Singleton.class) { // 加锁// 第二次检查:防止多线程同时通过第一次检查后重复创建if (instance == null) {instance = new Singleton();}}}return instance;} }
关键点:
volatile
修饰实例变量:防止instance = new Singleton()
的指令重排序(避免其他线程获取到未初始化完成的实例)。- 双重检查:外层检查避免频繁加锁,内层检查确保线程安全。
3. 懒汉式的适用场景
- 实例创建成本较高(如需要大量资源),且不一定会被使用。
- 对启动速度有要求,希望延迟初始化耗时资源。
与"饿汉式"(提前创建实例)相比,懒汉式的核心优势是延迟加载,但实现时需注意线程安全问题。
六、除了懒汉式,单例模式还有哪些实现方式?
除了懒汉式,单例模式还有多种实现方式,各有其特点和适用场景。以下是常见的几种:
1. 饿汉式(Eager Initialization)
- 特点:在类加载时就创建实例,是最简单的单例实现方式。
- 实现原理:利用类加载机制保证实例唯一(类加载过程是线程安全的)。
- 代码示例:
public class Singleton {// 类加载时直接初始化实例private static final Singleton instance = new Singleton();// 私有构造方法private Singleton() {}// 公开方法返回实例public static Singleton getInstance() {return instance;} }
- 优缺点:
- 优点:线程安全(类加载时初始化)、实现简单。
- 缺点:无论是否使用,类加载时就创建实例,可能浪费资源(如实例创建成本高)。
- 适用场景:实例占用资源少、肯定会被使用的场景。
2. 静态内部类(Static Nested Class)
- 特点:结合饿汉式的线程安全和懒汉式的延迟加载,是一种优雅的实现。
- 实现原理:静态内部类不会随外部类加载而初始化,只有第一次调用
getInstance()
时才会加载内部类并创建实例。 - 代码示例:
public class Singleton {// 静态内部类,仅在调用getInstance()时加载private static class SingletonHolder {// 内部类中创建实例private static final Singleton INSTANCE = new Singleton();}private Singleton() {}public static Singleton getInstance() {return SingletonHolder.INSTANCE; // 触发内部类加载} }
- 优缺点:
- 优点:线程安全(类加载机制保证)、延迟加载(内部类按需加载)、无锁开销。
- 缺点:无法传递参数(实例创建时不能动态传入参数)。
- 适用场景:大多数单例场景,是推荐的实现方式之一。
3. 枚举(Enum)
- 特点:利用Java枚举的天然单例特性,是《Effective Java》推荐的最佳方式。
- 实现原理:枚举类的实例在JVM中是唯一的,且枚举序列化机制能防止反射和反序列化破坏单例。
- 代码示例:
public enum Singleton {INSTANCE; // 唯一实例// 枚举类可以有自己的方法public void doSomething() {// 业务逻辑} }// 使用方式 Singleton.INSTANCE.doSomething();
- 优缺点:
- 优点:绝对线程安全、防止反射和反序列化攻击、实现极简。
- 缺点:无法延迟加载(枚举实例在类加载时创建)、灵活性较低(枚举类继承关系固定)。
- 适用场景:需要严格保证单例不被破坏的场景(如防止反射攻击)。
4. 注册式(容器管理)
- 特点:通过一个容器(如Map)管理多个单例实例,按需获取。
- 实现原理:将实例注册到容器中,使用时从容器中获取,类似Spring的单例Bean管理。
- 代码示例:
import java.util.HashMap; import java.util.Map;public class SingletonContainer {// 容器存储单例实例private static final Map<String, Object> singletonMap = new HashMap<>();// 私有构造方法private SingletonContainer() {}// 注册实例public static void registerSingleton(String key, Object instance) {if (!singletonMap.containsKey(key)) {singletonMap.put(key, instance);}}// 获取实例public static Object getSingleton(String key) {return singletonMap.get(key);} }// 使用方式 // 注册:SingletonContainer.registerSingleton("userService", new UserService()); // 获取:UserService service = (UserService) SingletonContainer.getSingleton("userService");
- 优缺点:
- 优点:可管理多个单例,灵活性高。
- 缺点:需要手动注册,线程安全需额外保证(如同步容器)。
- 适用场景:需要管理大量单例对象的框架(如Spring IoC容器)。
各种实现方式对比:
实现方式 | 线程安全 | 延迟加载 | 防止反射/反序列化 | 实现复杂度 |
---|---|---|---|---|
饿汉式 | 是 | 否 | 否 | 简单 |
懒汉式(DCL) | 是(需volatile) | 是 | 否 | 较复杂 |
静态内部类 | 是 | 是 | 否 | 简单 |
枚举 | 是 | 否 | 是 | 极简 |
注册式 | 需手动保证 | 是 | 否 | 较复杂 |
总结:
- 简单场景:优先选择静态内部类(平衡延迟加载和线程安全)。
- 严格安全场景:选择枚举(防止反射攻击)。
- 资源占用少:可选择饿汉式(实现最简单)。
- 框架级管理:选择注册式(灵活管理多个单例)。
单例模式的核心是保证实例唯一,选择实现方式时需权衡线程安全、资源消耗和使用场景。
七、并行与并发的区别?
并行(Parallelism)和并发(Concurrency)是描述多任务处理的两个重要概念,它们都涉及"同时处理多个任务",但核心含义和实现方式有本质区别:
1. 并发(Concurrency)
- 核心思想:“看起来同时执行”,通过任务切换(时间分片)让多个任务共享同一资源(如CPU),在宏观上表现为多个任务同时进行。
- 实现方式:单个CPU通过快速切换不同任务的执行上下文(如进程切换、线程切换),让多个任务交替执行。
- 本质:逻辑上的同时性,任务并非真正同时运行,而是CPU在任务间快速切换,造成"并行"的假象。
- 示例:
- 单CPU电脑同时打开浏览器、编辑器和音乐播放器,CPU在这些程序间快速切换执行。
- 一个服务员同时处理多个顾客的点餐请求(轮流服务,而非同时服务)。
2. 并行(Parallelism)
- 核心思想:“真正同时执行”,多个任务在物理上的不同资源(如多个CPU核心)上同时运行。
- 实现方式:多个CPU核心或处理器同时处理不同的任务,每个任务独立占用一个计算单元。
- 本质:物理上的同时性,任务在同一时刻真正并行执行。
- 示例:
- 多核心CPU中,一个核心运行浏览器,另一个核心运行音乐播放器,两者真正同时进行。
- 多个服务员同时分别服务不同的顾客。
关键区别对比:
维度 | 并发(Concurrency) | 并行(Parallelism) |
---|---|---|
资源需求 | 可基于单CPU实现 | 必须依赖多CPU/多核资源 |
执行方式 | 任务交替执行(时间分片) | 任务同时执行(空间分离) |
核心目标 | 提高资源利用率(如CPU利用率) | 提高任务处理速度(缩短总耗时) |
适用场景 | 多任务需共享资源时(如IO密集型) | 多任务可独立计算时(如CPU密集型) |
典型例子 | 单线程处理多个网络请求(异步IO) | 多线程并行计算大数据集 |
通俗理解:
- 并发:“一个人同时干多件事”(比如一边做饭一边听电话,通过快速切换注意力实现)。
- 并行:“多个人同时干多件事”(比如两个人分工,一个人做饭,一个人打扫卫生,真正同时进行)。
总结:
- 并发是处理多任务的能力(通过切换),并行是同时执行多任务的能力(通过多核)。
- 并行是并发的一种特殊情况:当并发任务数超过CPU核心数时,只能通过并发(切换)处理;当任务数小于等于核心数时,可通过并行(同时)处理。
- 现代系统通常同时利用并发和并行:例如多线程程序在多核CPU上运行时,部分线程并行执行,线程数超过核心数时则通过并发切换处理。
八、线程有几种创建方式?
在Java中,线程的创建方式主要有以下4种,每种方式适用于不同场景:
1. 继承Thread
类并重写run()
方法
- 原理:
Thread
类是Java线程的基础类,通过继承它并重写run()
方法定义线程执行逻辑。 - 步骤:
- 继承
Thread
类; - 重写
run()
方法(线程执行体); - 创建子类实例,调用
start()
方法启动线程(而非直接调用run()
)。
- 继承
- 示例代码:
public class MyThread extends Thread {@Overridepublic void run() {// 线程执行逻辑System.out.println("线程运行中:" + Thread.currentThread().getName());}public static void main(String[] args) {MyThread thread = new MyThread();thread.start(); // 启动线程,JVM会调用run()} }
- 缺点:Java单继承限制,继承
Thread
后无法再继承其他类。
2. 实现Runnable
接口并重写run()
方法
- 原理:
Runnable
是函数式接口(仅含run()
方法),通过实现它定义线程任务,再传递给Thread
执行。 - 步骤:
- 实现
Runnable
接口,重写run()
方法; - 创建
Thread
对象,将Runnable
实例作为参数传入; - 调用
Thread
的start()
方法启动线程。
- 实现
- 示例代码:
public class MyRunnable implements Runnable {@Overridepublic void run() {// 线程执行逻辑System.out.println("线程运行中:" + Thread.currentThread().getName());}public static void main(String[] args) {Runnable runnable = new MyRunnable();Thread thread = new Thread(runnable);thread.start(); // 启动线程} }
- 优点:避免单继承限制,可实现多个接口;适合多线程共享同一个任务实例的场景。
3. 实现Callable
接口并使用FutureTask
- 原理:
Callable
与Runnable
类似,但可返回结果且能抛出异常,需配合FutureTask
获取结果。 - 步骤:
- 实现
Callable<T>
接口(T
为返回值类型),重写call()
方法; - 创建
FutureTask<T>
对象,包装Callable
实例; - 将
FutureTask
作为参数传入Thread
,调用start()
启动; - 通过
FutureTask.get()
获取线程执行结果(会阻塞直到结果返回)。
- 实现
- 示例代码:
import java.util.concurrent.Callable; import java.util.concurrent.FutureTask;public class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {// 线程执行逻辑,返回结果int sum = 0;for (int i = 0; i <= 100; i++) {sum += i;}return sum;}public static void main(String[] args) throws Exception {Callable<Integer> callable = new MyCallable();FutureTask<Integer> futureTask = new FutureTask<>(callable);new Thread(futureTask).start();// 获取结果(会阻塞等待计算完成)System.out.println("1-100的和:" + futureTask.get());} }
- 优点:可获取线程执行结果,支持异常处理,适合需要任务返回值的场景。
4. 使用线程池(ExecutorService
)
- 原理:通过线程池管理线程生命周期,避免频繁创建销毁线程的开销,是生产环境推荐的方式。
- 步骤:
- 通过
Executors
工具类或ThreadPoolExecutor
创建线程池; - 提交
Runnable
或Callable
任务到线程池; - 关闭线程池(可选)。
- 通过
- 示例代码:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;public class ThreadPoolExample {public static void main(String[] args) {// 创建固定大小的线程池(3个线程)ExecutorService executor = Executors.newFixedThreadPool(3);// 提交任务(Runnable)for (int i = 0; i < 5; i++) {executor.submit(() -> {System.out.println("线程运行中:" + Thread.currentThread().getName());});}// 关闭线程池executor.shutdown();} }
- 优点:高效管理线程资源,控制并发数,降低系统开销,适合大量异步任务的场景。
总结:
创建方式 | 核心接口/类 | 是否有返回值 | 能否抛异常 | 灵活性 | 推荐场景 |
---|---|---|---|---|---|
继承Thread | Thread | 无 | 不能 | 低(单继承) | 简单场景 |
实现Runnable | Runnable | 无 | 不能 | 高 | 多线程共享任务 |
实现Callable +Future | Callable | 有 | 能 | 高 | 需要返回结果或异常处理 |
线程池 | ExecutorService | 可选 | 能 | 最高 | 生产环境、大量并发任务 |
实际开发中,线程池是最优选择(资源可控),其次是Runnable
或Callable
(避免单继承限制)。
九、Runnable 和 Callable 区别?
Runnable
和 Callable
是 Java 中用于定义线程任务的两个核心接口,它们的主要区别体现在返回值、异常处理和使用场景上,具体如下:
1. 核心方法与返回值
-
Runnable
:
包含唯一抽象方法void run()
,没有返回值。
该方法执行的任务结果无法直接返回给调用者。 -
Callable
:
是泛型接口,包含抽象方法V call() throws Exception
,有返回值(返回值类型由泛型参数V
指定)。
任务执行的结果可以通过返回值传递给调用者。
2. 异常处理
-
Runnable
:
run()
方法不能抛出受检异常(checked exception),只能在方法内部捕获处理。
若抛出非受检异常(unchecked exception),会导致线程终止,但无法被外部捕获。 -
Callable
:
call()
方法可以抛出受检异常,且异常会被传递给调用者(通过Future
捕获)。
3. 使用方式
-
Runnable
:
通常作为参数传递给Thread
类,通过thread.start()
启动线程。
也可提交给线程池(ExecutorService.submit(Runnable)
)。 -
Callable
:
不能直接作为Thread
的参数,必须通过FutureTask
包装后再传递给Thread
,或直接提交给线程池(ExecutorService.submit(Callable)
)。
需通过Future
对象获取返回值或异常(future.get()
)。
4. 代码示例对比
Runnable
示例
Runnable runnable = () -> {System.out.println("执行Runnable任务");// 无返回值,不能抛出受检异常
};// 方式1:通过Thread启动
new Thread(runnable).start();// 方式2:通过线程池启动
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.submit(runnable);
executor.shutdown();
Callable
示例
Callable<Integer> callable = () -> {System.out.println("执行Callable任务");return 100; // 有返回值
};// 方式1:通过FutureTask + Thread启动
FutureTask<Integer> futureTask = new FutureTask<>(callable);
new Thread(futureTask).start();
try {int result = futureTask.get(); // 获取返回值(会阻塞)System.out.println("结果:" + result);
} catch (Exception e) {e.printStackTrace(); // 捕获call()抛出的异常
}// 方式2:通过线程池启动
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(callable);
try {int result = future.get();System.out.println("结果:" + result);
} catch (Exception e) {e.printStackTrace();
}
executor.shutdown();
5. 总结对比表
特性 | Runnable | Callable<V> |
---|---|---|
方法名 | run() | call() |
返回值 | 无(void ) | 有(类型为 V ) |
异常处理 | 不能抛出受检异常 | 可以抛出受检异常 |
与 Thread 配合 | 可直接作为参数 | 需通过 FutureTask 包装 |
与线程池配合 | 支持(submit(Runnable) ) | 支持(submit(Callable) ) |
适用场景 | 无需返回结果的任务 | 需要返回结果或处理异常的任务 |
核心区别一句话总结:
Runnable
适合不需要返回结果、无受检异常的简单任务;Callable
适合需要返回结果或需要抛出受检异常的复杂任务,其结果通过 Future
获取。
十、为什么调用 start() 方法时会执行 run() 方法,那怎么不直接调用 run() 方法?
在Java中,start()
方法和run()
方法的作用有本质区别,直接调用run()
方法无法达到多线程的效果,具体原因如下:
1. start()
方法的作用
start()
是Thread
类的核心方法,其主要功能是启动一个新线程,而非直接执行任务:
- 它会向JVM申请创建一个新的线程(与主线程并行的独立执行流)。
- 新线程创建成功后,会处于就绪状态,等待CPU调度。
- 当CPU分配时间片给新线程时,才会自动调用
run()
方法(作为线程的执行体)。
简言之:start()
负责"创建线程并安排执行",run()
负责"定义线程要做什么"。
2. 直接调用run()
方法的问题
如果直接调用run()
方法,它会被当作一个普通的方法在当前线程(通常是主线程) 中执行,不会创建新线程:
- 此时
run()
方法的执行会阻塞当前线程,直到其执行完毕。 - 完全失去多线程的并行效果,与调用一个普通方法没有区别。
示例对比:
public class ThreadDemo {public static void main(String[] args) {Thread thread = new Thread(() -> {System.out.println("当前线程:" + Thread.currentThread().getName());});// 情况1:调用start()——创建新线程thread.start(); // 输出:当前线程:Thread-0(新线程)// 情况2:直接调用run()——在主线程执行thread.run(); // 输出:当前线程:main(主线程)}
}
3. 底层原理
start()
方法是一个native方法(依赖JVM和操作系统实现),它会触发操作系统创建新的线程实体,并将run()
方法设置为该线程的入口。run()
方法只是一个普通的实例方法,其执行依赖于所属线程的调度。如果没有通过start()
创建新线程,run()
就只能在当前调用者线程中执行。
总结:
start()
:启动新线程,间接触发run()
执行(多线程并行)。run()
:定义线程任务的普通方法,直接调用则在当前线程同步执行(无多线程效果)。
因此,必须通过start()
方法启动线程,才能真正利用多线程的并行能力;直接调用run()
方法只是普通的方法调用,无法实现多线程。