05-异常与多线程
对应原始资料:
JavaSE进阶_day08~day09
一、异常
1. 异常体系
Throwable
├── Error 严重问题(OutOfMemoryError、StackOverflowError),程序处理不了
└── Exception
├── RuntimeException(运行期异常,可避免)
│ NullPointerException、IndexOutOfBoundsException、
│ ClassCastException、ArithmeticException、ConcurrentModificationException
└── 非 RuntimeException(编译期异常/检查异常,必须处理)
IOException、SQLException、ClassNotFoundException2. 异常处理
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. 线程安全类
StringBuffer、Vector、Hashtable(旧版线程安全,性能差)。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. 工作流程(重点面试题)
- 任务来了,若核心线程未满,创建核心线程执行。
- 核心线程满,任务进队列。
- 队列满,创建非核心线程,直到 max。
- max 也满,触发拒绝策略。
5. 四种拒绝策略
AbortPolicy(默认):抛 RejectedExecutionException。CallerRunsPolicy:让提交任务的线程自己执行。DiscardPolicy:直接丢弃。DiscardOldestPolicy:丢弃队列最老任务,重新提交。
6. 推荐参数
- CPU 密集型:核心数 ≈ CPU 核心数 + 1。
- IO 密集型:核心数 ≈ CPU 核心数 × 2(或更大)。
练习建议
- 三种方式创建线程,打印线程名。
- 模拟 4 个窗口卖 100 张票,分别用 synchronized、Lock 解决线程安全。
- 用
wait/notify实现生产者消费者(一个篮子放鸡蛋)。 - 自定义 ThreadPoolExecutor,观察任务超过 max 时的拒绝策略。
- 故意制造一个死锁(两个锁互相等待),用 jstack 排查。