Skip to content

Chapter 17 · 多线程与并发基础

一个程序能同时做多件事 — 现代多核 CPU 时代的必修课。


🎯 本章目标

  • 理解 进程 vs 线程 的区别
  • 掌握创建线程的 4 种方式
  • 学会 synchronizedvolatileReentrantLock
  • 理解 线程池 的必要性
  • 知道 wait/notifyawait/signal

1. 进程 vs 线程

进程(Process)线程(Thread)
单位资源分配CPU 调度
内存独立内存空间共享进程内存
通信进程间通信(IPC,复杂)共享变量(简单但要锁)
切换开销
类比一座工厂工厂里的工人

💡 一个 JVM 进程里有 N 个线程。Tomcat、Spring、Netty 全是基于多线程。

进程线程关系


2. 创建线程的 4 种方式

2.1 继承 Thread

java
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程跑起来了:" + getName());
    }
}

new MyThread().start();    // 注意是 start(),不是 run()!

2.2 实现 Runnable(推荐)

java
class MyTask implements Runnable {
    public void run() { System.out.println("running"); }
}

new Thread(new MyTask()).start();

// Lambda 简化
new Thread(() -> System.out.println("Lambda task")).start();

💡 优先用 Runnable:Java 单继承,继承 Thread 就不能继承别的了。

2.3 实现 Callable + Future(带返回值)

java
Callable<Integer> task = () -> {
    Thread.sleep(1000);
    return 42;
};

ExecutorService pool = Executors.newFixedThreadPool(2);
Future<Integer> future = pool.submit(task);
Integer result = future.get();   // 阻塞等结果
pool.shutdown();

2.4 线程池(生产环境必用)

java
ExecutorService pool = Executors.newFixedThreadPool(4);
for (int i = 0; i < 10; i++) {
    int id = i;
    pool.submit(() -> System.out.println("任务 " + id + " by " + Thread.currentThread().getName()));
}
pool.shutdown();

3. 线程的生命周期

线程生命周期

NEW (新建)
   │ start()

RUNNABLE (可运行)
   │ ←→ 操作系统调度

RUNNING (运行中)

   ├── sleep() / 阻塞 IO ──→ TIMED_WAITING / WAITING / BLOCKED
   │                              │
   │                              │ 唤醒/超时
   ▼                              │
TERMINATED (终止) ←────────────────┘
状态触发
NEWnew Thread() 之后
RUNNABLEstart() 调用后
BLOCKED等待 synchronized 锁
WAITINGwait() / join() / park()
TIMED_WAITINGsleep(n) / wait(n) / join(n)
TERMINATEDrun() 结束

4. 线程安全:为什么要加锁?

java
class Counter {
    int count = 0;
    void inc() { count++; }      // ❌ 看似一行,实际三步
}

Counter c = new Counter();
// 100 个线程,每个加 1000
// 期望: 100000,实际:可能只有 80000+

count++ = 读 + 加 + 写,多线程交叉执行时丢更新。

线程安全问题


5. synchronized 关键字

三种写法

java
class Counter {
    int count = 0;

    public synchronized void inc1() {     // 锁 this
        count++;
    }

    public void inc2() {
        synchronized (this) {              // 同步块
            count++;
        }
    }

    public static synchronized void inc3() { // 锁 Counter.class
        // ...
    }

    private final Object lock = new Object();
    public void inc4() {
        synchronized (lock) {              // 锁专用对象(推荐)
            count++;
        }
    }
}

几个要点

  • 实例方法 synchronized → 锁 this
  • 静态方法 synchronized → 锁 类对象(Counter.class)
  • 同一把锁是 可重入 的(同一个线程可以反复进入)

6. volatile 关键字

"保证可见性,禁止重排序"

java
class Worker {
    private volatile boolean running = true;

    public void work() {
        while (running) {
            // do work
        }
    }

    public void stop() { running = false; }
}

不加 volatile 时,JIT 可能优化让 running 一直从寄存器读,主线程改了也看不到 → 死循环。

⚠️ volatile 不保证原子性count++ 加 volatile 仍然不安全。

关键字原子性可见性有序性
volatile
synchronized
AtomicInteger

7. ReentrantLock:更灵活的锁

java
ReentrantLock lock = new ReentrantLock();

lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();          // 必须在 finally 里!
}

比 synchronized 强在哪?

特性synchronizedReentrantLock
可中断lockInterruptibly()
公平锁new ReentrantLock(true)
尝试获取tryLock()
多 ConditionnewCondition()
关键字简洁

8. 线程间通信

8.1 wait / notify(synchronized 配套)

java
synchronized (lock) {
    while (条件不满足) lock.wait();      // 释放锁,等
    // 处理
    lock.notifyAll();                     // 唤醒所有
}

8.2 Condition(ReentrantLock 配套)

java
ReentrantLock lock = new ReentrantLock();
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

// 生产者
lock.lock();
try {
    while (满了) notFull.await();
    放入();
    notEmpty.signal();
} finally { lock.unlock(); }

9. 线程池

反复创建线程是最大的性能浪费!

java
// ❌ 不推荐(无限队列,可能 OOM)
ExecutorService pool = Executors.newFixedThreadPool(10);

// ✅ 推荐(自定义参数,可控)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    4,                         // 核心线程数
    8,                         // 最大线程数
    60, TimeUnit.SECONDS,      // 空闲存活时间
    new ArrayBlockingQueue<>(100),  // 工作队列
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);

7 大参数

  1. corePoolSize - 核心线程数
  2. maximumPoolSize - 最大线程数
  3. keepAliveTime - 空闲存活时间
  4. unit - 时间单位
  5. workQueue - 工作队列
  6. threadFactory - 线程工厂(命名)
  7. handler - 拒绝策略(队列满 + 线程满)

4 种拒绝策略

策略行为
AbortPolicy抛 RejectedExecutionException(默认)
CallerRunsPolicy让调用者自己执行
DiscardPolicy默默丢弃
DiscardOldestPolicy丢弃最老的,再加入

10. 实战练习

文件内容
ThreadCreate.java4 种创建方式
UnsafeCounter.java不加锁 vs 加锁的差异
SynchronizedDemo.javasynchronized 三种写法
VolatileDemo.javavolatile 可见性
ProducerConsumer.java生产者-消费者
ThreadPoolDemo.java自定义线程池

11. 浏览器演示

打开 demo.html

  • 线程生命周期动画
  • 竞态条件可视化
  • 线程池工作过程

12. 面试可能会问什么?

Q1: 创建线程的方式?

  1. 继承 Thread
  2. 实现 Runnable(推荐)
  3. 实现 Callable + Future(带返回值)
  4. 线程池(生产用)

Q2: synchronized 和 ReentrantLock 区别?

synchronizedReentrantLock
形式关键字
释放锁自动手动 unlock(必须 finally)
可中断
公平性非公平可公平可非公平
多 Condition

Q3: volatile 和 synchronized 区别?

  • volatile:保证可见性 + 禁止重排序,但不保证原子性
  • synchronized:全保证,但开销大

Q4: 线程池有哪些参数?

7 大参数(见上文),重点记 corePoolSize、maxPoolSize、queue、handler。

Q5: 为什么 Executors.newFixedThreadPool 不推荐?

底层用 无界队列 LinkedBlockingQueue,任务堆积可能 OOM。生产用 new ThreadPoolExecutor(...) 自己控制。

Q6: wait 和 sleep 区别?

waitsleep
来自ObjectThread
释放锁❌(抱着锁睡)
唤醒notify / 超时时间到 / 中断
必须在 synchronized 里

Q7: 死锁怎么避免?

  1. 加锁顺序一致
  2. tryLock + 超时
  3. 减少锁粒度
  4. 用 jstack 排查(Found one Java-level deadlock)

🎁 本章小结

✅ 进程是资源单位,线程是调度单位
✅ 4 种创建方式:Thread / Runnable / Callable / 线程池
✅ synchronized 全能但粗暴;ReentrantLock 灵活但繁琐
✅ volatile 保证可见性,不保证原子性
✅ 生产用线程池,不要 new Thread()

🔗 导航