IO流、多线程、反射
IO流
Java 中的流(Stream)、文件(File)和 IO(输入输出)是处理数据读取和写入的基础设施,它们允许程序与外部数据(如文件、网络、系统输入等)进行交互。
java.io 包是 Java 标准库中的一个核心包,提供了用于系统输入和输出的类,它包含了处理数据流(字节流和字符流)、文件读写、序列化以及数据格式化的工具。
java.io 是处理文件操作、流操作以及低级别 IO 操作的基础包。
java.io 包中的流支持很多种格式,比如:基本类型、对象、本地化字符集等等。
一个流可以理解为一个数据的序列。输入流表示从一个源读取数据,输出流表示向一个目标写数据。

文件IO
1 | // 字节流 - 适合图片、视频等 |
网络IO
1 | URI uri = new URI("https://www.baidu.com"); |
缓冲器流
1 | // 缓冲字节流 - 性能更好 |
装饰器模式
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
装饰器模式通过将对象包装在装饰器类中,以便动态地修改其行为。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
使用场景
- 当需要在不增加大量子类的情况下扩展类的功能。
- 当需要动态地添加或撤销对象的功能。
1 | // 核心思想:装饰类和被装饰类实现相同的接口 |
装饰器的优点:
增强功能:在不修改原有类的基础上添加新功能
灵活组合:可以多层嵌套,按需组合功能
统一接口:所有装饰流都继承自相同的基类
透明性:使用时与普通流没有区别
多线程
Java 给多线程编程提供了内置的支持。
进程(Process)
一个进程包括由操作系统分配的内存空间,包含一个或多个线程。一个线程不能独立的存在,它必须是进程的一部分。一个进程一直运行,直到所有的非守护线程都结束运行后才能结束。
线程(Thread)
线程是进程中的⼀个实体,是被系统独⽴调度和分派的基本单位。⼀个进程可以由多个线程组成,它们共享进程的内存空间和资源,但每个线程拥有⾃⼰的执⾏堆栈和程序计数器。
实现
Java 提供了三种创建线程的方法:
通过实现 Runnable 接口;
创建一个线程,最简单的方法是创建一个实现 Runnable 接口的类。为了实现 Runnable,一个类只需要执行一个方法调用 run()1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42class RunnableDemo implements Runnable {
private Thread t;
private final String threadName;
RunnableDemo(String name) {
threadName = name;
System.out.println("Creating " + threadName);
}
public void run() {
System.out.println("Running " + threadName);
try {
for (int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// 让线程睡眠一会
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}
public void start() {
System.out.println("Starting " + threadName);
if (t == null) {
t = new Thread(this, threadName);
t.start();
}
}
}
public class TestThread1 {
public static void main(String[] args) {
RunnableDemo R1 = new RunnableDemo("Thread-1");
R1.start();
RunnableDemo R2 = new RunnableDemo("Thread-2");
R2.start();
}
}通过继承 Thread 类本身;
创建一个线程的第二种方法是创建一个新的类,该类继承 Thread 类,然后创建一个该类的实例。继承类必须重写 run() 方法,该方法是新线程的入口点。它也必须调用 start() 方法才能执行。该方法尽管被列为一种多线程实现方式,但是本质上也是实现了 Runnable 接口的一个实例。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43class ThreadDemo extends Thread {
private Thread t;
private final String threadName;
ThreadDemo( String name) {
threadName = name;
System.out.println("Creating " + threadName );
}
public void run() {
System.out.println("Running " + threadName );
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// 让线程睡眠一会
Thread.sleep(50);
}
}catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
}
System.out.println("Thread " + threadName + " exiting.");
}
public void start () {
System.out.println("Starting " + threadName );
if (t == null) {
t = new Thread (this, threadName);
t.start ();
}
}
}
public class TestThread2 {
public static void main(String[] args) {
ThreadDemo T1 = new ThreadDemo( "Thread-1");
T1.start();
ThreadDemo T2 = new ThreadDemo( "Thread-2");
T2.start();
}
}通过 Callable 和 Future 创建线程。
创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值。
创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值。
使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程。
调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值。
1 | import java.util.concurrent.Callable; |
对于上述三种实现总结:
首选实现Runnable,继承 Thread 几乎没有必要;
直接 new Thread 适合演示与非常小的使用场景;
Callable + Future/FutureTask 提供结果、异常、取消能力,是 Runnable 的升级。
最佳现代方案:ExecutorService + Callable 或 CompletableFuture;
在 JDK 21+,可考虑虚拟线程进一步简化。
线程安全
我们需要多个线程同时访问一个类时,程序仍能正确工作。
1 | // 非线程安全示例 |
考虑⼀个简单的整数加法操作,如 count = count + 1。
尽管这看起来是⼀条简单的语句,但在多数处理器上,这个操作涉及下⾯⼏个步骤:
从内存中读取 count 的当前值。
在处理器中增加该值。
将新值写回内存。如果两个线程同时执⾏这个操作,那么可能出现以下情况:
线程A读取 count 的值为1。
线程B也读取 count 的值为1。
线程A增加值到2,并写回内存。
线程B也增加值到2,并写回内存。
在这种情况下,虽然 count = count + 1 被执⾏了两次,但 count 的值只从1变到2
为了避免这种情况,可以使用Java提供的同步关键字`synchronized,在任何时候最多只能由⼀个线程进⼊,这样就可以保证increment ⽅法在count++ 操作的线程安全性。
1 | public synchronized void increment() { |
另一种写法修饰代码块
1 | public void increment() { |
在规范上:
建议使⽤共享资源作为锁对象
对于实例⽅法建议使⽤
this作为锁对象对于静态⽅法建议使⽤类对象作为锁对象
类名.class
线程池(Executor)
自JDK 1.5开始提供线程池,并且默认提供了4种线程池的实例——Executors提供了静态方法用于生成线程池实例。
线程池:顾名思义,就是一个池子,里面管理着多个线程。你只需要把任务提交给线程池,而无需关心线程的创建和销毁。线程池会复用线程来执行任务。
为什么使用线程池
降低资源消耗:通过重复利用已创建的线程,减少线程创建和销毁造成的消耗。
提高响应速度:当任务到达时,任务可以不需要等到线程创建就能立即执行。
提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。
四种实例
1 | // 1. 固定大小线程池(80%场景用这个) |
提交任务的两种方式
1 | // 方式1:执行无返回值的任务 |
虚拟线程
虚拟线程(Virtual Threads)是Java 19引入的预览特性,并在Java 21中成为正式特性。它们旨在以更低的资源开销实现高并发,特别适用于I/O密集型任务。
1 | // 方式1:直接启动 |
由于虚拟线程非常的轻量,所以无需再通过线程池优化。
异步模型
在实际业务中,其实我们更应该关注异步编程模型,而把多线程更多的看作实现细节。
反射
反射 在 Java 中,反射是⼀种强⼤的机制,它允许程序在运⾏时检查或修改其⾃身⾏为。使⽤反射,程序能够访问类的属性和⽅法,即使在编译时这些类的名称并未明确给出。这使得 Java 程序可以在运⾏时动态地创建对象、调⽤⽅法、改变字段等,即便这些类、⽅法或字段在编写原始代码时不可知。
工作流程
- 获取
Class对象:首先获取目标类的Class对象。 - 获取成员信息:通过
Class对象,可以获取类的字段、方法、构造函数等信息。 - 操作成员:通过反射 API 可以读取和修改字段的值、调用方法以及创建对象。
1 | // 举例:输出⼀个类所有的字段和⽅法 |
注解
注解(Annotations)是从 Java 5 开始引⼊的⼀种元数据形式,它们提供了⼀种为代码添加信息的⽅法,但不直接影响代码的执⾏。注解可以被⽤于类、⽅法、变量、参数和Java包等。使⽤注解的主要⽬的是提供信息给编译器,进⾏代码分析,或者通过运⾏时反射机制实现特定功能。
注解很大程度上需要配合反射才能发挥作用
定义注解
1 | public MyAnnotation { |
使⽤注解
1 |
|
1 | // 举例:定义并读取注解 |
缺点
性能开销:由于反射涉及动态解析的类型,因此⽆法执⾏某些 Java 虚拟机优化。 因此,反射操作的性能要⽐⾮反射操作的性能要差,应该在性能敏感的应⽤程序中频繁调⽤的代码段中避免。
破坏封装性:反射调⽤⽅法时可以忽略权限检查,因此可能会破坏封装性⽽导致安全问题。
内部曝光:由于反射允许代码执⾏在⾮反射代码中⾮法的操作,例如访问私有字段和⽅法,所以反射的使⽤可能会导致意想不到的副作⽤,这可能会导致代码功能失常并可能破坏可移植性