写在前面:这是JavaSE系列的最后一篇!多线程是Java进阶的必经之路,也是面试中区分初级和中高级开发者的分水岭。今天我们讲多线程的基础知识,包括线程的创建、生命周期、同步机制和线程池。

在这里插入图片描述


一、进程与线程

1.1 基本概念

进程(Process):
- 操作系统分配资源的基本单位
- 每个进程有独立的内存空间
- 例如:打开一个浏览器就是一个进程

线程(Thread):
- CPU调度的基本单位
- 一个进程可以包含多个线程
- 线程共享进程的内存空间
- 例如:浏览器中同时下载多个文件,每个下载任务是一个线程

1.2 为什么需要多线程?

实际场景:想象一下你在浏览器里同时下载多个文件。如果是单线程,必须等第一个文件下载完才能开始第二个。而多线程可以让多个下载任务同时进行,大大缩短总时间。

// 单线程:任务串行执行
// 任务1(3秒)→ 任务2(2秒)→ 任务3(5秒)= 总共10秒

// 多线程:任务并行执行
// 任务1(3秒)
// 任务2(2秒)  → 总共5秒(取决于最慢的任务)
// 任务3(5秒)

// 好处:
// 1. 提高CPU利用率
// 2. 提高程序响应速度
// 3. 充分利用多核CPU

经验之谈:在实际项目中,多线程最常见的应用场景是:批量数据处理、异步任务执行、定时任务调度。但也要注意,线程不是越多越好,过多的线程会导致上下文切换开销增大,反而降低性能。


二、创建线程的四种方式

2.1 继承Thread类

// 方式1:继承Thread类,重写run方法
class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程:" + i);
        }
    }
}

// 启动线程
public class Test {
    public static void main(String[] args) {
        MyThread t = new MyThread();
        t.start();  // 启动线程(不是run!)
        
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程:" + i);
        }
    }
}

// 注意:调用start()才是启动线程
// 调用run()只是普通方法调用,还是在主线程执行

踩坑提醒:新手最容易犯的错误就是调用run()而不是start()。调用run()不会创建新线程,只是在当前线程执行方法体,完全达不到多线程的效果。

2.2 实现Runnable接口(推荐)

// 方式2:实现Runnable接口
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("子线程:" + i);
        }
    }
}

// 启动线程
public class Test {
    public static void main(String[] args) {
        Thread t = new Thread(new MyRunnable());
        t.start();
    }
}

// 方式3:Lambda表达式(Java 8+,最简洁)
new Thread(() -> {
    System.out.println("子线程运行");
}).start();

2.3 实现Callable接口(有返回值)

import java.util.concurrent.*;

// 方式4:实现Callable接口(可以返回结果)
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum;  // 返回结果
    }
}

public class Test {
    public static void main(String[] args) throws Exception {
        // 创建FutureTask
        FutureTask<Integer> task = new FutureTask<>(new MyCallable());
        
        // 启动线程
        new Thread(task).start();
        
        // 获取结果(会阻塞直到线程执行完毕)
        Integer result = task.get();
        System.out.println("1到100的和:" + result);  // 5050
    }
}

2.4 线程池(生产环境推荐)

经验之谈:在实际项目中,千万不要用new Thread()创建线程!线程创建和销毁是有开销的,而且线程数不可控可能导致OOM。线程池可以复用线程、控制并发数,是生产环境的标准做法。

import java.util.concurrent.*;

// 创建线程池
ExecutorService pool = Executors.newFixedThreadPool(3);  // 3个线程的固定线程池

// 提交任务
pool.execute(() -> System.out.println("任务1"));
pool.execute(() -> System.out.println("任务2"));
pool.execute(() -> System.out.println("任务3"));

// 关闭线程池
pool.shutdown();

2.5 四种方式对比

方式 优点 缺点 推荐度
继承Thread 简单直接 Java单继承限制 ★★☆☆☆
实现Runnable 灵活,可继承其他类 无返回值 ★★★★☆
实现Callable 有返回值 稍复杂 ★★★★☆
线程池 复用线程,性能好 需要理解线程池原理 ★★★★★

三、线程的生命周期

3.1 六种状态

NEW(新建)
  ↓ start()
RUNNABLE(可运行)
  ↓ 获得CPU时间片
  ↓ 等待锁 / sleep / join / IO
BLOCKED(阻塞)  ← 获得锁 → RUNNABLE
WAITING(等待)   ← notify / notifyAll → RUNNABLE
TIMED_WAITING(超时等待) ← 超时结束 → RUNNABLE
  ↓ run()执行完毕
TERMINATED(终止)

3.2 状态转换示例

Thread t = new Thread(() -> {
    try {
        Thread.sleep(1000);  // TIMED_WAITING
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
// t的状态:NEW

t.start();
// t的状态:RUNNABLE

// t执行完毕后
// t的状态:TERMINATED

四、线程同步

4.1 为什么需要同步?

踩坑提醒:多线程操作共享数据时,不加同步机制会导致数据不一致。我曾经遇到过一个Bug:多个线程同时修改库存数量,结果出现超卖。这就是典型的竞态条件问题。

// 不加同步的例子:多个线程同时操作共享数据
class Counter {
    private int count = 0;
    
    public void increment() {
        count++;  // 不是原子操作!
    }
}

// count++ 实际上是3步操作:
// 1. 读取count的值
// 2. count + 1
// 3. 写回count

// 如果两个线程同时执行count++,可能出现:
// 线程A读取count=0 → 线程B读取count=0
// 线程A写入count=1 → 线程B写入count=1
// 结果应该是2,实际是1(数据不一致)

4.2 synchronized关键字

// 方式1:同步代码块
class Counter {
    private int count = 0;
    private final Object lock = new Object();
    
    public void increment() {
        synchronized (lock) {  // 锁住对象
            count++;
        }
    }
}

// 方式2:同步方法
class Counter {
    private int count = 0;
    
    public synchronized void increment() {  // 锁住this
        count++;
    }
}

// 方式3:同步静态方法
class Counter {
    private static int count = 0;
    
    public static synchronized void increment() {  // 锁住Class对象
        count++;
    }
}

4.3 Lock接口(更灵活)

经验之谈:Lock比synchronized更灵活,可以实现公平锁、可中断锁、超时获取锁等。但使用Lock时一定要在finally中释放锁,否则一旦出现异常,锁就永远释放了,其他线程会一直阻塞。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();  // 加锁
        try {
            count++;
        } finally {
            lock.unlock();  // 一定要在finally中释放锁
        }
    }
}

踩坑提醒:忘记在finally中unlock是Lock使用的常见错误。相比之下synchronized会自动释放锁,更安全一些。

4.4 volatile关键字

// volatile:保证可见性,不保证原子性
class VolatileDemo {
    private volatile boolean running = true;
    
    public void stop() {
        running = false;  // 一个线程修改
    }
    
    public void run() {
        while (running) {  // 另一个线程能立即看到变化
            // 执行任务
        }
    }
}

// volatile vs synchronized
// volatile:保证可见性,不保证原子性
// synchronized:保证可见性和原子性

五、线程通信

5.1 wait和notify

class SharedResource {
    private int count = 0;
    
    // 生产者
    public synchronized void produce() throws InterruptedException {
        while (count >= 10) {
            wait();  // 等待(释放锁)
        }
        count++;
        System.out.println("生产:" + count);
        notifyAll();  // 通知消费者
    }
    
    // 消费者
    public synchronized void consume() throws InterruptedException {
        while (count <= 0) {
            wait();  // 等待
        }
        count--;
        System.out.println("消费:" + count);
        notifyAll();  // 通知生产者
    }
}

5.2 sleep和wait的区别

特性 sleep wait
所属类 Thread Object
释放锁 不释放 释放
使用位置 任何地方 synchronized块中
唤醒方式 自动超时 notify/notifyAll

六、线程池

6.1 为什么需要线程池?

// 不用线程池:
// 每次执行任务都创建新线程
// 创建和销毁线程开销大
// 线程数量不可控,可能OOM

// 用线程池:
// 复用线程,减少创建销毁开销
// 控制最大并发数
// 提供任务队列和拒绝策略

6.2 创建线程池

import java.util.concurrent.*;

// 1. 固定大小线程池
ExecutorService pool1 = Executors.newFixedThreadPool(5);
// 5个线程,任务队列无界

// 2. 缓存线程池
ExecutorService pool2 = Executors.newCachedThreadPool();
// 线程数量不固定,按需创建

// 3. 单线程池
ExecutorService pool3 = Executors.newSingleThreadExecutor();
// 只有1个线程,保证任务按顺序执行

// 4. 定时任务线程池
ScheduledExecutorService pool4 = Executors.newScheduledThreadPool(3);
pool4.schedule(() -> System.out.println("3秒后执行"), 3, TimeUnit.SECONDS);
pool4.scheduleAtFixedRate(() -> System.out.println("定时执行"), 0, 1, TimeUnit.SECONDS);

6.3 ThreadPoolExecutor(推荐)

踩坑提醒:阿里巴巴Java开发手册明确规定,生产环境禁止使用Executors的快捷方法创建线程池!因为newFixedThreadPool和newSingleThreadExecutor使用的是无界队列,任务堆积会导致OOM;newCachedThreadPool允许无限创建线程,也会导致OOM。

// 生产环境推荐手动创建线程池(不用Executors工厂方法)
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    2,                      // 核心线程数
    5,                      // 最大线程数
    60L,                    // 空闲线程存活时间
    TimeUnit.SECONDS,       // 时间单位
    new ArrayBlockingQueue<>(100),  // 任务队列(有界队列)
    Executors.defaultThreadFactory(),  // 线程工厂
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);

// 提交任务
pool.execute(() -> System.out.println("任务1"));  // 无返回值
Future<Integer> future = pool.submit(() -> {       // 有返回值
    return 42;
});
Integer result = future.get();  // 获取结果

// 关闭线程池
pool.shutdown();  // 不接受新任务,等待已提交的任务完成

6.4 拒绝策略

策略 说明
AbortPolicy 抛出RejectedExecutionException(默认)
CallerRunsPolicy 由提交任务的线程执行
DiscardPolicy 直接丢弃任务
DiscardOldestPolicy 丢弃队列中最老的任务

参考资料

  1. Oracle官方文档 - Concurrency
  2. Baeldung - Java Concurrency Tutorial

七、面试高频考点

考点1:start和run的区别

// start():启动新线程,在子线程中执行run()
// run():普通方法调用,在当前线程中执行

追问:多次调用start会怎样?
答案:会抛出IllegalThreadStateException。线程一旦启动,就不能再次启动。

考点2:synchronized和Lock的区别

特性 synchronized Lock
锁释放 自动释放 手动unlock
中断 不可中断 可以中断
公平性 非公平 可选公平
条件绑定 一个锁一个条件 一个锁多个条件

延伸:什么情况下用Lock不用synchronized?
答案:需要公平锁、可中断锁、超时获取锁、多个条件变量时,用Lock更灵活。

考点3:线程池的核心参数

// corePoolSize:核心线程数
// maximumPoolSize:最大线程数
// keepAliveTime:空闲线程存活时间
// workQueue:任务队列
// handler:拒绝策略

追问:线程池的执行流程是怎样的?
答案:提交任务→核心线程是否已满?否:创建核心线程执行任务;是:加入队列;队列已满?否:加入队列;是:创建非核心线程;非核心线程数达上限?否:创建非核心线程;是:执行拒绝策略。


八、总结

今天我们学习了:

  • ✅ 进程和线程的概念
  • ✅ 创建线程的四种方式
  • ✅ 线程的生命周期
  • ✅ 线程同步(synchronized、Lock、volatile)
  • ✅ 线程通信(wait/notify)
  • ✅ 线程池的使用

重点记忆

  1. 推荐用Runnable或线程池创建线程
  2. start()启动线程,run()只是普通调用
  3. synchronized保证原子性和可见性
  4. volatile只保证可见性
  5. 生产环境用手动创建的ThreadPoolExecutor

🎉 JavaSE全面教学系列完结

恭喜你学完了JavaSE全面教学系列的全部15篇文章!

学习路线回顾

Week 1(Day1-5):基础语法 ✅
Week 2(Day6-10):面向对象 ✅
Week 3(Day11-15):进阶特性 ✅

下一步建议

  1. 复习本系列所有文章
  2. 把每篇的代码都自己敲一遍
  3. 刷LeetCode巩固语法
  4. 学习JavaWeb/框架(Spring Boot等)
  5. 做一个完整的项目

互动话题:恭喜你完成了JavaSE全面教学系列!你学完之后有什么收获或困惑?欢迎在评论区分享!

如果这个系列对你有帮助,欢迎点赞、收藏、关注三连支持!后续我会更新Java进阶系列,关注我不迷路 👇


本文为【JavaSE全面教学】系列第15篇(完结),感谢你的学习!

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐