学习笔记:IO流,多线程,多线程安全问题(代码示例,企业面试题,企业实际应用场景)

32 阅读10分钟

一、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. 企业面试题

  1. 问:字节流和字符流的区别?实际开发中如何选择?答:字节流(InputStream/OutputStream)处理二进制数据(如图片、视频、音频),字符流(Reader/Writer)处理文本数据(按字符编码,如 UTF-8);处理文本用字符流,处理非文本用字节流。
  2. 问:什么是缓冲流?为什么使用缓冲流能提升性能?答:缓冲流在内存中开辟缓冲区,先将数据读取到缓冲区,再批量处理(而非每次读写都操作磁盘),减少磁盘 IO 次数,提升效率。
  3. 问:try-with-resources的作用?哪些类可以使用?答:自动关闭资源,避免资源泄漏;实现AutoCloseable接口的类都可以(如所有 IO 流、Socket、数据库连接等)。
  4. 问:如何实现大文件的复制?(避免一次性加载到内存)答:使用字节缓冲流(BufferedInputStream/BufferedOutputStream),按字节数组分批次读取和写入。

3. 企业实际应用场景

  1. 日志文件读写:系统日志(如 log4j、slf4j)底层通过 IO 流写入日志文件,生产环境中会用缓冲流 + 按大小 / 时间切割日志。
  2. 配置文件解析:读取application.properties/yml等配置文件,常用字符缓冲流按行读取解析。
  3. 文件上传下载:后端接收前端上传的文件(字节流),或向客户端返回文件(如导出 Excel、下载附件)。
  4. 大数据文件处理:处理 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() vs run()start()会创建新线程执行run()run()只是普通方法调用(主线程执行),面试高频考点。
  • Callable+FutureTask:支持获取线程执行结果,企业中异步任务(如异步查询、异步计算)常用。
  • join():让主线程等待子线程执行完成,避免主线程先结束导致程序退出。

2. 企业面试题

  1. 问:继承 Thread 和实现 Runnable 的区别?为什么推荐 Runnable?答:Thread 是类(单继承限制),Runnable 是接口(可多实现);Runnable 解耦了任务逻辑和线程控制,更符合面向接口编程。
  2. 问:start()run()的区别?调用run()会创建新线程吗?答:start()会触发 JVM 创建新线程,然后执行run();直接调用run()只是普通方法调用,不会创建新线程(主线程执行)。
  3. 问:Callable 和 Runnable 的区别?答:Callable 的call()有返回值、可抛异常;Runnable 的run()无返回值、只能抛运行时异常。
  4. 问:线程的生命周期有哪些状态?如何转换?答:新建(New)→就绪(Runnable)→运行(Running)→阻塞(Blocked/Waiting/Timed Waiting)→终止(Terminated);通过start()sleep()wait()notify()join()等方法转换。

3. 企业实际应用场景

  1. 异步任务处理:如电商下单后,异步发送短信 / 邮件通知(主线程处理下单,子线程处理通知,提升响应速度)。
  2. 多任务并行计算:如大数据统计(将数据分片,多线程并行计算,最后汇总结果)。
  3. 定时任务:如定时清理日志、定时同步数据(通过Timer或线程池实现)。
  4. 服务器端并发处理:如 Tomcat 的线程池,每个请求分配一个线程处理,提升并发能力。

三、多线程安全问题

1. 核心代码示例(问题复现 + 解决方案)

多线程安全问题的本质是多个线程同时操作共享资源,解决方案包括synchronizedLock、原子类等。

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. 企业面试题

  1. 问:什么是线程安全问题?产生的条件有哪些?答:多个线程同时操作共享资源,且至少有一个线程是写操作,导致数据不一致;条件:多线程、共享资源、非原子性操作。

  2. 问:synchronized 和 ReentrantLock 的区别?答:

    • 底层:synchronized 是 JVM 层面的锁,ReentrantLock 是 JDK 层面的锁(API)。
    • 灵活性:ReentrantLock 支持公平锁、可中断、超时获取锁,synchronized 不支持。
    • 解锁:synchronized 自动解锁,ReentrantLock 需手动解锁(finally)。
    • 性能:高并发下 ReentrantLock 性能更优(JDK 1.6 后 synchronized 已优化,差距缩小)。
  3. 问:如何解决线程安全问题?除了锁还有其他方式吗?答:

    • 加锁:synchronized、ReentrantLock。
    • 原子类:AtomicInteger/AtomicLong(CAS 机制,无锁并发,性能更高)。
    • 避免共享资源:如每个线程使用独立变量(ThreadLocal)。
    • 不可变对象:如 String、Integer,天生线程安全。
  4. 问:什么是 CAS?ABA 问题如何解决?答:CAS(Compare And Swap)是无锁并发的核心,包含三个值:预期值、当前值、更新值,只有当前值 = 预期值时才更新;ABA 问题:线程 1 将 A→B→A,线程 2 看到 A 认为未修改,导致错误;解决方案:使用AtomicStampedReference(加版本号)。

3. 企业实际应用场景

  1. 库存扣减:电商秒杀、下单扣库存,需保证库存不超卖(用锁或原子类确保扣减操作原子性)。
  2. 计数器统计:网站访问量、接口调用次数统计,用AtomicLong(CAS)比锁更高效。
  3. 缓存更新:多线程更新 Redis 缓存,需加锁避免缓存覆盖 / 脏数据。
  4. 线程池参数配置:Tomcat/Netty 的线程池核心参数(核心线程数、最大线程数),多线程修改时需保证线程安全。
  5. ThreadLocal 应用:如 Spring 的事务管理,用 ThreadLocal 存储当前线程的数据库连接,避免多线程共享连接导致事务混乱。

总结

  1. IO 流:核心是字节流 / 字符流的选择,企业中优先用缓冲流 + try-with-resources,主要用于文件读写、日志、文件上传下载。
  2. 多线程:推荐用 Runnable/Callable 创建线程,核心是线程生命周期和并发控制,用于异步任务、并行计算、服务器并发处理。
  3. 多线程安全:本质是共享资源的并发修改,解决方案包括 synchronized、ReentrantLock、原子类,企业中需根据场景选择(简单场景用 synchronized,复杂场景用 ReentrantLock,高性能统计用原子类)。