Skip to content

05-异常与多线程

对应原始资料:JavaSE进阶_day08~day09

一、异常

1. 异常体系

Throwable
├── Error            严重问题(OutOfMemoryError、StackOverflowError),程序处理不了
└── Exception
      ├── RuntimeException(运行期异常,可避免)
      │     NullPointerException、IndexOutOfBoundsException、
      │     ClassCastException、ArithmeticException、ConcurrentModificationException
      └── 非 RuntimeException(编译期异常/检查异常,必须处理)
            IOException、SQLException、ClassNotFoundException

2. 异常处理

java
try {
    // 可能抛出异常的代码
} catch (异常类型 变量) {
    // 处理
} catch (异常类型 | 异常类型2 变量) {  // JDK 7 多异常合并
} finally {
    // 一定执行(除非 System.exit)—— 常用于关资源
}

JDK 7 try-with-resources(自动关流):

java
try (FileInputStream fis = new FileInputStream("a.txt")) {
    // 用 fis
} catch (IOException e) { }

3. throws 与 throw

  • throws:声明在方法上,告诉调用者可能抛出哪些异常,把处理交给上层。
  • throw:方法体内手动抛出异常对象。
java
public void setAge(int age) throws IllegalAgeException {
    if (age < 0) throw new IllegalAgeException("年龄非法");
    this.age = age;
}

4. 自定义异常

继承 Exception(编译期)或 RuntimeException(运行期),并提供构造方法。

java
public class IllegalAgeException extends RuntimeException {
    public IllegalAgeException(String msg) { super(msg); }
}

二、多线程基础

1. 进程 vs 线程

  • 进程:正在运行的程序,独立内存。
  • 线程:进程中的执行单元,多线程共享进程内存

2. 并行 vs 并发

  • 并发:多线程交替执行(CPU 轮转)。
  • 并行:多线程同时执行(多核 CPU)。

3. 创建线程的三种方式

① 继承 Thread

java
class MyThread extends Thread {
    public void run() { System.out.println("子线程:" + Thread.currentThread().getName()); }
}
new MyThread().start();

② 实现 Runnable(推荐,可继承其他类、便于共享资源)

java
Runnable r = () -> System.out.println("运行中");
new Thread(r).start();

③ 实现 Callable(JDK 5,有返回值)

java
Callable<Integer> c = () -> { return 100; };
FutureTask<Integer> ft = new FutureTask<>(c);
new Thread(ft).start();
Integer result = ft.get();   // 阻塞获取返回值

start() 才会启动新线程;直接调 run() 只是普通方法调用。

4. Thread 常用方法

java
Thread.currentThread().getName();
Thread.sleep(1000);            // 睡眠 1 秒(静态,谁调睡谁)
t.setName("t1");
t.setPriority(Thread.MAX_PRIORITY);   // 1~10,默认 5
t.setDaemon(true);                     // 设为守护线程
Thread.yield();                        // 让步
t.join();                              // 等待 t 执行完

5. 线程调度与生命周期

  • Java 采用抢占式调度,优先级越高越容易抢到 CPU。
  • 状态:NEW → RUNNABLE → (BLOCKED / WAITING / TIMED_WAITING) → TERMINATED

三、线程安全

1. 线程安全问题三要素

共享资源 + 多线程并发 + 非原子操作 → 出现数据错乱。

经典案例:多个线程同时 ticket-- 卖票,出现负数。

2. 同步代码块

java
synchronized (obj) {        // obj 是"锁对象",同一把锁才互斥
    // 操作共享数据的代码
}

3. 同步方法

java
public synchronized void sell() { }   // 锁是 this
public static synchronized void m() { }  // 锁是 Class 对象

4. Lock 锁(JDK 5,推荐)

java
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();          // 必须在 finally 释放
}

synchronized vs Lock:

  • synchronized 自动释放锁、JVM 层;Lock 手动释放、API 层、更灵活(可中断、可超时、可公平)。

5. 线程安全类

  • StringBufferVectorHashtable(旧版线程安全,性能差)。
  • Collections.synchronizedList(...) 包装。
  • java.util.concurrent.* 并发包(推荐,见下文线程池)。

四、死锁

两个线程互相持有对方需要的锁,导致永久等待。

死锁四必要条件:互斥、请求保持、不剥夺、循环等待。

避免方法:固定锁的获取顺序、使用 tryLock(timeout)

五、线程通信

wait() / notify() / notifyAll() 必须由锁对象调用( synchronized 块内)。

经典:生产者消费者

java
synchronized (lock) {
    while (没有数据) lock.wait();      // 释放锁,进入等待
    // 取数据
    lock.notifyAll();                  // 唤醒所有等待
}

为什么用 while 不用 if? 防止虚假唤醒。

六、线程池(重点)

1. 为什么用线程池

  • 复用线程,避免频繁创建/销毁的开销。
  • 控制最大并发数,防止资源耗尽。

2. Executors 工厂(不推荐直接用)

java
ExecutorService pool = Executors.newFixedThreadPool(5);
pool.submit(() -> System.out.println("任务"));
pool.shutdown();

阿里规约:禁止用 Executors 创建线程池(队列无界易 OOM),应直接 new ThreadPoolExecutor。

3. ThreadPoolExecutor 七参数

java
new ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数
    int maximumPoolSize,     // 最大线程数
    long keepAliveTime,      // 非核心线程空闲存活时间
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,    // 任务队列
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler      // 拒绝策略
);

4. 工作流程(重点面试题)

  1. 任务来了,若核心线程未满,创建核心线程执行。
  2. 核心线程满,任务进队列。
  3. 队列满,创建非核心线程,直到 max。
  4. max 也满,触发拒绝策略

5. 四种拒绝策略

  • AbortPolicy(默认):抛 RejectedExecutionException。
  • CallerRunsPolicy:让提交任务的线程自己执行。
  • DiscardPolicy:直接丢弃。
  • DiscardOldestPolicy:丢弃队列最老任务,重新提交。

6. 推荐参数

  • CPU 密集型:核心数 ≈ CPU 核心数 + 1。
  • IO 密集型:核心数 ≈ CPU 核心数 × 2(或更大)。

练习建议

  1. 三种方式创建线程,打印线程名。
  2. 模拟 4 个窗口卖 100 张票,分别用 synchronized、Lock 解决线程安全。
  3. wait/notify 实现生产者消费者(一个篮子放鸡蛋)。
  4. 自定义 ThreadPoolExecutor,观察任务超过 max 时的拒绝策略。
  5. 故意制造一个死锁(两个锁互相等待),用 jstack 排查。