首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【多线程基础】

【多线程基础】

作者头像
艾伦耶格尔
发布2025-08-28 15:58:32
发布2025-08-28 15:58:32
2720
举报
文章被收录于专栏:Java基础Java基础

💡 摘要:你是否想学 Java 多线程,却被“线程安全”、“死锁”、“volatile”等术语吓退? 别担心,多线程并不可怕,关键是要从基础开始,一步步构建认知。 本文将带你从最基础的“进程与线程”讲起,理解“并行与并发”的区别,亲手创建第一个线程,再通过一个“卖票程序”直观感受多线程带来的问题,最后引出 synchronizedvolatile 的必要性。 在此基础上,再深入 JVM 内存模型、三大特性(原子性、可见性、有序性)和 happens-before 原则。 从零到一,循序渐进,让你真正理解多线程的“为什么”和“怎么办”。文末附面试高频问题解析,助你稳扎稳打,轻松入门并发编程。


一、从零开始:什么是进程?什么是线程?

1. 进程(Process)——程序的“身份证”

  • 定义:一个正在运行的程序就是一个进程
  • 特点
    • 拥有独立的内存空间(堆、栈、代码段)
    • 是操作系统资源分配的基本单位
    • 进程之间相互隔离,一个崩溃不影响另一个

🌰 例子:你同时打开微信、Chrome、IDEA,它们就是三个独立的进程。


2. 线程(Thread)——进程内的“执行流”

  • 定义:线程是进程内的一个执行单元,是 CPU 调度的基本单位。
  • 特点
    • 一个进程可以包含多个线程
    • 线程共享进程的内存(堆、方法区),但每个线程有独立的栈
    • 线程之间可以高效通信(共享变量)

🌰 例子:一个浏览器进程可能有多个线程:

  • 主线程(渲染页面)
  • 网络线程(下载资源)
  • JavaScript 引擎线程(执行脚本)

🔁 进程 vs 线程 对比

特性

进程

线程

所属关系

程序的实例

进程的执行单元

内存

独立内存空间

共享进程内存(堆)

创建开销

大(系统调用)

小(JVM 内部操作)

通信方式

管道、消息队列、共享内存

直接共享变量

隔离性

强(一个崩溃不影响其他)

弱(一个线程出错可能影响整个进程)

一句话总结进程是“工厂”,线程是“工人”。 一个工厂(进程)可以有多个工人(线程)协同工作。


二、为什么需要多线程?并行 vs 并发

1. 提升效率:充分利用 CPU

现代 CPU 都是多核的,如果程序是单线程的,只能使用一个核心,其他核心“闲着”。

多线程可以让多个任务并行执行,充分利用 CPU 资源。


2. 并行(Parallelism) vs 并发(Concurrency)

概念

定义

图示

并行

多个任务同时执行(多核 CPU)

CPU0: [任务A] CPU1: [任务B]

并发

多个任务交替执行(单核 CPU)

[A][B][A][B](快速切换)

Java 多线程的目标

  • 在多核 CPU 上实现并行
  • 在单核 CPU 上实现并发,提高响应速度

3. 多线程的典型应用场景

  • Web 服务器:同时处理多个用户请求
  • GUI 程序:主线程处理界面,后台线程处理耗时任务(如文件下载)
  • 数据处理:并行计算、大数据分析
  • 游戏开发:渲染、逻辑、网络通信分离

三、动手实践:创建你的第一个线程

Java 中创建线程有两种方式:

方式一:继承 Thread 类

代码语言:javascript
复制
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello from " + Thread.currentThread().getName());
    }
}

// 使用
MyThread t = new MyThread();
t.start(); // 启动线程(不要调用 run()!)

方式二:实现 Runnable 接口(推荐)

代码语言:javascript
复制
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?

  • Java 不支持多继承,但可以实现多个接口
  • 更符合“组合优于继承”的设计原则

四、多线程的“陷阱”:从一个卖票程序说起

1. 需求:模拟 3 个窗口卖 100 张票

代码语言:javascript
复制
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();
        }
    }
}

2. 问题出现了!

运行结果可能包含:

  • 卖出第 0 张票
  • 同一张票被卖了两次
  • 总共卖出了超过 100 张票

这就是典型的线程安全问题!


3. 问题根源分析

🧩 问题 1:tickets-- 不是原子操作

它分为三步:

  1. 读取 tickets 的值
  2. 减 1
  3. 写回 tickets

如果两个线程同时执行,可能都读到 10,然后都写回 9,导致少减一次。

🔑 这就是“原子性”问题

🧩 问题 2:内存可见性

线程可能将 tickets 缓存在自己的工作内存中,修改后没有立即同步到主内存,其他线程看不到最新值。

🔑 这就是“可见性”问题

🧩 问题 3:指令重排序

虽然这个例子不明显,但在复杂逻辑中,编译器/CPU 的重排序可能导致程序行为异常。

🔑 这就是“有序性”问题


五、解决方案:synchronized 和 volatile

1. 使用 synchronized 保证原子性和可见性

代码语言:javascript
复制
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() 方法
  • 进入和退出同步块时,会强制工作内存与主内存同步

✅ 问题解决!卖票程序现在安全了。


2. volatile 的适用场景

如果变量只做简单的读写,不需要原子性,可以用 volatile

代码语言:javascript
复制
private volatile boolean running = true;

public void run() {
    while (running) { // 每次都从主内存读取
        // do work
    }
}

public void stop() {
    running = false; // 立即刷新到主内存
}

volatile 保证了可见性禁止重排序,但不保证原子性


六、总结:正确的学习路径

  1. 先理解“是什么”:进程、线程、并行、并发
  2. 再动手“怎么做”:创建线程,写一个多线程程序
  3. 发现问题“为什么”:观察线程安全问题
  4. 学习“怎么解决”synchronizedvolatile
  5. 最后深入“原理”:JMM、原子性、可见性、有序性

🚀 地基要稳,高楼才不会倒。 按照这个路径学习,你不仅能“会用”多线程,更能“理解”多线程。


七、面试高频问题(基础版)

❓1. 进程和线程的区别?

: 进程是资源分配的基本单位,有独立内存; 线程是 CPU 调度的基本单位,共享进程内存。 线程更轻量,通信更高效。


❓2. 如何创建线程?哪种方式更好?

  1. 继承 Thread 类
  2. 实现 Runnable 接口 推荐 Runnable,避免单继承限制,更灵活。

❓3. start() 和 run() 有什么区别?

start():由 JVM 启动新线程,执行 run() 方法 run():普通方法调用,仍在当前线程执行


❓4. 什么是线程安全?

: 多线程环境下,程序的行为符合预期,不会出现数据错乱。 通常需要保证原子性、可见性、有序性。


❓5. synchronized 和 volatile 的区别?

  • synchronized:保证原子性、可见性、有序性,是重量级锁
  • volatile:只保证可见性、有序性,不保证原子性,轻量级
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-08-27,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、从零开始:什么是进程?什么是线程?
    • 1. 进程(Process)——程序的“身份证”
    • 2. 线程(Thread)——进程内的“执行流”
      • 🔁 进程 vs 线程 对比
  • 二、为什么需要多线程?并行 vs 并发
    • 1. 提升效率:充分利用 CPU
    • 2. 并行(Parallelism) vs 并发(Concurrency)
    • 3. 多线程的典型应用场景
  • 三、动手实践:创建你的第一个线程
    • 方式一:继承 Thread 类
    • 方式二:实现 Runnable 接口(推荐)
  • 四、多线程的“陷阱”:从一个卖票程序说起
    • 1. 需求:模拟 3 个窗口卖 100 张票
    • 2. 问题出现了!
    • 3. 问题根源分析
      • 🧩 问题 1:tickets-- 不是原子操作
      • 🧩 问题 2:内存可见性
      • 🧩 问题 3:指令重排序
  • 五、解决方案:synchronized 和 volatile
    • 1. 使用 synchronized 保证原子性和可见性
    • 2. volatile 的适用场景
  • 六、总结:正确的学习路径
  • 七、面试高频问题(基础版)
    • ❓1. 进程和线程的区别?
    • ❓2. 如何创建线程?哪种方式更好?
    • ❓3. start() 和 run() 有什么区别?
    • ❓4. 什么是线程安全?
    • ❓5. synchronized 和 volatile 的区别?
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档