Java 并发编程精讲,入门

作者:孙济伟

2026-07-20

前言

并发编程是 Java 后端面试的"必考题",但概念多、原理深,光背定义很难真正理解。这篇文章我用大白话 + 打比方的方式,把多线程的核心知识点串一遍,希望能帮你既学懂又能面试讲得清楚。


一、进程与线程的区别 ------ 工厂 vs 工人

把操作系统想象成一个大园区:

  • 进程 = 园区里的一家工厂。每家工厂有自己的厂房、设备、原材料(独立的地址空间、资源),厂和厂之间相互隔离,一家失火不会烧到隔壁。
  • 线程 = 工厂里的工人。工人们共享厂里的设备和材料(共享堆和方法区),手里各拿一张自己的任务清单(程序计数器)和笔记本(栈),各干各的活。

为什么说线程切换比进程切换开销小

换一家工厂干活,你得关门、锁设备、走一段路到新厂、开门、熟悉环境------这就是进程切换(要切换页表、刷新 TLB、重建内存映射)。

同一个工厂里换个工序呢?放下手里螺丝刀,拿个扳手继续干就行------这就是线程切换(只换栈指针和程序计数器,地址空间不变,页表不用动)。

面试追问:

Q:Java 线程和操作系统线程什么关系?

A:HotSpot 虚拟机是 1:1 映射,你 new 一个 Java 线程,底层就有一个内核线程对应。操作系统负责调度,Java 管不了,这就是为什么多线程程序有时性能反而不如预期------操作系统的调度开销不是 Java 能控制的。


二、多线程的三种实现方式 ------ 三条路,都能跑

方式一:继承 Thread 类

java 复制代码
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程运行中");
    }
}
new MyThread().start();

类比: 你直接买了个机器人(Thread),把它的芯片写死。想让它干别的?不行,只能重写芯片------这就是 Java 单继承的局限。

方式二:实现 Runnable 接口

java 复制代码
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程运行中");
    }
}
new Thread(new MyRunnable()).start();

类比: 你把任务写在一张任务单(Runnable)上,然后交给一个工人干。同一个任务单可以复印多份给不同工人------天然适合资源共享。

方式三:实现 Callable 接口 + FutureTask(带返程票)

java 复制代码
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        return 42;
    }
}
FutureTask<Integer> task = new FutureTask<>(new MyCallable());
new Thread(task).start();
Integer result = task.get();  // 等结果,像等代购回来

类比: 你把任务外包出去,对方做完能给你带个结果回来 (返回值),路上遇到坑还能喊一声"出问题了"(抛异常)。前两种是派活不管结果,Callable 是派活等着收结果。

三种方式对比

方式 返回值 异常处理 能不能再继承别的
Thread ❌ 堵死了
Runnable ✅ 随便
Callable ✅ 随便

面试追问:

Q:start() 和 run() 分不清?

A:run() 是菜谱,start() 是开火。你对着菜谱看半天(run()),菜不会自己熟;只有拧开燃气灶(start()),才算真正炒起来。


三、同步代码块 ------ 厕所里的门锁

多线程抢共享资源,就像好几个人同时想进同一个厕所。

java 复制代码
synchronized (厕所门锁) {
    // 一个人进去,锁门
    count++;  // 拉屎
    // 出来,解锁
}

没抢到锁的线程就得在外面等着------BLOCKED 状态

synchronized 的三种锁法

java 复制代码
// 1. 锁代码块 ------ 锁厕所隔间门
synchronized (lockObject) { ... }
// 2. 锁实例方法 ------ 锁整个卫生间(这个对象)
public synchronized void method() { ... }
// 3. 锁静态方法 ------ 锁整栋楼的卫生间(所有该类的实例)
public static synchronized void staticMethod() { ... }

锁升级:从"熟人模式"到"叫警察"

JDK 1.6 之后,synchronized 的锁不是一上来就很重,而是按需升级的,像安保力度逐步加强:

复制代码
无锁 → 偏向锁 → 轻量级锁 → 重量级锁  (只升不降)
  • 偏向锁(熟人模式):一个门只有你一个人进出,门上刻了你的名字,看到是你来,直接推门进,连锁都不用掏。
  • 轻量级锁(自旋协商):你和另一个人偶尔同时用到这个门,你们互相让一下(自旋等一会儿),不惊动管理员。
  • 重量级锁(叫警察来仲裁):很多人狂抢这个门,自旋等不下去了,只能叫管理员来维持秩序(操作系统内核介入,线程挂起/唤醒,开销最大)。

面试追问:

Q:synchronized 和 volatile 的区别?

A:volatile 就像门上装了个警报铃 ------值变了所有人立刻知道(保证可见性),但它不能阻止两个人同时推门(不保证原子性)。synchronized 是------一个人进去了,别人就进不去(既保证可见性,也保证原子性)。

Q:synchronized 公平吗?

A:不公平。你不排队,靠抢。synchronized 释放锁后谁抢到是谁的,不是先到先得。ReentrantLock 可以设成公平的。


四、锁(Lock)------ 更灵活的"门锁"

synchronized 虽然好用,但有些场合不够灵活。java.util.concurrent.locks.Lock 接口提供了更多功能。

ReentrantLock 用法

java 复制代码
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
    count++;
} finally {
    lock.unlock();  // 切记:一定要手动开锁!忘了就是死锁
}

Lock 比 synchronized 多了什么?

场景 synchronized ReentrantLock
开门后忘了锁 ✅ 自动锁 ❌ 得自己锁(finally)
能不能插队 ❌ 只能非公平 ✅ 公平/非公平均支持
等不及能不能走 ❌ 死等 ✅ tryLock 超时不候
能不能中途退出 ❌ 不能被打断 ✅ lockInterruptibly
叫人的精细度 只能"所有人全部叫醒" Condition 可以精确叫醒某一类

读锁 vs 写锁(ReadWriteLock):

想象一个图书馆:

  • 读锁 :看书的可以同时进好几个人(读读不互斥)

  • 写锁:管理员要整理书架,其他人全得出去(读写互斥,写写互斥)

java 复制代码
ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
rwLock.readLock().lock();   // 多人同时读
rwLock.writeLock().lock();  // 独占写

面试追问:

Q:ReentrantLock 怎么做到"可重入"的?

A:它有个计数器 state。同一个线程来一次 state +1,走一次 state -1,减到 0 才算真正释放。这就像你进自己家门------进去了还能进卧室、进书房,不会被自己锁在外面。同时它还会记住"这是谁的门"(exclusiveOwnerThread),别的线程来了一看不是自己,就得等。

Q:AQS(AbstractQueuedSynchronizer)是什么?

A:Lock 的底层核心骨架。它维护了一个 volatile int state (锁的状态)和一条 双向等待队列(排队的队伍)。抢不到锁的线程自动排到队尾,前一个人走了就唤醒下一个人。ReentrantLock、CountDownLatch、Semaphore 等全是基于 AQS 实现的------它就像乐高底座,上面搭什么积木都行。


五、等待唤醒机制 ------ 顾客和服务员

wait / notify:用 synchronized 的经典协作

想象你去一家面馆吃面:

java 复制代码
synchronized (厨房窗口) {
    while (面还没做好) {
        厨房窗口.wait();  // 面没好,先等着,把窗口让给别人
    }
    // 面好了,端走吃
}
// 后厨
synchronized (厨房窗口) {
面做好了;
厨房窗口.notify();  // 喊一声:面好了,谁来端?
// 厨房窗口.notifyAll();  // 喊:所有人的面都好了!
}

三个铁律:

  1. wait/notify 必须在 synchronized 里面------就像只能在厨房窗口(锁)前喊,在自己家里喊服务员听不到(抛 IllegalMonitorStateException)。
  2. wait() 会释放锁------把窗口让给厨师,不像 sleep() 抱着锁睡觉。
  3. 被唤醒后重新抢锁------即使被叫醒了,也得挤到窗口前挤过别人才行。

为什么 wait 要在 while 循环里?

面试高频问题。因为存在虚假唤醒------服务员可能喊了一嗓子"面好了",你的面其实还没好,只是隔壁桌的面好了(系统底层有时会一次性唤醒所有等待线程)。如果是 if 判断,醒了就往下走,你端到一碗没煮好的面。while 循环会再检查一次:面没好?继续等。

Condition:更精细的"叫号系统"

Lock 的 Condition 好比是智能叫号系统

java 复制代码
ReentrantLock lock = new ReentrantLock();
Condition 面好了 = lock.newCondition();    // 面条好了的队列
Condition 饺子好了 = lock.newCondition();  // 饺子好了的队列
java 复制代码
// 等面的顾客
lock.lock();
try {
    while (!面已做好) 面好了.await();
    // 端面
} finally { lock.unlock(); }
// 后厨喊:面好了
lock.lock();
try {
面已做好 = true;
面好了.signal();  // 只叫醒等面的,不影响等饺子的
} finally { lock.unlock(); }

synchronized 的 wait/notify 就一个等待集------像大厅里一群人一起等,服务员喊一声"好了",每个人都醒了,自己去找哪个是自己的。Condition 可以分成多个队列------"等面的坐左边,等饺子的坐右边",哪样好了叫哪样,效率高得多。

wait vs sleep

wait() sleep()
谁的方法 Object(Java 万物皆可等) Thread(线程自己睡)
睡觉交不交钥匙 ✅ 交锁(让别人进去) ❌ 抱着锁睡(别人进不去)
在哪用 必须在同步块里 随便哪都能睡
谁叫醒 notify/notifyAll 到点自己醒

六、高频面试题速记

线程的六种状态(一生)

复制代码
NEW(出生) → RUNNABLE(干活) → BLOCKED(没抢到锁)
                         → WAITING(等人叫)
                         → TIMED_WAITING(限时等)
            → TERMINATED(凉了)
  • NEW:new 出来了,还没 start()
  • RUNNABLE:start() 了,包括正准备干和正在干
  • BLOCKED:synchronized 没抢到锁,门口排队
  • WAITING:wait() 或 join(),等别人叫
  • TIMED_WAITING:sleep(3秒) 或 wait(3秒),限时等
  • TERMINATED:跑完了

实现线程安全的六种方法

  1. synchronized ------ 锁门,简单粗暴
  2. Lock(ReentrantLock)------ 功能更全的门锁
  3. volatile ------ 只保证通知到位,不保证没人抢
  4. 原子类(AtomicInteger)------ 基于 CAS,无锁并发
  5. 线程安全容器(ConcurrentHashMap 等)------ 已经封装好了,拿来就用
  6. ThreadLocal ------ 每人一份副本,你改你的我改我的

死锁 ------ 经典"筷子的故事"

四个人吃饭,桌上只有四根筷子,每人拿了一根:

  1. 互斥:一根筷子一次只能一个人拿(资源互斥)
  2. 请求与保持:我拿着这根筷子,还去够旁边的筷子(不撒手还继续要)
  3. 不可剥夺:别人不能从我手里硬抢(除非我自己放)
  4. 循环等待:每个人都等旁边的人放下筷子,形成闭环

破局方法:打破任意一个条件。最常见的是按顺序拿筷子------约定好大家都先拿编号小的,再拿编号大的,就永远不会死锁。


总结

并发编程的核心,用一个场景就能串起来:

相关推荐
weixin_408099675 小时前
2026 图片去水印 API 接口完全指南:一键去除图片水印(附 Python/Java/PHP/C# 示例)
java·python·php·图片处理·api调用·图片去水印·石榴智能
爱喝水的鱼丶5 小时前
SAP-ABAP:ALV通用封装实践——搭建可复用的ALV开发工具类,减少80%重复代码
开发语言·性能优化·sap·abap·erp·alv
钱六两5 小时前
#3、SpringAI 接入deepSeek大模型(喂饭版)
java·spring boot·ai编程
脱胎换骨-军哥6 小时前
C++/Rust无缝互操作:混合系统新常态
开发语言·c++·rust
songroom6 小时前
Kimi K3:Rust封装XTP接口详细教程实践
开发语言·后端·rust
kebeiovo6 小时前
游戏服务端开发:Actor模型详解(Go语言)
开发语言·后端·golang
迷途呀6 小时前
Python:函数中的参数类型
开发语言·笔记·python·langchain·nlp
null_177 小时前
IntelliJ IDEA 极致流畅配置方案:Ultra 9 285K + 64GB 内存实测
java·ide·intellij-idea
Herbert_hwt7 小时前
建立Java程序开发
java·开发语言
好好沉淀7 小时前
@ExcelIgnoreUnannotated 和 @AutoMapper 详解
java