💡 摘要:你是否想学 Java 多线程,却被“线程安全”、“死锁”、“volatile”等术语吓退? 别担心,多线程并不可怕,关键是要从基础开始,一步步构建认知。 本文将带你从最基础的“进程与线程”讲起,理解“并行与并发”的区别,亲手创建第一个线程,再通过一个“卖票程序”直观感受多线程带来的问题,最后引出
synchronized和volatile的必要性。 在此基础上,再深入 JVM 内存模型、三大特性(原子性、可见性、有序性)和 happens-before 原则。 从零到一,循序渐进,让你真正理解多线程的“为什么”和“怎么办”。文末附面试高频问题解析,助你稳扎稳打,轻松入门并发编程。
🌰 例子:你同时打开微信、Chrome、IDEA,它们就是三个独立的进程。
🌰 例子:一个浏览器进程可能有多个线程:
特性 | 进程 | 线程 |
|---|---|---|
所属关系 | 程序的实例 | 进程的执行单元 |
内存 | 独立内存空间 | 共享进程内存(堆) |
创建开销 | 大(系统调用) | 小(JVM 内部操作) |
通信方式 | 管道、消息队列、共享内存 | 直接共享变量 |
隔离性 | 强(一个崩溃不影响其他) | 弱(一个线程出错可能影响整个进程) |
✅ 一句话总结: 进程是“工厂”,线程是“工人”。 一个工厂(进程)可以有多个工人(线程)协同工作。
现代 CPU 都是多核的,如果程序是单线程的,只能使用一个核心,其他核心“闲着”。
✅ 多线程可以让多个任务并行执行,充分利用 CPU 资源。
概念 | 定义 | 图示 |
|---|---|---|
并行 | 多个任务同时执行(多核 CPU) | CPU0: [任务A] CPU1: [任务B] |
并发 | 多个任务交替执行(单核 CPU) | [A][B][A][B](快速切换) |
✅ Java 多线程的目标:
Java 中创建线程有两种方式:
Thread 类class MyThread extends Thread {
@Override
public void run() {
System.out.println("Hello from " + Thread.currentThread().getName());
}
}
// 使用
MyThread t = new MyThread();
t.start(); // 启动线程(不要调用 run()!)Runnable 接口(推荐)class MyTask implements Runnable {
@Override
public void run() {
System.out.println("Task running in " + Thread.currentThread().getName());
}
}
// 使用
Thread t = new Thread(new MyTask());
t.start();✅ 为什么推荐 Runnable?
public class TicketSeller {
private int tickets = 100;
public void sell() {
if (tickets > 0) {
try {
Thread.sleep(10); // 模拟卖票耗时
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
" sold ticket No." + tickets--);
}
}
public static void main(String[] args) {
TicketSeller seller = new TicketSeller();
for (int i = 1; i <= 3; i++) {
new Thread(seller::sell, "Window-" + i).start();
}
}
}运行结果可能包含:
❌ 这就是典型的线程安全问题!
tickets-- 不是原子操作它分为三步:
tickets 的值tickets如果两个线程同时执行,可能都读到 10,然后都写回 9,导致少减一次。
🔑 这就是“原子性”问题。
线程可能将 tickets 缓存在自己的工作内存中,修改后没有立即同步到主内存,其他线程看不到最新值。
🔑 这就是“可见性”问题。
虽然这个例子不明显,但在复杂逻辑中,编译器/CPU 的重排序可能导致程序行为异常。
🔑 这就是“有序性”问题。
synchronized 和 volatilesynchronized 保证原子性和可见性public synchronized void sell() {
if (tickets > 0) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() +
" sold ticket No." + tickets--);
}
}synchronized 保证同一时刻只有一个线程能进入 sell() 方法✅ 问题解决!卖票程序现在安全了。
volatile 的适用场景如果变量只做简单的读写,不需要原子性,可以用 volatile:
private volatile boolean running = true;
public void run() {
while (running) { // 每次都从主内存读取
// do work
}
}
public void stop() {
running = false; // 立即刷新到主内存
}✅
volatile保证了可见性和禁止重排序,但不保证原子性。
synchronized、volatile🚀 地基要稳,高楼才不会倒。 按照这个路径学习,你不仅能“会用”多线程,更能“理解”多线程。
答: 进程是资源分配的基本单位,有独立内存; 线程是 CPU 调度的基本单位,共享进程内存。 线程更轻量,通信更高效。
答:
Thread 类Runnable 接口
推荐 Runnable,避免单继承限制,更灵活。start() 和 run() 有什么区别?答:
start():由 JVM 启动新线程,执行run()方法run():普通方法调用,仍在当前线程执行
答: 多线程环境下,程序的行为符合预期,不会出现数据错乱。 通常需要保证原子性、可见性、有序性。
synchronized 和 volatile 的区别?答:
synchronized:保证原子性、可见性、有序性,是重量级锁volatile:只保证可见性、有序性,不保证原子性,轻量级