一、IO 流
1. 核心代码示例(文件读写 + 缓冲流优化)
IO 流是 Java 中处理输入输出的核心,分为字节流和字符流,实际开发中优先使用缓冲流提升性能。
示例代码
import java.io.*;
/**
* IO流核心示例:文件读写(字符流+缓冲流)
* 包含:文件读取、文件写入、异常处理、资源自动关闭(try-with-resources)
*/
public class IODemo {
// 读取文件内容
public static String readFile(String filePath) {
// try-with-resources 自动关闭资源,无需手动close
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
StringBuilder content = new StringBuilder();
String line;
// 按行读取,避免一次性加载大文件到内存
while ((line = br.readLine()) != null) {
content.append(line).append(System.lineSeparator());
}
return content.toString();
} catch (FileNotFoundException e) {
System.err.println("文件不存在:" + filePath);
throw new RuntimeException(e);
} catch (IOException e) {
System.err.println("文件读取失败");
throw new RuntimeException(e);
}
}
// 写入内容到文件(覆盖模式)
public static void writeFile(String filePath, String content) {
try (BufferedWriter bw = new BufferedWriter(new FileWriter(filePath))) {
bw.write(content);
// 手动flush确保内容写入(缓冲流默认满了才刷)
bw.flush();
} catch (IOException e) {
System.err.println("文件写入失败");
throw new RuntimeException(e);
}
}
// 追加内容到文件
public static void appendFile(String filePath, String content) {
// FileWriter第二个参数为true表示追加
try (BufferedWriter bw = new BufferedWriter(new FileWriter(filePath, true))) {
bw.write(content);
bw.flush();
} catch (IOException e) {
System.err.println("文件追加失败");
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
String filePath = "test.txt";
// 写入文件
writeFile(filePath, "Hello IO流\n");
// 追加内容
appendFile(filePath, "追加的内容");
// 读取文件并打印
String content = readFile(filePath);
System.out.println("文件内容:\n" + content);
}
}
代码说明:
try-with-resources:Java 7 + 特性,自动关闭实现AutoCloseable的资源(如流),避免忘记 close 导致资源泄漏。- 缓冲流(
BufferedReader/BufferedWriter):相比普通流,减少磁盘 IO 次数,提升读写效率,是企业开发的首选。 - 异常处理:区分文件不存在、读写失败等场景,符合企业级代码的健壮性要求。
2. 企业面试题
- 问:字节流和字符流的区别?实际开发中如何选择?答:字节流(
InputStream/OutputStream)处理二进制数据(如图片、视频、音频),字符流(Reader/Writer)处理文本数据(按字符编码,如 UTF-8);处理文本用字符流,处理非文本用字节流。 - 问:什么是缓冲流?为什么使用缓冲流能提升性能?答:缓冲流在内存中开辟缓冲区,先将数据读取到缓冲区,再批量处理(而非每次读写都操作磁盘),减少磁盘 IO 次数,提升效率。
- 问:
try-with-resources的作用?哪些类可以使用?答:自动关闭资源,避免资源泄漏;实现AutoCloseable接口的类都可以(如所有 IO 流、Socket、数据库连接等)。 - 问:如何实现大文件的复制?(避免一次性加载到内存)答:使用字节缓冲流(
BufferedInputStream/BufferedOutputStream),按字节数组分批次读取和写入。
3. 企业实际应用场景
- 日志文件读写:系统日志(如 log4j、slf4j)底层通过 IO 流写入日志文件,生产环境中会用缓冲流 + 按大小 / 时间切割日志。
- 配置文件解析:读取
application.properties/yml等配置文件,常用字符缓冲流按行读取解析。 - 文件上传下载:后端接收前端上传的文件(字节流),或向客户端返回文件(如导出 Excel、下载附件)。
- 大数据文件处理:处理 GB 级大文件时,用分批次读写(字节数组),避免 OOM(内存溢出)。
二、多线程
1. 核心代码示例(三种创建方式 + 线程控制)
多线程是提升程序并发能力的核心,企业中常用Runnable(推荐)或Callable(有返回值)创建线程。
java
运行
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
/**
* 多线程核心示例:三种创建方式
* 1. 继承Thread类
* 2. 实现Runnable接口(推荐,避免单继承限制)
* 3. 实现Callable接口(有返回值、可抛异常)
*/
public class ThreadDemo {
// 方式1:继承Thread
static class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Thread方式:" + Thread.currentThread().getName() + " - " + i);
try {
Thread.sleep(100); // 模拟耗时操作
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
// 方式2:实现Runnable
static class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println("Runnable方式:" + Thread.currentThread().getName() + " - " + i);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
// 方式3:实现Callable(有返回值)
static class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 5; i++) {
sum += i;
System.out.println("Callable方式:" + Thread.currentThread().getName() + " - 累计:" + sum);
Thread.sleep(100);
}
return sum;
}
}
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 方式1启动
MyThread thread1 = new MyThread();
thread1.setName("线程1");
thread1.start(); // 注意:start()才是启动线程,run()只是普通方法调用
// 方式2启动
Thread thread2 = new Thread(new MyRunnable(), "线程2");
thread2.start();
// 方式3启动(需要FutureTask接收返回值)
FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
Thread thread3 = new Thread(futureTask, "线程3");
thread3.start();
// 获取Callable的返回值(会阻塞,直到线程执行完成)
Integer result = futureTask.get();
System.out.println("Callable线程返回值:" + result);
// 主线程等待子线程执行完成(join())
thread1.join();
thread2.join();
System.out.println("所有子线程执行完成");
}
}
代码说明:
start()vsrun():start()会创建新线程执行run(),run()只是普通方法调用(主线程执行),面试高频考点。Callable+FutureTask:支持获取线程执行结果,企业中异步任务(如异步查询、异步计算)常用。join():让主线程等待子线程执行完成,避免主线程先结束导致程序退出。
2. 企业面试题
- 问:继承 Thread 和实现 Runnable 的区别?为什么推荐 Runnable?答:Thread 是类(单继承限制),Runnable 是接口(可多实现);Runnable 解耦了任务逻辑和线程控制,更符合面向接口编程。
- 问:
start()和run()的区别?调用run()会创建新线程吗?答:start()会触发 JVM 创建新线程,然后执行run();直接调用run()只是普通方法调用,不会创建新线程(主线程执行)。 - 问:Callable 和 Runnable 的区别?答:Callable 的
call()有返回值、可抛异常;Runnable 的run()无返回值、只能抛运行时异常。 - 问:线程的生命周期有哪些状态?如何转换?答:新建(New)→就绪(Runnable)→运行(Running)→阻塞(Blocked/Waiting/Timed Waiting)→终止(Terminated);通过
start()、sleep()、wait()、notify()、join()等方法转换。
3. 企业实际应用场景
- 异步任务处理:如电商下单后,异步发送短信 / 邮件通知(主线程处理下单,子线程处理通知,提升响应速度)。
- 多任务并行计算:如大数据统计(将数据分片,多线程并行计算,最后汇总结果)。
- 定时任务:如定时清理日志、定时同步数据(通过
Timer或线程池实现)。 - 服务器端并发处理:如 Tomcat 的线程池,每个请求分配一个线程处理,提升并发能力。
三、多线程安全问题
1. 核心代码示例(问题复现 + 解决方案)
多线程安全问题的本质是多个线程同时操作共享资源,解决方案包括synchronized、Lock、原子类等。
java
运行
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
/**
* 多线程安全问题示例:
* 1. 复现线程安全问题(多线程卖票,出现超卖/重复卖)
* 2. 解决方案1:synchronized(同步方法/同步代码块)
* 3. 解决方案2:ReentrantLock(显式锁,更灵活)
*/
public class ThreadSafeDemo {
// 共享资源:10张票
private static int ticketCount = 10;
// 显式锁(ReentrantLock)
private static final Lock lock = new ReentrantLock();
// 不安全的卖票方法(会出现超卖)
public static void unsafeSellTicket() {
new Thread(() -> {
while (ticketCount > 0) {
// 模拟出票耗时
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖出1张票,剩余:" + --ticketCount);
}
}, "线程A").start();
new Thread(() -> {
while (ticketCount > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖出1张票,剩余:" + --ticketCount);
}
}, "线程B").start();
}
// 解决方案1:synchronized同步方法
public static synchronized void safeSellTicketBySync() {
if (ticketCount > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖出1张票,剩余:" + --ticketCount);
}
}
// 解决方案2:ReentrantLock显式锁
public static void safeSellTicketByLock() {
lock.lock(); // 加锁
try {
if (ticketCount > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(Thread.currentThread().getName() + "卖出1张票,剩余:" + --ticketCount);
}
} finally {
lock.unlock(); // 解锁(必须放finally,避免异常导致锁无法释放)
}
}
public static void main(String[] args) {
// 1. 测试不安全的卖票(会出现剩余票为负数)
// unsafeSellTicket();
// 2. 测试同步方法解决安全问题
/*new Thread(() -> {
while (ticketCount > 0) {
safeSellTicketBySync();
}
}, "线程A").start();
new Thread(() -> {
while (ticketCount > 0) {
safeSellTicketBySync();
}
}, "线程B").start();*/
// 3. 测试显式锁解决安全问题
new Thread(() -> {
while (ticketCount > 0) {
safeSellTicketByLock();
}
}, "线程A").start();
new Thread(() -> {
while (ticketCount > 0) {
safeSellTicketByLock();
}
}, "线程B").start();
}
}
代码说明:
- 线程安全问题复现:
unsafeSellTicket()中,两个线程同时操作ticketCount,会出现--ticketCount执行时的并发问题(如剩余 1 张票时,两个线程都判断>0,最终卖出 2 张,剩余 - 1)。 synchronized:隐式锁,可修饰方法或代码块,自动加锁 / 解锁,简单易用但灵活性低。ReentrantLock:显式锁,需手动lock()加锁、unlock()解锁(必须放 finally),支持公平锁 / 非公平锁、可中断锁等,企业中复杂场景(如超时获取锁)常用。
2. 企业面试题
-
问:什么是线程安全问题?产生的条件有哪些?答:多个线程同时操作共享资源,且至少有一个线程是写操作,导致数据不一致;条件:多线程、共享资源、非原子性操作。
-
问:synchronized 和 ReentrantLock 的区别?答:
- 底层:synchronized 是 JVM 层面的锁,ReentrantLock 是 JDK 层面的锁(API)。
- 灵活性:ReentrantLock 支持公平锁、可中断、超时获取锁,synchronized 不支持。
- 解锁:synchronized 自动解锁,ReentrantLock 需手动解锁(finally)。
- 性能:高并发下 ReentrantLock 性能更优(JDK 1.6 后 synchronized 已优化,差距缩小)。
-
问:如何解决线程安全问题?除了锁还有其他方式吗?答:
- 加锁:synchronized、ReentrantLock。
- 原子类:
AtomicInteger/AtomicLong(CAS 机制,无锁并发,性能更高)。 - 避免共享资源:如每个线程使用独立变量(ThreadLocal)。
- 不可变对象:如 String、Integer,天生线程安全。
-
问:什么是 CAS?ABA 问题如何解决?答:CAS(Compare And Swap)是无锁并发的核心,包含三个值:预期值、当前值、更新值,只有当前值 = 预期值时才更新;ABA 问题:线程 1 将 A→B→A,线程 2 看到 A 认为未修改,导致错误;解决方案:使用
AtomicStampedReference(加版本号)。
3. 企业实际应用场景
- 库存扣减:电商秒杀、下单扣库存,需保证库存不超卖(用锁或原子类确保扣减操作原子性)。
- 计数器统计:网站访问量、接口调用次数统计,用
AtomicLong(CAS)比锁更高效。 - 缓存更新:多线程更新 Redis 缓存,需加锁避免缓存覆盖 / 脏数据。
- 线程池参数配置:Tomcat/Netty 的线程池核心参数(核心线程数、最大线程数),多线程修改时需保证线程安全。
- ThreadLocal 应用:如 Spring 的事务管理,用 ThreadLocal 存储当前线程的数据库连接,避免多线程共享连接导致事务混乱。
总结
- IO 流:核心是字节流 / 字符流的选择,企业中优先用缓冲流 + try-with-resources,主要用于文件读写、日志、文件上传下载。
- 多线程:推荐用 Runnable/Callable 创建线程,核心是线程生命周期和并发控制,用于异步任务、并行计算、服务器并发处理。
- 多线程安全:本质是共享资源的并发修改,解决方案包括 synchronized、ReentrantLock、原子类,企业中需根据场景选择(简单场景用 synchronized,复杂场景用 ReentrantLock,高性能统计用原子类)。