写点什么

Java 王者修炼手册【并发篇 - 并发基础】:从线程状态到同步机制的底层修炼

作者:DonaldCen
  • 2025-12-05
    广东
  • 本文字数:3663 字

    阅读完需:约 12 分钟

Java 王者修炼手册【并发篇-并发基础】:从线程状态到同步机制的底层修炼

大家好,我是程序员强子。


又来刷英雄熟练度咯~今天专攻 Java 并发基础,必须打牢地基!


之前练的 ConcurrentHashMap 底层涉及到非常多的并发知识,并没有把细节展开,今天开始准备深入啦~


并发基础直接是整个多线程战场的 通用基础,是后续啃 JUC分布式锁框架并发处理的打底子内容,练不扎实,高阶操作全是空中楼阁!


我们来看一下,今晚我们准备练习哪些内容:


  • 线程状态相关:有哪些状态?状态之间如何转换?状态之间的区别?

  • 多线程****任务载体:Thread 类和 Runnable ,Callable 与 Runnable 区别,FutureTask 作用?

  • 通信协作相关:wait ()、notify ()、notifyAll () 、join ()等核心原理?线程同步有哪些方式?

  • 线程安全****与切换:线程不安全是指什么?本质是什么?什么是线程上下文切换?发生在哪些场景?开销体现在哪些方面?如何量化?频繁上下文切换对系统性能有什么影响?如何减少上下文切换?

线程状态

有哪些状态?


  • NEW:刚创建还没调用 start();

  • RUNNABLE:调用 start()后进入,可能在运行中状态,也能在 就绪状态 ,JVM 不区分这俩,因为 CPU 调度权在操作系统手里,JVM 只认 **能被调度 **这个状态;

  • BLOCKED

  • 等待 synchronized 锁的释放,是 被动等待

  • 没有持有任何锁(它是在抢锁的路上被拦住了)

  • 持有锁的线程释放锁(比如退出 synchronized 块),此时 JVM 会从 BLOCKED 队列里选一个线程唤醒,让它去抢锁

  • WAITING

  • 主动调用 wait()/join()后 挂机等信号

  • 无限期等待(没超时,不被唤醒就永远等)

  • 触发场景

  • object.wait(),等 notify()/notifyAll()唤醒

  • thread.join(),等线程执行完(底层是线程终止时自动调用 notifyAll())

  • LockSupport.park(),调用后进入 WAITING,等**LockSupport.unpark(thread)**唤醒

  • TIMED_WAITING:时间到了会自动唤醒,不用死等信号

  • 带超时的等待,比如 sleep(1000)或 wait(1000);

  • 触发场景

  • Thread.sleep(long):不释放任何锁,超时后自动唤醒(进入 RUNNABLE)

  • object.wait(long):释放 synchronized 锁,超时或被 notify()唤醒;

  • thread.join(long):主线程等 t 执行完,最多等 long 毫秒;

  • LockSupport.parkNanos(long)/parkUntil(long):带超时的暂停,超时或被 unpark 唤醒

  • TERMINATED:run () 执行完 退场


sleep(1000)和 wait(1000)有什么区别?


sleep(1000)和 wait(1000)都进 TIMED_WAITING


但前者抱着锁,后者放了锁**睡 **


这也是为什么 sleep()不能用来做线程协作(会占着锁不让),而 wait()可以


线程状态转换流程是怎么样的?



从 WAITING 被 notify()唤醒后,不会直接回 RUNNABLE,而是先去抢锁


抢不到就进 BLOCKED,抢到了才回 RUNNABLE;


举例:线程 A 持有锁并调用 wait()释放锁,线程 B 拿到锁后 notify()A,A 被唤醒后会先尝试重新拿锁,拿不到就堵在 BLOCKED

任务载体

Thread 类和 Runnable 有哪些区别?为什么推荐使用 Runnable?


Thread 是 线程本体,自带启动(start ())、中断等能力,我们可以通过 extends Thread 来实现一个线程。


但是如果这个类本身已经继承了一个父类呢?由于 Java 只有一个父类,所以这种场景是不是有点别扭?但是可以


implements 多个接口,所以推荐使用 Runnable 接口;


Runnable 代码示例


class FileRunnable implements Runnable {    @Override    public void run() {        // 尝试读文件(可能抛IOException,属于受检异常)        try {            Files.readAllBytes(Paths.get("test.txt"));         } catch (IOException e) {            // 只能捕获,无法向上抛(run()声明不允许抛受检异常)            e.printStackTrace();        }        // 无返回值,若想把读取结果给主线程,只能用共享变量(麻烦)    }}
复制代码


Callable 与 Runnable 有什么区别?


Callable 有返回值、支持受检异常;Runnable 无返回值、仅支持非受检异常


Callable 代码示例


// 泛型参数<String>表示返回值类型class FileCallable implements Callable<String> {    private String filePath;    public FileCallable(String filePath) {        this.filePath = filePath;    }    // 有返回值,且声明抛出受检异常    @Override    public String call() throws Exception {        // 读文件,直接抛出异常(由调用方处理)        byte[] data = Files.readAllBytes(Paths.get(filePath));        return "文件内容:" + new String(data); // 返回结果    }}
复制代码


FutureTask 有啥作用?


它能把 Callable/Runnable 包起来:


  • get():阻塞等结果

  • cancel():中途取消任务

  • 状态判断:isDone()看任务是否完成


FutureTask 代码示例


public class FutureTaskDemo {    public static void main(String[] args) throws ExecutionException, InterruptedException {        // 1. 创建Callable任务        FileCallable callable = new FileCallable("test.txt");        // 2. 用FutureTask包装Callable(FutureTask实现了Runnable)        FutureTask<String> futureTask = new FutureTask<>(callable);        // 3. 把FutureTask传给Thread(因为它是Runnable)        Thread thread = new Thread(futureTask);        thread.start();
// 4. 主线程通过futureTask获取结果(会阻塞,直到任务完成) String result = futureTask.get(); System.out.println("任务结果:" + result);
// 其他常用方法 System.out.println("任务是否完成:" + futureTask.isDone()); // true System.out.println("任务是否被取消:" + futureTask.isCancelled()); // false }}
复制代码

线程协作

多线程配合就像打配合战,没默契就会乱套,这些机制就是 战术暗号


为什么 wait ()/notify () 必须在锁内调用?


底层原因:防止 信号丢失


比如线程 A 想等信号,还没进 wait (),线程 B 就 notify () 了,A 再进 wait () 就永远等不到(信号过期)


加锁后,A 的 等信号 和 B 的 发信号 被原子化,避免这种情况


必须先拿锁(synchronized 块),否则抛 IllegalMonitorStateException


join () 让主线程 等待 的原理


主线程调用 t.join(),其实是主线程进入 t 对象的 wait () 状态(释放 t 的锁)


等 t 执行完,JVM 会自动调用 t.notifyAll()唤醒主线程


案例:主线程要汇总 3 个子线程的计算结果,用 join () 确保子线程都跑完再汇总,否则可能拿到空数据。


volatile 与 wait/notify 的区别有哪些?


volatile 只能 传状态(比如开关),不能让线程挂起


wait/notify 能 精准控节奏(比如 “生产完再消费”),但必须带锁。


所以 别用 volatile 做计数,比如 count++,因为它不保证原子性

线程安全与切换

线程不安全是指什么意思?


指多线程抢共享变量时,结果和预期不符。


本质:共享可变状态 + 非原子操作


什么是线程的上下文?


本质是线程执行到某一时刻的 全部状态信息


就像你写报告写到一半去接电话,回来时需要知道** 刚才写到第 3 段第 2 行**,光标在哪个字后面,脑子里刚想到的案例是什么


这些 状态 就是你继续写报告的 上下文


上下文具体包含这些关键信息:


  • CPU 寄存器 : 比如线程 A 正在算 1+2+3,寄存器里可能存着 3(1+2 的结果),这是它继续算下一步 +3 的基础 ,就像 你脑子里 临时记住的中间结果

  • 程序计数器(PC):记录线程下一条要执行的指令地址,就像 你夹在报告里的 书签

  • 栈指针:指向线程栈的顶部(Java 线程栈存局部变量、方法调用链路等),你记在草稿纸上的任务清单和层级

  • 内存页表:线程访问内存时的地址映射关系(虚拟地址到物理地址的转换),确保线程能正确读写自己的内存数据,你电脑里的 文件路径索引(当前需要写的文件和资料精确的位置,记不住的话就得全局翻查资料,甚至找不到丢失了思路)


什么是 上下文切换?为什么会有上下文切换?


上下文切换,就是 CPU 从执行线程 A 切换到执行线程 B 时,必须做的 保存 A 的状态 + 加载 B 的状态 的过程


会有上下文切换的原因:


CPU 核心数量有限,但需要执行的线程 / 任务数量远多于核心数


CPU 必须通过 轮流执行 让多个线程 看似同时运行,而切换就是 **轮流 **的实现方式


切换太频繁会有什么后果?如何减少切换?


时间浪费:保存 / 恢复现场占 CPU 时间(比如一次切换要 1us,100 万次就是 0.1 秒)


缓存失效:线程 A 的缓存数据对线程 B 没用,CPU 得重新从主存读,变慢。


减少切换方案:


  • 线程池核心数:CPU 密集型(比如计算)设成 核心数 ±1,IO 密集型(比如数据库操作)设成 核心数 * 2

  • 无锁编程:用 CAS(比如 AtomicInteger)代替 synchronized,线程不用阻塞等锁,减少切换

  • 虚拟线程:IO 阻塞时自动 “让出” 内核线程,避免内核级切换。

总结

今天把并发基础这块地基打牢了!


线程状态咋转换、Thread 和 Runnable 的区别、wait/notify 这些通信套路,还有线程不安全的本质、上下文切换的开销,全捋得明明白白~


下一场该练并发里的 关键字王牌了!volatile 咋保证可见性、final 在并发里的特殊作用、CAS 的底层逻辑、Unsafe 的硬核操作,还有 ThreadLocal 的线程隔离套路。。。 这些可是并发安全的核心考点,必须吃透原理,练出实战手感。


熟练度刷不停,知识点吃透稳,下期接着练~

发布于: 刚刚阅读数: 5
用户头像

DonaldCen

关注

有个性,没签名 2019-01-13 加入

跟我在峡谷学Java 公众号:程序员悟空的宝藏乐园

评论

发布
暂无评论
Java 王者修炼手册【并发篇-并发基础】:从线程状态到同步机制的底层修炼_线程安全_DonaldCen_InfoQ写作社区