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

42 阅读8分钟

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

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

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

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

2000484.jpg

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

  • 线程状态相关:有哪些状态?状态之间如何转换?状态之间的区别?
  • 多线程任务载体: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 的线程隔离套路。。。 这些可是并发安全的核心考点,必须吃透原理,练出实战手感。

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