多线程 - 初阶

目录

1.认识线程

1.1概念

1)线程是什么

官方

自身理解

2)为什么要有线程

官方

自身理解

3)进程和线程的区别

官方

自身理解

4)Java 的线程 和 操作系统线程 的关系

官方

自身理解

1.2第一个多线程程序

代码

自身理解

观察的两种方式

1.使⽤ jconsole 命令并且观察线程

1)运行jconsole.exe(在安装的jdk的bin目录里面)

2)运行结果图

3)线程的选择及其解释

2.使用编译器idea断点观察

1.3创建线程

方法1 继承 Thread 类

代码

自身理解

方法2 实现 Runnable接口

代码

自身理解​编辑

方法3 继承Thread,重写run,但是使用匿名内部类

代码

自身理解

方法4 实现Runnable,重写run,匿名内部类

代码

理解

方法5 [常用/推荐]使用lambda表达式

代码

自身理解

1.4多线程的优势-增加运行速度

2. Thread 类及常见方法

2.1Thread 的创建构造方法

语法

具体设置名称例子

2.2Thread的几个常见属性

语法

isAlive()的例子

2.3 启动⼀个线程 - start()

官方

自身理解

2.4 终止⼀个线程

官方

自身理解

2.5 等待⼀个线程 - join()

官方

自身理解

2.6 获取当前线程引用

方法

​编辑

代码

注意

2.7 休眠当前线程

方法

​编辑

代码

自身理解

3. 线程的状态

3.1 观察线程的所有状态

官方

自己理解

4. 多线程带来的的风险-线程安全 (重点)

4.1概念

官方

自身理解

4.2不安全的例子

代码

为什么会这样

一、要点总结

二、生活例子(以 “两人同时记账” 类比)

4.3线程不安全的原因

1.根本原因

2.代码结构因素

3.直接原因

4.4 解决之前的线程不安全问题

1.针对 “系统抢占式执行”(根本原因)

2.针对 “代码结构”(多线程改同一变量)

3.针对 “操作非原子性”(直接原因)

自身理解

5. synchronized 关键字 - 监视器锁 monitor lock

5.1 synchronized 的特性

1) 互斥

官方

自身理解

2) 可重入

官方

自身理解

5.2 synchronized 使用示例

1) 修饰代码块: 明确指定锁哪个对象.

锁任意对象

锁当前对象

2) 直接修饰普通方法: 锁的 SynchronizedDemo 对象

3) 修饰静态方法: 锁的 SynchronizedDemo 类的对象

5.3 Java 标准库中的线程安全类

不安全

安全

6. volatile 关键字

官方

功能

自身理解

7. wait 和 notify notifyAll

官方

功能

7.1 wait()方法

概述

自身理解

什么情况下使用

wait注意点

wait 的两种等待方式

wait 和 notify 的 “配对” 规则

代码1

注意点

wait的内部做了三件事

报错原因

解决方案

代码2

运行结果

运行流程

自身理解

7.2 notify()方法

概述

自身理解

7.3 notifyAll()方法

官方

自身理解

 多线程的小结

​编辑

8. 多线程案例

8.1 单例模式

官方

自身理解

饿汉模式 - 单线程版

代码

代码解释

流程

自身理解

懒汉模式-单线程版

代码

代码解释

流程

自身理解

注意

8.2 阻塞队列

阻塞队列是什么

一、阻塞队列:线程安全的 “智能排队系统”

二、生产者消费者模型:分工协作的 “包饺子流水线”

1.解耦合

2.削峰填谷

三、解耦:分布式系统的 “快递中转站”

标准库中的阻塞队列

文字总结

自身理解

一、技术内容总结

二、生活例子类比

阻塞队列实现

8.3 定时器

定时器是什么

标准库中的定时器

实现定时器

一、技术内容总结

1. Timer(定时器)的核心组成

2. Java 标准库中的 Timer 使用

二、自身理解

1. Timer 组成类比

2. 标准库 Timer 使用类比

代码

流程图

代码解释

main部分

class MyTimer部分

class MyTimerTask部分

代码途中的问题(重点)

1.怎么计算时间定时

​编辑

2.任务时间和当前时间的差

3.为什么要加锁

4.加锁的位置

5.加完锁后,所以地方都锁吗

6.等待使用wait还是sleep

7.代码执行流程是怎么样

8.4 线程池

线程池是什么

标准库中的线程池

1. corePoolSize(核心线程数)

2. maximumPoolSize(最大线程数)

3. keepAliveTime(存活时间)

4. unit(时间单位)

5. workQueue(任务队列)

6. threadFactory(线程工厂)

7. handler(拒绝策略)

拒绝的4个方法/策略

1. AbortPolicy(抛异常拒绝)

2. CallerRunsPolicy(提交者自己处理)

3. DiscardOldestPolicy(丢弃最旧任务)

4. DiscardPolicy(默默丢弃新任务)

实现线程池

方法​编辑

代码

多线程总结

扩展问题

一、基础概念类

线程和进程的区别是什么?

线程的生命周期有哪些状态?

如何创建线程?有哪些方式?

线程的常用方法有哪些?

二、Thread类

1.start和run有什么区别?

2.如何启动更多线程?

3.怎么终止线程?

方法一:要给标志位上加 volatile 关键字

方法二:使用Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.

4.catch写法不同,为什么输出结果不同?

5.等待线程的执行顺序是怎么样的?

三、线程安全(重点)

1.为什么要学习线程安全?

2.怎么利用 synchronized 来加锁?

3.死锁是啥?怎么解决

官方

自身理解

发生死锁的必要条件

典型死锁场景

破坏死锁的简单方法

死锁代码 + 解决死锁的代码

4.内存可见性引起的线程安全问题

概念

例子

官方解释

自身解释

解决代码

5.饿汉模式和懒汉模式的差别

图展示

自身理解

6.饿汉模式和懒汉模式谁是线程安全

饿汉模式(是天然线程安全)

懒汉模式(不是天然)

1. 初始懒汉式(线程不安全)

2. 加锁版本(解决安全,但效率低)

3. 双重检查 + volatile(最终线程安全)

7.饿汉模式和懒汉模式的使用场景

1. 资源紧张场景

2. 实例使用不确定场景

3. 延迟加载需求场景

1. 资源占用少场景

2. 追求快速启动场景

3. 高并发且线程安全场景

四、锁机制类

synchronized的原理是什么?

ReentrantLock和synchronized的区别是什么?

什么是锁的可重入性?

五、volatile关键字

volatile 和 synchronized 的区别

什么情况下使用volatile

流程图

文字描述

代码

自身理解

六、wait

wait 和 sleep 的对比(重点)

相同点

不同点

七、线程池

为什么使用线程池

自身理解

一、开了一家餐厅:

二、线程池的核心优势

三、现实中的应用场景

四、总结

线程池的核心参数有哪些?

什么是CPU/IO密集型的任务

1. CPU 密集型任务

2. IO 密集型任务

总结对比

如何设定线程的数量

1. CPU 密集型任务的线程数量策略

2. IO 密集型任务的线程数量策略

总结对比


1.认识线程

1.1概念

1)线程是什么

官方
⼀个线程就是⼀个 "执行流". 每个线程之间都可以按照顺序执⾏⾃⼰的代码. 多个线程之间 "同时" 执⾏
着多份代码.
自身理解

用生活例子理解:线程就像 工厂里一条独立的生产线。工厂要完成很多任务,单靠一条生产线(比如只生产杯子)效率低。有多条生产线(线程),就能同时生产杯子、盘子等不同物品,让工厂整体效率变高。放在电脑里,程序通过线程实现 “同时做几件事”,比如一边下载文件(一个线程)、一边播放音乐(另一个线程)。

2)为什么要有线程

官方
⾸先, "并发编程" 成为 "刚需".
单核 CPU 的发展遇到了瓶颈. 要想提高算力, 就需要多核 CPU. ⽽并发编程能更充分利⽤多核 CPU
资源.
有些任务场景需要 "等待 IO", 为了让等待 IO 的时间能够去做⼀些其他的⼯作, 也需要⽤到并发编程.
其次, 虽然多进程也能实现 并发编程, 但是线程⽐进程更轻量.
创建线程⽐创建进程更快.
销毁线程⽐销毁进程更快.
调度线程⽐调度进程更快.
最后, 线程虽然⽐进程轻量, 但是⼈们还不满⾜, 于是⼜有了 "线程池"(ThreadPool) 和 "协程"
(Coroutine)
自身理解

在生活中,线程就好比餐厅里的多个服务员。一个服务员(单线程)一次只能为一位顾客点单、上菜、结账,效率很低。但如果有多个服务员(多线程),就可以同时为不同顾客服务,有的点单,有的上菜,有的结账,餐厅就能同时处理更多顾客的需求,整体效率大幅提高 。

3)进程和线程的区别

官方
进程是包含线程的. 每个进程⾄少有⼀个线程存在,即主线程。
进程和进程之间不共享内存空间. 同⼀个进程的线程之间共享同⼀个内存空间
进程是系统分配资源的最⼩单位,线程是系统调度的最⼩单位。
⼀个进程挂了⼀般不会影响到其他进程. 但是⼀个线程挂了, 可能把同进程内的其他线程⼀起带⾛(整个进程崩溃).
自身理解

  • 资源占用:比如电脑上同时运行的音乐播放器程序和浏览器程序,它们是两个不同的进程,有各自独立的资源。而线程共享所属进程的资源,相当于大房子里不同的人,共享房子里的空间和设施 。
  • 独立性:进程相互独立,不同进程之间不能直接共享数据,就像不同房子之间是隔开的。线程之间的隔离性没那么强,同一进程内的线程可以直接访问共享数据,比如一个程序里多个线程可以同时访问程序中的全局变量。
  • 上下文切换开销:进程切换时,因为各自独立,要保存和恢复大量信息,开销大,就像把一个大房子里的所有东西都收拾好放到一边,再把另一个大房子的东西拿出来布置,很麻烦。线程切换相对简单,因为共享进程的资源,开销小,类似大房子里不同人的位置变换,没那么复杂。
  • 控制方式:进程的创建和管理一般通过操作系统的指令或接口来操作;线程在程序里就可以通过代码灵活创建、销毁和管理,比如在 Java 中可以用代码轻松创建新线程。

4)Java 的线程 和 操作系统线程 的关系

官方
线程是操作系统中的概念. 操作系统内核实现了线程这样的机制, 并且对⽤⼾层提供了⼀些 API 供⽤⼾
使⽤(例如 Linux 的 pthread 库).
Java 标准库中 Thread 类可以视为是对操作系统提供的 API 进⾏了进⼀步的抽象和封装
自身理解
  • 联系:Java 线程需要依赖操作系统线程来实现底层的运行和调度。就像你用手机 APP(Java 线程),APP 的各种操作最终要靠手机系统(操作系统线程)来执行。Java 虚拟机(JVM )在运行时会和操作系统交互,把 Java 线程映射到操作系统线程上 ,让 Java 线程能利用系统资源执行任务。
  • 区别
    • 调度方式:操作系统线程由操作系统内核调度,而 Java 线程是由 JVM 调度。可以理解为,操作系统线程是听从操作系统这个 “大管家” 的安排,Java 线程则是在 JVM 这个 “小管家” 的管理下运行 。
    • 上下文切换成本:Java 线程切换时,要先在 JVM 中保存和恢复线程状态等信息,开销比操作系统线程切换更高。比如切换两个操作系统线程像在两个相邻房间快速走动;而切换 Java 线程,就像在房间里还要先整理好东西再出去,更费时间。
    • 系统资源占用:操作系统线程有自己的线程栈、线程控制块等系统资源;Java 线程除了这些,还需要 Java 堆内存和栈内存,所以 Java 线程消耗的系统资源略多一些 。

1.2第一个多线程程序

代码
package thread;

// 1.创建一个自己的类,继承这个 Thread
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("Hello world");
    }
}

public class ThreadDemo1 {

    public static void main(String[] args) {

        // 2.根据刚才的类,创建出实例,(线程实例,才是真正的线程)
        //new 自己上面自定义的类名
        //MyThread t = new MyThread();虽然语法正确,但属于 “子类引用指向子类对象”,缺少多态带来的灵活性
        Thread t = new MyThread();
        // 3.调用 Thread 的star方法,才会真正调用系统api,在系统内部创建出线程
        t.start();

    }

}
自身理解

观察的两种方式

1.使⽤ jconsole 命令并且观察线程
1)运行jconsole.exe(在安装的jdk的bin目录里面)
2)运行结果图

3)线程的选择及其解释

2.使用编译器idea断点观察

1.3创建线程

方法1 继承 Thread 类

代码
package thread;

class MyThread2 extends Thread{ 
    @Override
    public void run() {
        while (true){
            System.out.println("hello word");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

}
public class ThreadDemo2 {
    public static void main(String[] args) {
        //创建 MyThread 类的实例
        Thread t = new MyThread2();
        
        //调⽤ start ⽅法启动线程
        t.start();

        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }

}
自身理解

方法2 实现 Runnable接口

代码
package thread;

class MyThread3 implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("hello runnable");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}
public class ThreadDemo3 {

    public static void main(String[] args) {
        //Runnable runnable = new MyThread3();
        //Thread t = new Thread(runnable);
        //下面是上面两段代码的精简写法
        Thread t = new Thread(new MyThread());
        t.start();

        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }

}
自身理解

方法3 继承Thread,重写run,但是使用匿名内部类

代码
package thread;

public class ThreadDemo4 {

    public static void main(String[] args) {
        // 使⽤匿名类创建 Thread ⼦类对象

        //new Runnable() { ... }:是一个匿名内部类
       //类名            实例
        Thread t = new Thread(){

            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };

        t.start();

        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }

}
自身理解

方法4 实现Runnable,重写run,匿名内部类

代码
package thread;

public class ThreadDemo5 {

    public static void main(String[] args) {
        // 使⽤匿名类创建 Runnable ⼦类对象
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello runnable");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        });

        t.start();

        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }

}
理解

方法5 [常用/推荐]使用lambda表达式

代码
package thread;

public class ThreadDemo6 {

    public static void main(String[] args) {
        Thread t = new Thread(()-> {

            while (true){
                System.out.println("hello thread");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }

        });

        t.start();

        while (true){
            System.out.println("hello main");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }

    }
}
自身理解

1.4多线程的优势-增加运行速度


public class ThreadDemo8 {
    private static final int ARRAY_SIZE = 100000000;
    private static final int THREAD_COUNT = 4;
    private static int[] array;

    public static void main(String[] args) {
        // 初始化数组
        array = new int[ARRAY_SIZE];
        for (int i = 0; i < ARRAY_SIZE; i++) {
            array[i] = i + 1;
        }

        // 单线程计算
        long singleThreadStartTime = System.currentTimeMillis();
        long singleThreadSum = singleThreadSum();
        long singleThreadEndTime = System.currentTimeMillis();
        System.out.println("单线程计算结果: " + singleThreadSum);
        System.out.println("单线程计算耗时: " + (singleThreadEndTime - singleThreadStartTime) + " 毫秒");

        // 多线程计算
        long multiThreadStartTime = System.currentTimeMillis();
        long multiThreadSum = multiThreadSum();
        long multiThreadEndTime = System.currentTimeMillis();
        System.out.println("多线程计算结果: " + multiThreadSum);
        System.out.println("多线程计算耗时: " + (multiThreadEndTime - multiThreadStartTime) + " 毫秒");
    }

    // 单线程求和方法
    private static long singleThreadSum() {
        long sum = 0;
        for (int i = 0; i < ARRAY_SIZE; i++) {
            sum += array[i];
        }
        return sum;
    }

    // 多线程求和方法
    private static long multiThreadSum() {
        SumThread[] threads = new SumThread[THREAD_COUNT];
        int chunkSize = ARRAY_SIZE / THREAD_COUNT;

        // 创建并启动线程
        for (int i = 0; i < THREAD_COUNT; i++) {
            int start = i * chunkSize;
            int end = (i == THREAD_COUNT - 1) ? ARRAY_SIZE : (i + 1) * chunkSize;
            threads[i] = new SumThread(start, end);
            threads[i].start();
        }

        // 等待所有线程执行完毕
        for (SumThread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 汇总所有线程的计算结果
        long sum = 0;
        for (SumThread thread : threads) {
            sum += thread.getPartialSum();
        }
        return sum;
    }

    // 自定义线程类,用于计算数组部分元素的和
    static class SumThread extends Thread {
        private final int start;
        private final int end;
        private long partialSum;

        public SumThread(int start, int end) {
            this.start = start;
            this.end = end;
        }

        @Override
        public void run() {
            partialSum = 0;
            for (int i = start; i < end; i++) {
                partialSum += array[i];
            }
        }

        public long getPartialSum() {
            return partialSum;
        }
    }
}


/*
单线程计算结果: 5000000050000000
单线程计算耗时: 28 毫秒
多线程计算结果: 5000000050000000
多线程计算耗时: 20 毫秒
*/

2. Thread 类及常见方法

2.1Thread 的创建构造方法

语法

//各个部分的举例子
Thread t1 = new Thread();
Thread t2 = new Thread(new MyRunnable());
Thread t3 = new Thread("这是我的名字");
Thread t4 = new Thread(new MyRunnable(), "这是我的名字");

具体设置名称例子

package thread;

public class ThreadDemo7 {

    public static void main(String[] args) {
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println("hello thread");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        },"这是我的线程");


        //在 start 之前,设置线程为 后台线程(不能在 start 之后设置)
        t.setDaemon(true);

        t.start();

    }

}

2.2Thread的几个常见属性

语法

isAlive()的例子

public class ThreadDemo8 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            //这个线程的运行时间大约是 1s
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
        System.out.println("start 之前:" + t.isAlive());
        t.start();
        System.out.println("start 之后:" + t.isAlive());
        Thread.sleep(2000);

        //2s 之后,线程 t 已经结束了
        System.out.println("t 之后:" + t.isAlive());
    }
}

2.3 启动⼀个线程 - start()

官方

  1. 启动方式:在 Java 中,若想让写好的线程代码 “动起来”,需用 Thread 类的 start() 方法启动线程。
  2. 唯一性:对同一个 Thread 对象,start() 只能调用一次。多次调用会让程序 “混乱”,导致错误。
  3. 本质:调用start创建出新的线程本质上是start会调用系统的api,来完成创建线程的操作

自身理解
  • 写好的线程代码:好比一份 “备餐计划”(比如 “准备汉堡:煎肉饼、切蔬菜、组装”)。
  • Thread 对象:相当于一个 “备餐小组”,这个小组 “拿着” 备餐计划,知道要做什么。
  • start() 方法:类似 “店长下令开工”。店长对这个备餐小组只能下一次 “开工令”(调用一次 start())。一旦下令,餐厅(电脑系统)会真正安排资源(厨房设备、员工时间等),让小组按计划开始备餐(系统通过 API 创建线程,执行代码)。
    如果对同一个备餐小组多次下 “开工令”,店长会困惑、员工也会乱套,程序同理 —— 多次调用 start() 会让程序报错,无法正常运行。

2.4 终止⼀个线程

官方

让线程run方法(入口方法)执行完毕

自身理解
  • 线程的 run 方法:好比外卖员接到的 “送外卖任务清单”,里面写着要给哪些地址送餐、取餐路线等具体操作。
  • 线程终止:当外卖员按照任务清单,把所有订单都送到顾客手里(run 方法里的代码执行完),没有额外的送餐任务了,此时送外卖这个 “工作流程” 就结束了,相当于线程终止。
    简单说,线程就像外卖员的工作,做完 run 里定义的所有事,自然就 “下班”(终止)了。

2.5 等待⼀个线程 - join()

官方
  1. 核心作用join() 让一个线程等待另一个线程完成工作,强制设定线程结束先后顺序
  2. 线程执行常态:通常多个线程各自运行,谁先结束是随机的,像多个工人同时干活,完工时间不确定。
  3. 调整执行顺序join() 能干预这种随机性,比如线程 B 调用 join() 等待线程 A,就确保线程 A 先结束,线程 B 再结束。
  4. 阻塞特性:等待过程中,使用 join() 的线程会暂停,无法执行其他操作,这就是 “阻塞”。
自身理解
  • 线程类比:组装电脑需要多步骤,比如 “装主板”(线程 A)和 “装系统”(线程 B)。正常情况下,若两人同时做,谁先完成不确定。
  • join() 的应用:如果负责 “装系统” 的人说:“我得等你装完主板,我再装系统”(即线程 B 对线程 A 调用 join())。此时,一定是 “装主板” 的人先完成(线程 A 先结束),“装系统” 的人才能开始,最后完成装系统(线程 B 结束)。
  • 阻塞体现:在等 “装主板” 完成的时间里,负责 “装系统” 的人只能闲着,不能干别的,这就是 join() 导致的阻塞。
  • 口诀:谁使用join(),剩下的人都要等他结束才行。

简单理解:join() 就像生活中 “排队接力”,必须等前一个人做完,后一个人才能动手,等待时后一个人只能暂停(阻塞),以此确定做事的先后顺序。

2.6 获取当前线程引用

方法

代码
public class ThreadDemo {
 public static void main(String[] args) {

 Thread thread = Thread.currentThread();
 System.out.println(thread.getName());

 }
}

注意

2.7 休眠当前线程

方法

代码
public class ThreadDemo {
 public static void main(String[] args) throws InterruptedException {
     System.out.println(System.currentTimeMillis());
     Thread.sleep(3 * 1000);
     System.out.println(System.currentTimeMillis());
 }
}

自身理解
因为线程的调度是不可控的,所以,这个方法只能保证,实际休眠时间是大于等于参数设置的休眠时间

3. 线程的状态

3.1 观察线程的所有状态

官方

自己理解

假设你去银行办业务,整个流程就像线程状态的变化:

  1. NEW(新建):你拿了排队号,但还没轮到你去窗口办业务。
  2. RUNNABLE(可运行):轮到你去窗口,正在办业务(执行中);或者你在窗口前准备好材料,随时能开始办(就绪状态)。
  3. BLOCKED(阻塞):你想去某个窗口办业务,但窗口被别人占用了,你只能排队等这个窗口 “解锁”。
  4. WAITING(等待):办业务时,工作人员说 “必须等你朋友来了才能办”,你没期限地干等,直到朋友出现。
  5. TIMED_WAITING(定时等待):你跟工作人员说 “我先等 10 分钟,10 分钟后朋友没来,我就不等了”,这 10 分钟就是定时等待。
  6. TERMINATED(终止):业务办完,你离开银行,整个办事流程结束。

通过这个例子理解:线程就像人在银行办事,不同状态对应不同的办事阶段,抢资源、等待条件、定时等待等操作,最终做完事就 “终止”。

4. 多线程带来的的风险-线程安全 (重点)

4.1概念

官方

自身理解

  • 线程安全场景
    食堂打饭窗口有明确规则:排队打饭,一次只允许一个人到窗口打饭(相当于代码有 “保护机制”)。不管是只有你一个人打饭(单线程),还是很多人排队打饭(多线程),大家都按规则来,不会乱,最后都能顺利打到饭,这就是 “线程安全”。

  • 线程不安全场景
    食堂打饭窗口没有规则,很多人同时挤到窗口抢着打饭(多线程执行无保护的代码)。这时候可能有人插队、有人打饭的量被弄错,甚至饭盆被碰翻(出现 bug)。但如果只有你一个人打饭(单线程),没有争抢,一切顺利。这种 “单线程没问题,多线程就乱套” 的情况,就是 “线程不安全”。

4.2不安全的例子

代码
package thread;

//线程不安全的例子

public class ThreadDemo19 {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        Thread t2 = new Thread(() ->{
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });

        t1.start();
        t2.start();

        t1.join();
        t2.join();
        
        // 预期结果应该是 10w
        // 但是实际的结果并不是这样,而且还不到10w
        System.out.println("count = " + count);

    }

}

为什么会这样

如果是一个线程执行上述的三个指令,当然没问题
如果是两个线程,并发的执行上述操作,此时就会存在变数!!!(线程之间调度的顺序是不确定的!)

一、要点总结
  1. 代码核心问题

    • 代码中 count++ 操作在单线程下正常,但多线程下会出问题。因为 count++ 底层由三个 CPU 指令组成:
      • 读取(load):从内存读数据到 CPU 寄存器;
      • 计算(add):寄存器里的数据 +1;
      • 保存(save):把结果写回内存。
    • 多线程时,CPU 调度线程的顺序不可控,可能导致两个线程的指令交叉执行,最终结果比预期值(10 万)小。
  2. 线程不安全的本质
    多个线程操作共享数据(如代码里的 count)时,因操作步骤拆分且调度无序,导致数据被错误修改。

二、生活例子(以 “两人同时记账” 类比)

假设你和朋友共用一个记账本,记录家庭总支出。每次花钱,就在本子上的数字基础上加 1。

  • 理想流程(单线程)
    你先拿本子(读取当前数字)→ 计算加 1(如从 5 算成 6)→ 写回本子(保存结果)。朋友等你完成后再操作,最后数字一定正确。
  • 混乱场景(多线程)
    1. 你和朋友同时抢着拿本子(两个线程同时 “读取数据”);
    2. 你们各自在心里把数字加 1(各自 “计算”);
    3. 然后你们都把加 1 后的数字写回本子(“保存”)。但本子上最终只加了 1(比如从 5 变成 6),而不是加 2。
      原因:朋友看到的是你操作前的数字,你们的操作互相覆盖了。这就像代码里的 count++,多线程交叉操作导致结果错误,体现了线程不安全。

4.3线程不安全的原因

1.根本原因

操作系统中线程采用 “抢占式执行” 和 “随机调度” 机制,线程间执行顺序存在不确定性,为线程安全问题埋下隐患。

2.代码结构因素

仅当多个线程同时修改同一个变量时,才会引发线程安全问题。以下情况无安全风险:单个线程修改一个变量;多个线程仅读取同一个变量(变量内容不变);多个线程修改不同变量。

3.直接原因

多线程对共享变量的修改操作不具备 “原子性”。例如 count++ 会被拆解为多个 CPU 指令,若线程执行到一半被调度切走,其他线程介入操作,就会破坏数据一致性。而单个 CPU 指令本身是原子的(要么不执行,要么完整执行)。

4.4 解决之前的线程不安全问题

1.针对 “系统抢占式执行”(根本原因)

无法干预,因操作系统内核已实现该机制,修改等同于开发新系统,难以推广使用。

2.针对 “代码结构”(多线程改同一变量)

分情况处理,部分代码结构可调整,部分受限于业务逻辑等因素无法调整。

3.针对 “操作非原子性”(直接原因)

通过加锁解决。锁具备互斥、排他特性(如 Java 的synchronized关键字),将多个非原子操作(如count++拆解的多个 CPU 指令)“打包” 为原子操作,确保执行的整体性,避免线程安全问题。

自身理解

5. synchronized 关键字 - 监视器锁 monitor lock

5.1 synchronized 的特性

1) 互斥

官方
某个线程执行到某个对象的 synchronized 中时, 其他线程如果也执⾏
到同⼀个对象 synchronized 就会阻塞等待.
进⼊ synchronized 修饰的代码块, 相当于 加锁
退出 synchronized 修饰的代码块, 相当于 解锁
自身理解

排队等待窗口打饭刷钱,如果不刷钱,下一位同学就不打饭刷钱。

2) 可重入

官方
同步块对同⼀条线程来说是可重入的,不会出现自己把自己锁死的问题。
自身理解

整个房子只有一道指纹锁(大门)。你用指纹打开大门后,屋内所有房间都不需要再单独开锁,可以自由进出。

5.2 synchronized 使用示例

synchronized 本质上要修改指定对象的 "对象头". 从使⽤⻆度来看, synchronized 也势必要搭配⼀个 具体的对象来使用。

1) 修饰代码块: 明确指定锁哪个对象.

锁任意对象
public class SynchronizedDemo {
 private Object locker = new Object();
 
 public void method() {
     synchronized (locker) {
 
     }
 }
}
锁当前对象
public class SynchronizedDemo {
 public void method() {
     synchronized (this) {
 
     }
 }
}

2) 直接修饰普通方法: 锁的 SynchronizedDemo 对象

public class SynchronizedDemo {
 public synchronized void methond() {
 }
}

3) 修饰静态方法: 锁的 SynchronizedDemo 类的对象

public class SynchronizedDemo {
 public synchronized static void method() {
 }
}

5.3 Java 标准库中的线程安全类

不安全

ArrayList,LinkedList,HashMap,TreeMap,HashSet,TreeSet,StringBuilder

安全
Vector (不推荐使用) ,HashTable (不推荐使用) ,ConcurrentHashMap,StringBuffer

6. volatile 关键字

官方

其中一个核心功能,就是保证内存可见性
(另一个功能,禁止指令重排序)

功能

  1. volatile 能保证内存可见性:让多个线程都能及时看到变量的最新值,避免重复做 “无效读取”(类似文段里重复执行结果不变的 load 操作)。
  2. 禁止指令重排:确保相关操作按代码顺序执行,不乱序。
  3. volatile 不保证原子性:

自身理解

想象全班同学靠班级公告栏获取作业通知。

  • 数据可见性:老师更新作业通知后,volatile 就像立刻给全班 “广播”,每个同学都能马上看到最新通知,不会有人还盯着旧内容看。
  • 禁止指令重排:老师贴通知时,volatile 会确保通知内容按正确顺序张贴(比如先写语文作业,再写数学作业),不会颠倒顺序,避免混乱。

7. wait 和 notify notifyAll

官方

由于线程之间是抢占式执行的, 因此线程之间执行的先后顺序难以预知.
但是实际开发中有时候我们希望合理的协调多个线程之间的执行先后顺序

功能

完成这个协调工作, 主要涉及到三个方法
wait() / wait(long timeout): 让当前线程进入等待状态.
notify() / notifyAll(): 唤醒在当前对象上等待的线程

7.1 wait()方法

概述

它们和 join 类似,作用是让线程按规定顺序执行。虽然计算机系统对多个线程的调度是随机的,但通过 wait 和 notify,能在应用层面 “干预” 线程顺序 —— 让后执行的线程主动等待,先让前面的线程完成任务。

自身理解
 

假设 “煮饭线程” 没完成时,“炒菜线程” 用 wait 等着(主动放弃先做的机会)。

什么情况下使用

就像排队买奶茶。你排到窗口时,发现想喝的奶茶原料没了(条件不具备),这时候你不会一直占着位置,而是先站到旁边等着(放弃竞争)。直到店员把原料准备好了(其他 “线程” 让条件满足),你再去排队窗口参与购买(解除阻塞,重新竞争)。

wait注意点

  1. wait 的两种等待方式
    • 无超时等待(死等):一直等,没结果就不罢休,像等公交,不设时间限制,一直等到车来。
    • 有超时等待:设定等待时间(单位毫秒),时间到了没人 “通知” 就不等了。比如等公交只等 30 分钟,30 分钟车不来就走。
    • 注意:死等是下策,程序设计里要像生活中 “容错” 一样,考虑灵活性,不能一直僵死等待。
  2. wait 和 notify 的 “配对” 规则
    • 必须通过同一个 “对象” 联系。比如借教室:
      • 用 A 教室登记等待(object1.wait ()),就得 A 教室的人发通知(object1.notify ())才能唤醒。如果用 B 教室发通知(object2.notify ()),A 教室等待的人不会被唤醒。
      • 若多个线程用同一个对象等待,notify 会随机唤醒其中一个,像多个同学用同一教室登记等待,老师喊一声(notify),随机唤醒一个同学。

代码1

package thread;

//wait的简单使用

public class ThreadDemo24 {

    public static void main(String[] args) throws InterruptedException {

        //随便拿个对象,都可以进行wait!!
        Object object = new Object();

        //直接调用wait,会出现非法监控异常
        //synchronized (object) {
        //    object.wait();
        //}

        synchronized (object) {
            System.out.println("wait之前");
            object.wait();
            System.out.println("wait之后");
        }

    }
}

注意点
wait的内部做了三件事


1.释放锁
2.进入阻塞等待
3.当其他线程调用notify的时候

报错原因

因为,你都没有锁,那有什么可以释放的呢?等同于,先拿到衣服再筛选适不适合!

解决方案
所以,wait 必须放到 synchronized里面 使用!!!
调用wait的对象,必须和synchronized中的锁对象是一致的!!

因此,wait解锁必然是解的object的锁

后续wait被唤醒之后,重新获取锁,当然还是获取到object的锁!!

其他的调用notify的线程,也是需要使用同样的对象

代码2

package thread;

public class ThreadDemo25 {

    public static void main(String[] args) {

        //需要有一个统一的对象进行加锁,wait,notify

        Object locker = new Object();

        Thread t1 = new Thread(() ->{
           synchronized (locker){
               System.out.println("t1 wait 之前");
               try {
                   //wait需要放在synchronized里面
                   locker.wait();
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
               System.out.println("t1 wait 之后");
           }
        });

        Thread t2 = new Thread(() ->{
            try {
                Thread.sleep(5000);

                //notify和一样wait都需要放在synchronized里面
                synchronized (locker) {
                    System.out.println("t2 notify 之前");
                
                    //呼喊/通知
                    locker.notify();
                    System.out.println("t2 notify 之后");
                }

            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });

        t1.start();
        t2.start();

    }
}
运行结果

运行流程

程序中两个线程(t1 和 t2)使用 wait 和 notify 时的执行过程:

  1. t1 先拿到 “锁”(类似拿到资源使用权),打印信息后进入 wait 状态,释放锁并等待;
  2. t2 等待一段时间后拿到锁,打印信息,用 notify 唤醒 t1;
  3. 但 t2 没释放锁时,t1 想重新拿锁会暂时 “卡住”(阻塞),直到 t2 释放锁,t1 才能继续执行。
自身理解

想象两个人用同一台 ATM 机:

  • 甲(t1):先走到 ATM 前(拿到锁),发现机器没钱,就站到旁边等着(释放位置,进入等待)。
  • 乙(t2):说 “我先处理点事,5 分钟后再来”(sleep 5000)。5 分钟后,乙走到 ATM 前(拿到锁),操作前喊一声:“钱已经存进去啦!”(notify,唤醒甲)。
  • 但乙还没离开 ATM 时(未释放锁),甲想回去用 ATM 会被挡住(阻塞)。直到乙用完离开(释放锁),甲才能重新用 ATM(继续执行)。

7.2 notify()方法

概述

与wait()方法一致

自身理解
等 “煮饭线程” 完成(比如饭煮好了,发出  notify 通知),“炒菜线程” 再开始炒菜。
这样就能保证 “先煮饭,再炒菜” 的顺序,虽然厨房(类比计算机系统)本来可能随机安排任务,但通过人为约定( wait 和  notify),让任务按计划顺序进行。

7.3 notifyAll()方法

官方

唤醒同一个对象上所有等待的线程。

自身理解

多个同学都用 A 教室登记等待,老师喊 “所有人来教室”(notifyAll),大家都被唤醒。但之后大家要重新 “抢” 教室(竞争锁),按顺序使用,谁先抢到不确定。因此,相比全部唤醒(notifyAll),单独唤醒(notify)更好控制。

 多线程的小结

8. 多线程案例

8.1 单例模式

官方

单例模式指一个类在程序运行(进程)中,只允许创建一个实例(对象)。

自身理解
 

想象班级的钥匙管理。教室门锁只需要一把钥匙(对应单例模式中的 “一个实例”)就能开门。如果每个同学都私自配一把钥匙(创建多个实例),不仅容易混乱(不知道谁该用钥匙),还浪费资源(钥匙多了没必要)。

单例模式就像制定规则:班级只允许存在一把官方钥匙,专人管理,避免多余钥匙出现,既规范又节省资源。

饿汉模式 - 单线程版

代码
package thread;

//多线程的例子 -- 单线程 -- “饿汉模式”
//就期望这个类只有一个实例(一个进程中)
class Singleton{
    //1.创建好这个实例,通过静态成员保持
    private static Singleton instance = new Singleton();

    //2.静态方法,获取当前的实例
    public static Singleton getInstance(){
        return instance;
    }

    //3.将构造方法进行私有化,外部不能进行 new
    private Singleton(){}

}

public class ThreadDemo26 {

    public static void main(String[] args) {
        //此时 new 就会报错
        //Singleton s = new Singleton();

        //只能使用规定的方法,而且这个不管写几次,都是获取同一个对象
        Singleton s =Singleton.getInstance();
        Singleton s2 =Singleton.getInstance();
        System.out.println(s == s2);
    }

}
代码解释
流程
  • 代码实现:类中定义一个静态属性(如 instance)存储唯一实例,同时将类的构造方法设为私有(private Singleton())。这样其他代码无法随意创建新实例,只能通过类提供的 getInstance 方法获取已经创建好的实例。
  • “饿汉” 含义:类在加载时就立刻创建实例,就像很 “迫切” 地早早完成实例创建,程序一启动实例就存在了。

自身理解
 

想象一家 24 小时便利店,店里只需要一个 “总收银系统” 来管理所有收款。

  • 饿汉模式实现:便利店一开门(程序启动,类加载),就立刻准备好唯一的 “总收银系统”(创建实例)。
  • 私有构造方法:把创建收银系统的方式设为 “内部专用”(私有构造方法),员工不能自己随便再弄一个收银系统(其他代码无法 new 新实例),只能使用已经准备好的那个收银系统(通过 getInstance 获取实例)。这样就保证整个便利店永远只有一套收银系统,避免混乱和资源浪费。

懒汉模式-单线程版

代码
package thread;

//就期望这个类只能有唯一的实例(一个进程中)
class SingletonLasy{

    //这个引用指向唯一实例,这个引用先初始化为 null,而不是立刻创建实例
    //(这个是懒汉和饿汉的区别,饿汉直接new)
    //private static SingletonLasy instance = null;
    private volatile static SingletonLasy instance = null;
    private static Object locker = new Object();

    public static SingletonLasy getInstance(){
        //为什么在最外层又套一层instance == null?
        //因为我第一次就创建过了,后续不想创建浪费时间空间 -- (需要的时候就加锁,不需要就不加)

        // 如果 instance 为 null,就说明是首次调用,首次调用就需要考虑安全问题,就要加锁
        // 如果非 null,就说明是后续的调用,就不必要加锁了
        if(instance == null){
            //加锁
            synchronized (locker) {
                if(instance == null) {
                    instance = new SingletonLasy();
                }
            }
        }
        return instance;
    }

    private SingletonLasy(){}

}
public class ThreadDemo27 {
    public static void main(String[] args) {

        //调用静态方法getInstance,不需要使用new的
        SingletonLasy s1 = SingletonLasy.getInstance();
        SingletonLasy s2 = SingletonLasy.getInstance();
        System.out.println(s1 == s2);
    }
}

代码解释
流程
  1. 类的定义:定义了一个名为 SingletonLasy 的类,这个类的目标是在整个进程中只有一个实例。
  2. 静态成员变量private volatile static SingletonLasy instance = null; 这行代码声明了一个静态的、私有的、volatile 修饰的 instance 变量,初始值为 nullvolatile 关键字的作用是保证变量在多线程环境下的可见性,避免指令重排序。
  3. 静态锁对象private static Object locker = new Object(); 声明了一个静态的、私有的锁对象 locker,用于在多线程环境下进行同步操作。
  4. 获取实例的方法getInstance() 方法是一个静态方法,用于获取 SingletonLasy 类的唯一实例。在方法内部,首先检查 instance 是否为 null,如果为 null,则进入同步块。在同步块内部,再次检查 instance 是否为 null,如果仍然为 null,则创建一个新的 SingletonLasy 实例并赋值给 instance。这样做的目的是为了避免多个线程同时创建实例,保证线程安全。
  5. 私有构造方法private SingletonLasy() 是一个私有构造方法,这意味着外部代码无法直接通过 new 关键字来创建 SingletonLasy 类的实例,只能通过 getInstance() 方法来获取实例。
  6. 主方法:在 main 方法中,通过调用 SingletonLasy.getInstance() 方法两次,分别获取两个实例 s1 和 s2,然后比较它们是否相等。由于单例模式保证了类只有一个实例,所以 s1 和 s2 应该是同一个对象,因此 s1 == s2 的结果为 true
自身理解

想象有一个非常珍贵的古董花瓶,全世界只有一个。这个花瓶存放在一个博物馆里,博物馆有一个专门的窗口来提供给游客参观这个花瓶。

 
  • 单例类:这个古董花瓶就相当于 SingletonLasy 类,它是独一无二的。
  • 静态成员变量:博物馆的工作人员知道花瓶的存放位置,这个位置信息就相当于 instance 变量,它指向唯一的花瓶。
  • 锁对象:博物馆的窗口有一个门,这个门就相当于 locker 锁对象。当有游客想要参观花瓶时,需要先通过这个门进入参观区域。
  • 获取实例的方法:游客想要参观花瓶,只能通过博物馆的窗口来申请参观。工作人员会先检查是否有其他游客正在参观(相当于检查 instance 是否为 null),如果没有,就打开门让游客进入参观区域(相当于加锁),然后再次确认花瓶是否还在那里(再次检查 instance 是否为 null),如果在,就允许游客参观(创建实例)。
  • 私有构造方法:这个古董花瓶是独一无二的,不能再复制一个新的花瓶,就像 SingletonLasy 类的私有构造方法一样,外部代码无法直接创建新的实例。

注意

8.2 阻塞队列

阻塞队列是什么

当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

一、阻塞队列:线程安全的 “智能排队系统”

文字总结

 
  • 特性
    1. 线程安全:多线程操作时,不会出现混乱(比如多个线程同时添加或获取元素,结果准确无误)。
    2. 阻塞特性
      • 队列满时:新元素想加入(入队列),得等着,直到队列有空位。
      • 队列空时:取元素(出队列)的操作也得等着,直到队列有元素。
  • 类比生活
    像医院的挂号窗口。如果挂号单堆满了(队列满),新患者挂号(入队列)得等;如果没挂号单了(队列空),护士处理挂号(出队列)也得等新患者来。

二、生产者消费者模型:分工协作的 “包饺子流水线”

文字总结

 
  • 角色分工
    • 生产者:生产数据并放入阻塞队列(如擀饺子皮)。
    • 消费者:从阻塞队列取出数据处理(如包饺子)。
  • 优势:让多线程分工明确,提升效率,避免混乱。
 

结合包饺子例子

 
  • 流程
    1. 和面(准备工作,单线程完成)。
    2. 擀饺子皮(生产者,多线程同时擀皮)。
    3. 包饺子(消费者,多线程同时包)。
  • 协作细节
    • 桌子充当阻塞队列:
      • 若擀皮太快,桌子堆满饺子皮(队列满),擀皮的人得停下等。
      • 若包饺子太快,桌子没饺子皮了(队列空),包饺子的人得停下等。

1.解耦合

概念总结
解耦合是让两个关联的部分,通过中间层联系,减少直接依赖。就像两个人通过翻译交流,翻译就是中间层。即使一方说话方式变了,另一方也能通过翻译理解,系统更灵活稳定。

 

生活例子

 
  • 快递场景(对应第二张图):
    你(A)在网上买东西,商家(B)发货。如果没有快递站(中间层),你和商家得直接对接物流,依赖很强。有了快递站后,你填地址给商家,商家把货发往快递站,快递站再配送。你和商家不用管物流细节,依赖降低。就算快递站换了配送路线,对你和商家影响也小。
  • 外卖场景
    你(A)点外卖,餐厅(B)做菜。外卖平台是中间层。你通过平台下单,餐厅通过平台接单。即使餐厅换了菜单,或你换了口味,双方通过平台对接,不会直接互相干扰,这就是解耦合。

2.削峰填谷

概念总结
削峰填谷是一种 “调节波动” 的机制。当资源(如水、数据、任务等)短时间内大量涌入(高峰),或突然减少(低谷)时,通过中间 “存储缓冲环节” 平衡波动。高峰时存储多余资源,低谷时释放资源,让整体使用更平稳,避免高峰冲击或低谷浪费。

 

生活例子

 
  • 水库场景(对应第一张图):
    三峡大坝就像 “削峰填谷小能手”。上游雨量大,水来得又快又急(高峰),大坝关闸蓄水,把水暂时存起来;上游雨量少,水不够用(低谷),大坝开闸放水。这样下游的水流量始终稳定,既不会被急流冲垮,也不会没水用。
  • 用电场景
    白天工厂、家庭用电多(用电高峰),晚上用电少(用电低谷)。电力公司会在低谷时,用多余电力把水抽到高处水库;高峰时,放水库的水发电。这就是用电的 “削峰填谷”,平衡电力供需。

  • 削峰填谷:像水库调节水量、电力调节供需,核心是平衡资源波动。
  • 解耦合:像快递站、外卖平台,核心是减少模块间直接依赖,让系统更灵活。

三、解耦:分布式系统的 “快递中转站”

文字总结

 
  • 解耦作用:降低模块间依赖。如分布式系统中,服务器通过阻塞队列协作,不直接交互。
  • 好处
    • 某服务器故障,对其他服务器影响小。
    • 新增服务器时,原有代码几乎不用改。
 

类比快递分拣

 
  • 快递站是 “阻塞队列”,收件点(生产者)把快递放快递站,派件点(消费者)从快递站取件。
  • 收件点和派件点不直接联系,通过快递站中转。即使新增收件点或派件点,对现有流程影响很小。

标准库中的阻塞队列

文字总结
  1. 阻塞队列基础概念
    Java 标准库中的 BlockingQueue 是一个 “智能排队系统” 接口,有多种实现类:
 
  • ArrayBlockingQueue:像固定容量的杯子,用数组实现,容量固定。
  • LinkedBlockingQueue:像可延伸的链条,用链表实现,容量可灵活变化(也可设置固定容量)。
  • PriorityBlockingQueue:像 “VIP 排队通道”,元素按优先级排队,优先级高的先处理。
 
  1. 核心方法特点
 
  • put 方法:往队列加东西时,若队列满了,就 “卡住不动”(阻塞),直到队列有空位。
  • take 方法:从队列取东西时,若队列空了,也会 “卡住不动”(阻塞),直到队列有东西。
  • offer 方法:和 put 都是 “排队” 操作,但队列满时,offer 不卡住,直接告诉你 “没加上”(返回结果)。
 
  1. 学习知识的层次
 
  • 会用:像会用手机拍照功能,知道按哪个按钮。
  • 懂原理:像明白手机拍照如何调用摄像头、处理图像。
  • 能实现:像自己设计一个拍照功能,从无到有做出来。对阻塞队列来说,“能实现” 就是自己写出类似功能的代码。

自身理解
  1. put 方法:食堂打饭排队
    食堂窗口排队打饭,窗口最多容纳 10 人(队列满)。第 11 个人来打饭时,就得在窗口外等着(阻塞),直到有人打完饭离开(队列有空位),才能进去打饭。这就是 put 的阻塞效果。

  2. take 方法:图书馆借热门书
    图书馆某本热门书被借完了(队列空)。这时你去借这本书,就得等着(阻塞),直到有人还书(队列有元素),你才能借到书。这就是 take 的阻塞效果。

  3. offer 方法:便利店自助结账
    便利店的自助结账机最多同时容纳 5 人使用。第 6 个人想用自助结账时,机器直接提示 “已满,请去人工窗口”(返回结果),不会让他等着。这就是 offer 不阻塞的特点。

一、技术内容总结
  1. 加锁与阻塞逻辑

    • 加锁位置:在操作队列(入队、出队)时,需通过 synchronized 加锁,确保多线程操作队列的安全性。例如在 put 方法中,对 locker 加锁,保证同一时间只有一个线程能执行入队操作。
    • 阻塞与唤醒
      • 当队列满时,入队线程需阻塞(用 locker.wait());当其他线程出队成功(队列不满),需唤醒阻塞的入队线程(用 locker.notify())。
      • 同理,队列空时,出队线程阻塞;入队成功后唤醒出队线程。
    • if 改 while 的原因
      • 最初用 if 判断队列满 / 空,只检查一次条件。但线程被唤醒时,队列状态可能已变化(比如多个线程等待)。
      • 改用 while 后,每次唤醒都重新检查条件。若队列仍满 / 空,继续阻塞,避免 “误唤醒” 导致的逻辑错误。
  2. 多线程编程的难点

    • 多线程执行顺序随机,需确保各种执行顺序下程序结果都正确。例如,两个线程同时操作队列,需通过锁、阻塞、唤醒机制,保证数据一致性。
二、生活例子类比
  1. 排队买奶茶(阻塞与唤醒)

    • 场景:奶茶店排队,柜台最多容纳 10 人(队列容量)。
    • 阻塞:第 11 个人来买奶茶(队列满),需在门口等待(wait() 阻塞)。
    • 唤醒:当有人买完奶茶离开(出队,队列不满),店员喊 “下一位”(notify() 唤醒),等待的人才能进店买奶茶。
  2. 闹钟与起床(if 改 while 的意义)

    • if 的问题:你定了 7:30 的闹钟(if 只判断一次),但可能 6:30 就醒了,若不看时间直接起床,可能太早。
    • while 的优势:每次醒来(被唤醒)都先看时间(重新判断队列条件)。若没到 7:30(队列仍满 / 空),继续睡(继续阻塞);若到时间(队列条件满足),再起床(执行操作)。
 

通过这些例子可以理解:

 
  • 多线程操作队列时,锁是 “排队规则”,保证秩序;阻塞与唤醒是 “等待和叫号”,协调线程;while 是更严谨的 “条件检查”,避免意外情况。即使没学过编程,也能通过生活场景掌握这些核心逻辑。

阻塞队列实现

package thread;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

//阻塞队列

public class ThreadDemo28 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);
        //进入,为什么不使用offer,因为put可以堵塞
        queue.put("aaa");

        //有元素的时候就进行弹出元素
        String elem = queue.take();
        System.out.println("elem = " + elem);

        //没有元素的时候就进行堵塞
        elem = queue.take();
        System.out.println("elem = " + elem);

    }
}

//-------
package thread;

//阻塞队列

//为了简单,不写泛型的形式,考虑是单纯的String
class MyBlockingQueue{

    private int head = 0;
    private int tail = 0;

    private String elems[] = null;
    private int size = 0;

    //准备锁对象,如果使用 this 也可以
    //这里创建锁对象是为了更好的展示
    private Object locker = new Object();

    public MyBlockingQueue(int capacity){
        elems = new String[capacity];
    }

    public void put(String elem) throws InterruptedException {

        // 锁加到这里和加到方法上本质是一样的,加到方法上面是给 this 加锁
        // 此处是给 locker 加锁
        synchronized (locker) {
            //if只判定一次条件,一旦程序进入阻塞之后,再被唤醒.这中间隔的时间,这个过程中会有很多的变数!
            //就难以保证,你的条件是否仍然满足入队列的条件是否具备
            //所以将 if 修改为 which
            while (size >= elems.length) {
                //队列满,后续也需要这个代码阻塞
                //那么就可以考虑使用wait,因为不知道要休眠多少时间
                //所以不考虑使用sleep
                //1.
                locker.wait();
            }

            //新的元素放在 tail 指向的位置上
            elems[tail] = elem;
            tail++;
            if (tail >= elems.length) {
                tail = 0;
            }
            size++;

            //2.
            locker.notify();

        }
    }

    public String take() throws InterruptedException {

        String elem = null;

        //引入锁确保线程安全
        synchronized (locker) {
            while (size == 0) {
                //队列空了,后续也需要这个代码阻塞

                //2.
                locker.wait();
            }

            elem = elems[head];
            head++;
            if (head >= elems.length) {
                head = 0;
            }
            size--;

            // 元素出队列成功后,加上锁唤醒
            //有睡眠就要有唤醒
            //1.
            locker.notify();

        }
        return elem;
    }

}

public class ThreadDemo29 {
    public static void main(String[] args) {
        MyBlockingQueue queue = new MyBlockingQueue(1000);
//        queue.put("aaa");
//        queue.put("bbb");
//        queue.put("ccc");
//        queue.put("ddd");
//
//        String elem = "";
//        elem = queue.take();
//        System.out.println("elem: " + elem);
//        elem = queue.take();
//        System.out.println("elem: " + elem);
//        elem = queue.take();
//        System.out.println("elem: " + elem);
//        elem = queue.take();
//        System.out.println("elem: " + elem);

        //此时就来模拟使用队列阻塞来实现,生产者,消费者
        //生产者
        Thread t1 = new Thread(() ->{
            int n = 1;
            while (true){
                try {
                    queue.put(n + "");
                    System.out.println("生产元素 " + n);
                    n++;

                    //这里加上睡眠时间,造成消费者消费 > 生产者生产
                    //从而形成消费者等待生产者
                    //同理放在消费者睡眠,生产者就会 > 消费者
                    Thread.sleep(500);

                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        //消费者
        Thread t2 = new Thread(() ->{
            while (true){
                try {
                    String n = queue.take();
                    System.out.println("消费元素 " + n);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });

        t1.start();
        t2.start();
    }
}

8.3 定时器

定时器是什么

类似于⼀个 "闹钟". 达到⼀个设定的时间之后, 就执行某个指定好的代码。

标准库中的定时器

·标准库中提供了⼀个 Timer 类. Timer 类的核心方法为 schedule .
·schedule 包含两个参数. 第⼀个参数指定即将要执⾏的任务代码, 第⼆个参数指定多⻓时间之后
执行 (单位为毫秒).
Timer timer = new Timer();
timer.schedule(new TimerTask() {
 @Override
 public void run() {
 System.out.println("hello");
 }
}, 3000);

实现定时器

一个带优先级队列(不要使用 PriorityBlockingQueue, 容易死锁!)
队列中的每个元素是一个 Task 对象.
Task 中带有一个时间属性, 队⾸元素就是即将要执行的任务
同时有一个 worker 线程一直扫描队⾸元素, 看队首元素是否需要执行

一、技术内容总结
1. Timer(定时器)的核心组成
  • 计时线程:相当于一个 “时间管理员”,专门盯着时间,到点就触发任务执行。
  • 任务队列:用来存放所有 “预约” 的任务。这些任务带着 “延迟时间”,比如 “3 秒后执行”“5 分钟后执行”。
  • 优先级队列:任务按执行时间排序,时间短的任务优先处理。就像排队时,赶时间的人优先办事,避免无意义的扫描全部任务,提升效率。
2. Java 标准库中的 Timer 使用
  • 创建 TimerTimer timer = new Timer(); 相当于买了一个 “定时闹钟”。
  • 添加任务:用 schedule 方法安排任务,比如:
timer.schedule(new TimerTask() {  
    @Override  
    public void run() {  
        System.out.println("hello timer");  
    }  
}, 3000); // 3000 毫秒后执行任务  
  • 这就像给闹钟设置 “提醒事项”,时间一到,任务(如打印文字)就会执行。
  • 结束 Timer:用 timer.cancel(); 主动关闭定时器,否则它会一直运行(因为内置的是 “前台线程”,会阻止程序结束)。

二、自身理解
1. Timer 组成类比
  • 计时线程:类似餐厅的 “叫号员”,时刻盯着时间,到点就喊顾客用餐。
  • 任务队列:像餐厅的 “排队系统”,记录每个顾客的预约信息(比如 “10 分钟后用餐”)。
  • 优先级队列:好比 “加急通道”,赶时间的顾客(任务)优先安排,叫号员不用挨个查所有人,只看最急的顾客即可,节省时间。
2. 标准库 Timer 使用类比
  • 创建 Timer:相当于买一个 “智能提醒器”。
  • 添加任务:比如用提醒器设置 “3 分钟后提醒烧水”,到时间提醒器就会响(执行任务)。
  • 结束 Timer:用完提醒器后关掉它,不然它会一直耗电(持续运行,阻止程序结束)。
 

通过这些类比可以理解:Timer 就是一个 “任务闹钟”,按约定时间执行任务,而背后的线程、队列等机制,就像现实中协调任务的 “管理员” 和 “排队系统”,让一切有序进行。

代码
package thread;

import java.util.PriorityQueue;

//通过这个类,来描述一个任务

//Comparable 接口存在的核心意义,就是强制要求实现类提供 compareTo 方法
class MyTimerTask implements Comparable<MyTimerTask>{
    //在什么时间来执行这个任务
    //此处约定这个 time 是一个 ms 级别的时间戳
    private long time;
    //实际任务要执行的代码
    private Runnable runnable;

    public long getTime(){
        return time;
    }

    // delay 期望是一个 “相对时间”
    public MyTimerTask(Runnable runnable,long delay){
        this.runnable = runnable;
        //计算一下真正要执行任务的绝对时间,(使用绝对时间,方便判定任务是否到达时间的)
        //                   获取当前系统时间戳     延迟的毫秒数
        this.time = System.currentTimeMillis() + delay;
    }

    public void run(){
        runnable.run();
    }

    @Override
    public int compareTo(MyTimerTask o) {
        //是写法1 return (int) (this.time - o.time);
        //还是写法2 return (int) (o.time - this.time);
        //答案是写出来试试就知道了
        return (int) (this.time - o.time);
    }
}


// 通过这个类,来表示一个定时器
class MyTimer{
    //负责扫描任务队列,执行任务的线程
    private Thread t = null;

    //任务队列
    private PriorityQueue<MyTimerTask> queue = new PriorityQueue<>();

    //搞个锁对象,此处使用 this 也可以
    // 示例:假设后续有需要加锁的逻辑
//    public void someSynchronizedMethod() {
//        synchronized (this) { // 直接用 this 作为锁
//            // 同步代码块
//        }
//    }
    private Object locker = new Object();


    public void schedule(Runnable runnable,long delay){

        //加锁
        synchronized (locker){
            MyTimerTask task = new MyTimerTask(runnable, delay);
            queue.offer(task);

            //添加新的元素之后,就可以唤醒扫描的线程的 wait 了(不等于null的时候唤醒)
            locker.notify();
        }
    }

    public void cancel(){
        // 主动结束 t 线程,因为扫描线程创建的是前台进程,不会主动结束
        //interrupt
        //if (t != null) {
        //  t.interrupt();
        //}
        //然后呢在下面的while里面写成!Thread.currentThread().isInterrupted()
    }

    //构造方法,创建扫描线程,让扫描线程完成判定和执行
    public MyTimer(){

        //第一个分支(构造函数)
        //创建扫描线程 t -------------------

        t = new Thread(() ->{
            //扫描线程就需要循环反复的扫描队首元素,然后判定队首元素是不是时间到了
            //如果时间没到,啥都不干
            //如果时间到了,就执行这个任务并且把这个任务从队列中删掉

            //扫描的逻辑 -------------------

            while (true){
                try {
                    //如果锁加在while外面,就会导致while释放不辽,在 schedule卡住

                    synchronized (locker) {
                        while (queue.isEmpty()) {
                            //暂时先不做处理
                            //continue;

                            //需要的时候才判断,不要的时候就不要判断
                            //null的时候去循环没有意义,停下来还能节省空间
                            //加上waity以及通知
                            locker.wait();
                        }
                        MyTimerTask task = queue.peek();
                        //获取当前的时间
                        long curTime = System.currentTimeMillis();
                        if (curTime >= task.getTime()) {
                            //当前时间已经达到了任务时间,就可以执行任务了
                            queue.poll();
                            task.run();
                        } else {
                            //当前时间没有达到了任务时间,暂时先不执行
                            //不能使用sleep,会错过新的任务,也无法释放锁
                            //Thread.sleep(task.getTime() - curTime);
                            locker.wait(task.getTime() - curTime);
                        }
                    }
                }catch (InterruptedException e){
                    e.printStackTrace();
                }
            }
        });

        //要记得star!!! 启动扫描线程 t----------
        t.start();
    }
}

public class ThreadDemo31 {

    public static void main(String[] args) {

        //总起点
        MyTimer timer = new MyTimer();

        //第二个分支(添加任务)
        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 3000");
            }
        },3000);

        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 2000");
            }
        },2000);

        timer.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello 1000");
            }
        },1000);
        System.out.println("hello main");
    }
}

流程图

代码解释

规定了main,class MyTimer(定时器),class MyTimerTask(定时器任务)三个部分

main:创建MyTimer,并添加任务

class MyTimer:构建基本条件,创建线程 t 及其功能,启动线程 t

class MyTimerTask:执行任务的逻辑

main部分

class MyTimer部分

class MyTimerTask部分

代码途中的问题(重点)
1.怎么计算时间定时

时间戳+ delay(相对延迟时间)= 最终得到任务执行的绝对时间

2.任务时间和当前时间的差

3.为什么要加锁

4.加锁的位置

5.加完锁后,所以地方都锁吗

6.等待使用wait还是sleep

在sleep过程中,又有别的线程调用了schedule,安排新的任务,新的任务时间是13:30,就不能进行操作;另外,sleep也不能解锁。

1)wait的过程中,有新的任务来了,wait就会被唤醒!schedule有notify的
根据新的任务重新计算要等待的时间了,你也不知道新的任务是不是最早的任务!!
2)wait过程中,没有新的任务,时间到了.按照原定计划,执行之前的这个最早的任务即可

7.代码执行流程是怎么样

8.4 线程池

线程池是什么

线程池是一种 多线程处理机制,通过维护一组 工作线程(Worker Threads),动态分配这些线程来执行异步任务或回调。它通过复用线程资源,避免频繁创建和销毁线程的开销,从而提升系统性能和稳定性。

标准库中的线程池

1. corePoolSize(核心线程数)

含义:线程池中始终保留的 “常驻线程” 数量,即使这些线程暂时没任务,也不会被销毁。
生活实例:像餐厅里 “固定雇佣的全职服务员”。无论餐厅是忙是闲,这些服务员始终在岗,随时准备接待顾客。

2. maximumPoolSize(最大线程数)

含义:线程池能容纳的线程最大数量,即核心线程 + 临时扩展线程的总和。
生活实例:餐厅里 “全职服务员 + 兼职服务员” 的总人数上限。比如全职有 5 人,最多还能招 3 个兼职,那最大线程数就是 8。

3. keepAliveTime(存活时间)

含义:当线程数超过核心线程数时,多余的临时线程在空闲状态下能保留的最长时间,超时后会被销毁。
生活实例:餐厅里 “兼职服务员的空闲等待时间”。比如兼职服务员空闲超过 1 小时没活干,就可以下班(销毁)。

4. unit(时间单位)

含义:给 keepAliveTime 定义时间单位,比如秒、分钟、小时等。
生活实例:给 “兼职空闲等待时间” 定规则,是 “1 小时” 还是 “30 分钟”,这里的 “小时”“分钟” 就是时间单位。

5. workQueue(任务队列)

含义:存储等待处理任务的队列。当核心线程都在忙时,新任务会先进入队列排队。
生活实例:餐厅的 “顾客候餐区”。当所有服务员都在服务顾客时,新到的顾客需要先在候餐区排队,等有空余服务员再接待。

6. threadFactory(线程工厂)

含义:创建线程的 “模板”,定义线程如何创建、命名、设置属性等。
生活实例:餐厅的 “招聘渠道”。比如通过中介招服务员(统一培训、统一工服),这里的 “中介” 就像线程工厂,确保每个新线程(服务员)有规范的创建方式。

7. handler(拒绝策略)

含义:当任务队列满了,且线程数已达到最大线程数时,对新任务的处理策略。
生活实例:餐厅的 “满客处理方式”。比如当候餐区满员、服务员也全在忙时,新顾客来了是直接拒绝(抛异常),还是劝其下次再来(丢弃任务),这就是拒绝策略。

拒绝的4个方法/策略
1. AbortPolicy(抛异常拒绝)

含义:直接拒绝新任务,还会抛出异常(RejectedExecutionException),就像明确说 “不伺候了”。
生活实例
餐厅已满员(队列满、服务员全忙),这时新顾客非要进店,店员直接拒绝:“没位置了!”,顾客一听急了(抛异常),大吵大闹,最后新顾客没进店,店里也被闹得鸡飞狗跳(任务没处理,还引发问题)。


2. CallerRunsPolicy(提交者自己处理)

含义:新任务不交给线程池,让 “提交任务的线程” 自己执行。相当于 “你催我,那就自己干”。
生活实例
快递站爆仓(快递太多,员工忙不过来),你去寄快递(提交任务),快递员说:“太忙了,你自己把快递送去目的地吧!” 于是你只能自己跑腿送快递(提交任务的线程自己处理任务)。


3. DiscardOldestPolicy(丢弃最旧任务)

含义:扔掉队列里最久的老任务,腾出位置给新任务。类似 “喜新厌旧”。
生活实例
游乐园排队玩项目(任务队列),队伍已满。这时来了个 VIP 游客(新任务),工作人员直接把排队最久的游客(最旧任务)拉出来,让 VIP 游客顶替位置,说:“你下次再来吧,这位先玩!”


4. DiscardPolicy(默默丢弃新任务)

含义:安静地扔掉新任务,不处理也不提醒,像 “当没看见”。
生活实例
超市搞促销,免费发鸡蛋,排队的人太多(队列满)。后来的人还想领鸡蛋(新任务),工作人员直接无视,既不解释也不处理,后来的人只能白跑一趟(新任务被丢弃)。

实现线程池

方法

代码
package thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

//简单创建线程池的写法

public class ThreadDemo32 {

    public static void main(String[] args) {
        //创建线程池 -- 得到一个数量为 4 个线程的线程池
        ExecutorService service = Executors.newFixedThreadPool(4);

        //往线程池插入数据
        service.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        });

    }
}


//------------------

package thread;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

class MyThreadPoolExecutor{

    //将创建的线程都放入 threadList 链表进行保存
    private List<Thread> threadList = new ArrayList<>();

    //用来保存任务的队列
    private BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(1000);

    //通过 n 指定创建多少个线程
    public MyThreadPoolExecutor(int n){
        for (int i = 0; i < n; i++) {
            Thread t = new Thread(() ->{
                //线程池中的核心代码部分
               // 线程要做的事情就是把任务队列中的任务不停的取出来,并且进行执行
                while (true){
                    //取元素
                    try {
                        //任何实现了 Runnable 接口的类 或者 使用 Lambda 表达式表示的 Runnable 对象
                        //都代表着一个可以被线程执行的任务
                        //在你的线程池代码里,BlockingQueue<Runnable> queue 是用来存储待执行任务的队列。
                        //当线程池中的线程从队列中取出任务时,这些任务是以 Runnable 对象的形式存在的,所以使用 Runnable 类型的变量来接收。
                        //(使用 Runnable 来 封装 任务并让线程执行)

                        //此处的 take 带有阻塞功能的
                        //如果队列为 空 ,此处 take 就会进行阻塞
                        Runnable runnable = queue.take();

                        //取出一个任务即可
                        runnable.run();
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            });
            t.start();
            threadList.add(t);
        }
    }

    //Runnable 接口的核心用途就是定义一个可执行的任务
    //Runnable是拿来重写run,来进行我需要进行的操作的
    //就相当于,我需要定制操作的东西,就要使用run
    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

}


public class ThreadDemo33 {
    public static void main(String[] args) throws InterruptedException {

        //规定了4个线程 + 1000个任务
        MyThreadPoolExecutor executor = new MyThreadPoolExecutor(4);
        for (int i = 0; i < 1000; i++) {

            //变量捕获,设置为final
            //匿名内部类访问了外部循环中非final 且非effectively final"的变量
            //这违反了Java语法规则

            int n = i;


            executor.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("执行任务:" + n +",当前线程为:" + Thread.currentThread().getName());
                }
            });
        }
    }
}

多线程总结

扩展问题

一、基础概念类

  1. 线程和进程的区别是什么?

    • 答案:线程是进程中的一个执行单元,共享所属进程的资源,而进程是系统分配资源的最小单位,拥有独立的内存空间等资源。线程的创建和销毁开销较小,而进程的开销较大。线程之间相互依赖,一个线程崩溃可能影响整个进程,而进程之间相互独立。

    • 生活例子:线程就像一个餐厅里的服务员,他们共享餐厅的厨房、餐具等资源(进程)。多个服务员(线程)可以同时为不同的顾客服务,但如果一个服务员犯了错误(线程崩溃),可能会影响整个餐厅的运营(进程崩溃)。而不同的餐厅(进程)之间是相互独立的,一个餐厅出问题不会影响另一个餐厅。

  2. 线程的生命周期有哪些状态?

    • 答案:线程的生命周期包括新建、就绪、运行、阻塞、等待、超时等待和终止等状态。这些状态反映了线程从创建到执行完毕的整个过程。

    • 生活例子:线程的生命周期就像一个人的一天。新建状态就像刚起床但还没开始活动,就绪状态就像准备好开始一天的工作,运行状态就像在工作,阻塞状态就像在等待某个任务完成(比如等电梯),等待和超时等待状态就像在休息或等待特定时间,终止状态就像一天的工作结束,进入休息时间。

  3. 如何创建线程?有哪些方式?

    • 答案:创建线程的方式包括继承Thread类、实现Runnable接口、使用匿名内部类和Lambda表达式等。

    • 生活例子:创建线程就像安排任务给不同的人。继承Thread类就像让一个人专门负责某种任务,实现Runnable接口就像让不同的人按照统一的任务规范去执行,匿名内部类和Lambda表达式就像临时安排任务给某个人,不需要提前定义好任务执行者。

  4. 线程的常用方法有哪些?

    • 答案:线程的常用方法包括start()run()sleep(long millis)yield()join()isAlive()getName()setName(String name)等。

    • 生活例子start()就像告诉某个人开始工作,run()就是工作的具体内容,sleep(long millis)就像让某个人休息一段时间,yield()就像让某个人暂停一下让别人先做,join()就像等待某个人完成工作后再继续,isAlive()就像检查某个人是否还在工作,getName()setName(String name)就像获取和设置某个人的名字。

二、Thread类

1.start和run有什么区别?

  • start():是线程启动的 “入口操作”。调用 start() 时,JVM 会为线程分配资源,使线程进入就绪队列,等待 CPU 调度。调度成功后,线程会自动执行关联的 run() 方法,真正实现多线程的并发执行。
  • run():是线程业务逻辑的 “载体”,用于封装线程要执行的具体任务(如数据计算、文件读取等)。若直接调用 run(),它只是作为普通方法在当前线程中顺序执行;只有通过 start() 启动线程,run() 中的代码才会在新线程中独立运行,这也是多线程并发的核心机制。

简单总结:start() 负责 “启动线程,让线程参与 JVM 调度”;run() 负责 “定义线程要做的事”,二者配合实现多线程功能。

2.如何启动更多线程?

//要想启动更多线程,就是得创建新的对象!!
public class ThreadDemo10 {

    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{
            System.out.println("hello1");
        });
        Thread t2 = new Thread(() ->{
            System.out.println("hello2");
        });
        t1.start();
        t2.start();
    }

}

3.怎么终止线程?

核心也就是让run方法能够提前就结束=>非常取决于具体代码实现方式了.

方法一:要给标志位上加 volatile 关键字
package thread;

//终止线程--添加全局变量isQuit来控制

//1.能否将其变成局部变量isQuit?
//2.isQuit = true;和printf("让t线程结束")谁先写?


public class ThreadDemo12 {
    private static boolean isQuit = false;

    public static void main(String[] args) {

        //1.

        //当我将全局变量变成局部变量的时候,就需要加上 final
        //因为lamba表达式的变量捕获
        //boolean isQuit = false;

        //当我们加上final,外部的修改又不能运行
        //因为被 final 修饰的局部变量,在初始化后不能再次赋值
        //final boolean isQuit = false;

       Thread t = new Thread(() ->{
           while (!isQuit){
               System.out.println("我是一个线程,工作中!");
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }
           }
           System.out.println("线程工作完毕!");
       });
       t.start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        //System.out.println("让t线程结束");
        //isQuit = true;

        //2.

        //如果是修改成这样的话,main先打印还是t结束先打印就不知道了
        isQuit = true;
        System.out.println("让t线程结束");

    }
}

方法二:使用Thread.interrupted() 或者 Thread.currentThread().isInterrupted() 代替自定义标志位.

进行提前唤醒的操作

package thread;

//对12的优化写法
public class ThreadDemo13 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{

            while (!Thread.currentThread().isInterrupted()){
                System.out.println("我是一个线程,正在工作中!");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    //throw new RuntimeException(e);

                    //e.printStackTrace();
                    break;
                }
            }
            System.out.println("线程执行完毕");
        });

        t.start();

        Thread.sleep(3000);
        //使用 interrupt 方法,先修改刚才标志位的值
        //修改为true
        System.out.println("让 t 线程结束");
        t.interrupt();
    }

}

4.catch写法不同,为什么输出结果不同?

5.等待线程的执行顺序是怎么样的?

package thread;

import java.util.Random;

public class ThreadDemo14 {

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() ->{
            Random random = new Random();
            int n = random.nextInt();
            for (int i = 0; i < n; i++) {
                System.out.println("我是一个线程,正在工作中...");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("线程执行结束");
        });

        t.start();

        //这个操作就是线程等待
        //让 main线程 等 t线程结束
        t.join();

        System.out.println("这是主线程,期望这个日志在 t 结束后打印");

    }

}

三、线程安全(重点)

1.为什么要学习线程安全?

2.怎么利用 synchronized 来加锁?

 利用关键字来进行加锁操作

3.死锁是啥?怎么解决

官方

加锁是能解决线程安全问题,但是如果加锁方式不当,就可能产生死锁!!

死锁是多线程编程里会出现的一个问题。简单来说,当两个或多个线程都在等待对方释放锁,从而没办法继续执行下去时,就出现了死锁。

自身理解

小明和小红一起吃饺子,小明手里拿着酱油,小红手里拿着醋 。小明想要醋,小红想要酱油,但两人都不愿先把自己手里的调料给对方,都等着对方先给,结果谁都没法按自己的意愿吃饺子,这就如同编程里的死锁,两个线程都在等对方释放资源,导致程序无法继续运行 。

发生死锁的必要条件
  1. 互斥使用:锁就像单人马桶,一个人用着,其他人必须等,不能一起用。
  2. 不可抢占:好比借了别人的书,只有借书人主动还书,别人不能直接抢过来。锁也是,拿到锁的线程不主动释放,别人抢不走。
  3. 请求保持:比如你左手拿了苹果,还想右手拿香蕉,不放下苹果就去拿香蕉。线程拿了一个锁 A,还想拿另一个锁 B,且不放下 A。
  4. 循环等待:两人互相等着对方给东西。比如 A 等 B 的锁,B 等 A 的锁,形成 “死循环”。

解决死锁的核心:破坏这四个条件中的任意一个,死锁就不会发生啦!

典型死锁场景

1.一个线程,一把锁。(这个锁没有被synchronized修饰,不具备可以重复锁的功能)

2.两个线程,两把锁。线程1获取到锁A,线程2获取到锁B,接下来,1尝试获取B,2尝试获取A。

3.N个线程M把锁

把场景想象成一场大型派对,有 N 位客人(代表 N 个线程),派对上有 M 个热门游戏项目(代表 M 把锁),每个游戏同一时间只能容纳一人参与。

客人们各自先选择了一个游戏开始玩,之后他们又想去玩其他客人正在玩的游戏。但大家都不愿意先结束自己正在玩的游戏,于是所有人都卡在那里,既不能继续玩新游戏,也不愿意放弃当前游戏,派对的欢乐节奏就停滞了,对应到编程里就是出现了死锁。

破坏死锁的简单方法
  1. 避免 “请求保持”

    • 例子:吃饺子时,一次拿够醋、酱油等所有需要的调料,别拿了醋又去拿酱油。
    • 编程:线程获取锁时,一次性拿完所有需要的锁,不分开拿。
  2. 打破 “循环等待”

    • 例子:给调料定顺序(如先拿醋,再拿酱油),大家都按这顺序拿,就不会出现 “你等我、我等你” 的循环。
    • 编程:给锁编号,强制线程按固定顺序获取锁,避免循环等待。
  3. 允许 “锁被抢占”

    • 例子:设定拿调料的 “限时”,超时还没拿到全部调料,就放下已拿的,让别人先拿。
    • 编程:给锁设置获取超时机制,若超时,线程主动释放已持有的锁,让其他线程有机会获取。

死锁代码 + 解决死锁的代码
package thread;

public class ThreadDemo22 {

    public static void main(String[] args) {

        Object A = new Object();
        Object B = new Object();

        Thread t1 = new Thread(() ->{
           synchronized (A){
               //sleep一下,给 t2 也能拿到 B
               try {
                   Thread.sleep(1000);
               } catch (InterruptedException e) {
                   throw new RuntimeException(e);
               }

               //尝试获取 B,并没有释放 A
               synchronized (B){
                   System.out.println("t1 拿到了两把锁");
               }
           }
        });

        Thread t2 = new Thread(() ->{
            //B -> A  解决加锁的问题
            //如果保持上面是 B,下面是 A,跟t1一样就会导致 死锁 !!!
            synchronized (A){
                //sleep一下,给 t1 也能拿到 A
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

                //尝试获取 A,并没有释放 B

                //A -> B 解决加锁的问题
                synchronized (B){
                    System.out.println("t2 拿到了两把锁");
                }
            }
        });

        t1.start();
        t2.start();

    }
}

4.内存可见性引起的线程安全问题

概念

如果一个线程写,一个线程读,这个时候是否会有线程安全问题呢??
也是可能存在的!!

例子

当我们实际输入非0的值的时候
发现t1并没有真的结束!!!
因此当下这个情况也是 bug!!!

官方解释
  1. load 操作结果不变:每次执行 load(类似 “获取数据”),结果都一样。比如等用户输入时,程序在几秒内循环执行了上百亿次,可这期间数据根本没变化。
  2. load 操作开销大:load 操作比 “条件跳转”(判断下一步做什么)更耗资源,因为访问寄存器(高速存储区)比访问内存快很多,频繁用高消耗的 load 很不划算。

自身解释

想象你等烧水,水壶要 5 分钟才能开。但你每隔 1 秒就凑过去看 “水开了没”(这就是 load 操作),每次看水都没开(结果不变)。而且 “凑过去看” 这个动作(load 操作)比单纯在心里判断 “水开没开”(条件跳转)更麻烦、更费精力。这就像文段里说的:频繁做高消耗的 load 操作,结果还不变,纯属浪费。

解决代码
package thread;

import java.util.Scanner;

public class ThreadDemo23 {

    //1.2
    //private static int flag = 0;

    //3.增加volatile关键字
    //强制读写内存. 速度是慢了, 但是数据变的更准确了
    
    private volatile static int flag = 0;

    public static void main(String[] args) {
        Thread t1 = new Thread(() ->{
            //1.吃了 JVM 的优化配置 -- 一直运行
//           while (flag == 0){
//               //循环啥的不写
//           }

            //2.不吃,因为睡眠了 -- 能出结果
//            while (flag == 0){
//                try {
//                    Thread.sleep(10);
//                } catch (InterruptedException e) {
//                    throw new RuntimeException(e);
//                }
//            }

            while (flag == 0){
              //循环啥的不写
           }

            System.out.println("t1  线程结束!");
        });

        Thread t2 = new Thread(() ->{
            System.out.println("请输入 flag 的值:");
            Scanner scanner = new Scanner(System.in);
            flag = scanner.nextInt();
        });

        t1.start();
        t2.start();

    }
}

5.饿汉模式和懒汉模式的差别

图展示

自身理解
  • 饿汉式
    餐厅提前把所有菜品都做好(不管有没有顾客点),比如把 100 道菜全做完摆桌上。虽然顾客来后能直接吃,但准备过程耗时久,还可能浪费(如果菜没卖完)。
  • 懒汉式
    餐厅等顾客点菜后,再一道一道做。比如顾客先点 1 道菜,厨房马上做这 1 道;顾客再加点菜,厨房继续做新点的。这样不会提前浪费精力备菜,但每次点菜后需要等一会儿菜才能上桌。

总结

  • 饿汉式是 “提前准备好所有”,适合资源占用不高、需要快速使用的场景。
  • 懒汉式是 “用到再准备”,适合资源庞大、按需使用的场景,避免一开始浪费过多资源。

6.饿汉模式和懒汉模式谁是线程安全

饿汉模式(是天然线程安全)

饿汉模式(程序启动,类加载的时候,就创建实例)
天然就是线程安全.只是涉及到读操作。

懒汉模式(不是天然)

首次调用的时候,涉及到读和写.就可能存在线程安全问题.=>创建出多个实例
1)加锁.把if和new包裹到一起
2)在锁外头加上条件.首次调用才加锁,后续调用不必加锁了.
3)指令重排序带来的问题.volatile


new操作可能涉及到三个步骤
a)申请内存
b)调用构造方法正常abc来执行
c)把内存地址赋值给引用可能被优化成acb

1. 初始懒汉式(线程不安全)
class SingletonLazy {  
    private static SingletonLazy instance = null;  
    public static SingletonLazy getInstance() {  
        if (instance == null) {  
            instance = new SingletonLazy();  
        }  
        return instance;  
    }  
    private SingletonLazy() {}  
}  

问题:多线程下,多个线程同时检测到 instance == null,会创建多个实例,破坏单例。
类比:奶茶店没排队规则,多人同时买奶茶,店员给每个人都做一杯,最后每人拿到不同的奶茶。

2. 加锁版本(解决安全,但效率低)
class SingletonLazy {  
    private static SingletonLazy instance = null;  
    public static SingletonLazy getInstance() {  
        synchronized (SingletonLazy.class) { // 加锁  
            if (instance == null) {  
                instance = new SingletonLazy();  
            }  
        }  
        return instance;  
    }  
    private SingletonLazy() {}  
}  

问题:每次调用 getInstance 都要加锁,即使实例已创建。就像奶茶店不管有没有人,所有人都必须排队,浪费时间。

3. 双重检查 + volatile(最终线程安全)
class SingletonLazy {  
    private volatile static SingletonLazy instance = null; // 加 volatile  
    private static final Object locker = new Object();  
    public static SingletonLazy getInstance() {  
        if (instance == null) { // 第一次检查  
            synchronized (locker) {  
                if (instance == null) { // 第二次检查  
                    instance = new SingletonLazy();  
                }  
            }  
        }  
        return instance;  
    }  
    private SingletonLazy() {}  
}  
  • 双重检查:先判断 instance 是否为空,不为空直接返回,减少加锁次数。
  • volatile:禁止指令重排,确保 instance 初始化完成后再赋值,避免其他线程拿到未初始化的对象。
    类比:奶茶店设置 “先看是否有做好的奶茶(第一次检查),没有再排队(加锁),排队时再确认一次(第二次检查)”,同时确保奶茶制作流程正确,不会 “半成品” 就给顾客。

7.饿汉模式和懒汉模式的使用场景

  • 懒汉模式适用场景
    • 1. 资源紧张场景

      描述:当某样东西平时不常用,但准备起来很麻烦 / 占地方时,等需要时再准备更划算。
      例子:家庭露营装备(帐篷、睡袋等):平时放在仓库占地方,且组装耗时。只有计划露营时才拿出来清洗、组装,避免长期占用客厅空间。

    • 2. 实例使用不确定场景

      描述:某样东西可能永远用不上,但准备起来要花钱 / 费精力,不如等需要时再准备。
      例子:家庭备用药品(如退烧药、烫伤膏):平时不一定生病,但每次购买都要花钱且有保质期。需要时再去药店买,避免过期浪费。

    • 3. 延迟加载需求场景

      描述:想灵活控制某件事的启动时机,按需触发。
      例子:按需订阅的流媒体会员(如 Netflix):想看电影时才开通会员,而不是提前付费全年。用几天就暂停,灵活控制成本。

  • 饿汉模式适用场景
    • 1. 资源占用少场景

      描述:某样东西很便宜 / 不占地方,提前准备好反而更方便。
      例子:家里的牙刷、牙膏:价格低且体积小,每天必用。直接买好放在卫生间,随时能用,无需临时购买。

    • 2. 追求快速启动场景

      描述:某件事需要快速响应,提前准备能节省时间。
      例子:早餐店的包子:开店前就蒸好一批包子,顾客一来就能直接拿,避免现做等待,提升效率。

    • 3. 高并发且线程安全场景

      描述:多人同时需要某样东西,必须保证公平且不出错。
      例子:医院的挂号系统:提前分配好所有诊室和医生的排班表(相当于提前创建实例),患者挂号时直接按规则分配,避免冲突。

一句话理解

  • 懒汉模式像 “外卖”:饿了才点,不囤货。
  • 饿汉模式像 “冰箱里的泡面”:提前准备好,随时能吃。

四、锁机制类

  1. synchronized的原理是什么?

    • 答案synchronized是基于Java对象头中的monitor(监视器锁)实现的,每个对象都有一个monitor,当线程进入synchronized代码块或方法时,会尝试获取对象的monitor,获取成功则持有该monitor,否则会阻塞等待。

    • 生活例子synchronized的原理就像一个会议室的钥匙,只有拿到钥匙的人才能进入开会,其他人要在外面等钥匙被归还。

  2. ReentrantLocksynchronized的区别是什么?

    • 答案ReentrantLock提供了更灵活的锁操作,如尝试非阻塞获取锁、可中断获取锁、绑定多个条件等。ReentrantLock需要手动释放锁,而synchronized在方法执行完或代码块执行完后自动释放锁。

    • 生活例子ReentrantLock就像一个可以预约的会议室,你可以提前预订,也可以随时取消预订。而synchronized就像一个只能在需要时临时进入的会议室,用完自动释放。

  3. 什么是锁的可重入性?

    • 答案:锁的可重入性是指一个线程在获取到锁后,可以再次获取该锁而不会被阻塞。

    • 生活例子:锁的可重入性就像一个人已经进入了会议室开会,如果需要再次进入同一个会议室(比如去拿资料),可以直接进入而不需要再次等待。

volatile关键字

volatile 和 synchronized 的区别

synchronized 能够保证原子性(要么全部执行并且在执行过程中不会被任何因素打断,要么就完全不执行)
volatile 保证的是内存可见性。
自身理解
假设你要从自己的银行账户给朋友转 500 元。
它包含两个步骤,一是从你的账户扣除 500 元,二是往你朋友的账户增加 500 元。
这两个步骤必须要么都成功完成,就像一个整体一样;要么都不做。如果在扣除你账户的钱之后,突然因为系统故障等原因,没有给你朋友的账户增加钱,那就会出大问题,你的钱平白无故少了,这就是原子性被破坏的后果。
static class Counter {
 volatile public int count = 0;
 void increase() {
 count++;
 }
}
public static void main(String[] args) throws InterruptedException {
 final Counter counter = new Counter();
 Thread t1 = new Thread(() -> {
 for (int i = 0; i < 50000; i++) {
 counter.increase();
 }
 });
 Thread t2 = new Thread(() -> {
 for (int i = 0; i < 50000; i++) {
 counter.increase();
 }
 });
 t1.start();
 t2.start();
 t1.join();
 t2.join();
 System.out.println(counter.count);
}
最终 count 的值仍然⽆法保证是 100000.

什么情况下使用volatile

流程图

文字描述

代码
package thread;

//就期望这个类只能有唯一的实例(一个进程中)
class SingletonLasy{

    //private static SingletonLasy instance = null;
//加入volatile关键字,保证只能运行 123顺序
//1.每次访问必须重新读取内存(我后台更新了东西,前台刷新一下就行)
//2.禁止指令重新排列

    private volatile static SingletonLasy instance = null;

    private static Object locker = new Object();

    public static SingletonLasy getInstance(){
        
        if(instance == null){
            synchronized (locker) {
                if(instance == null) {
                    instance = new SingletonLasy();
                }
            }
        }
        return instance;
    }

    private SingletonLasy(){}

}
public class ThreadDemo27 {
    public static void main(String[] args) {

        //调用静态方法getInstance,不需要使用new的
        SingletonLasy s1 = SingletonLasy.getInstance();
        SingletonLasy s2 = SingletonLasy.getInstance();
        System.out.println(s1 == s2);
    }
}

自身理解

场景:餐厅打饭(单例对象 = 一份饭)。

  • 正常流程:取碗(申请内存)→ 打饭(初始化)→ 端走(赋值给变量)。
  • 指令重排序问题
    • 顾客 A:取碗(1)→ 先端着空碗给窗口看(3,赋值未初始化的对象)→ 还没打饭(2,未初始化),就被朋友叫走。
    • 顾客 B:看到窗口有碗(以为有饭),直接端走空碗。结果吃的时候发现碗里没饭,出问题。
      总结:步骤顺序错乱(指令重排序),导致后续使用 “半成品” 资源(未初始化的对象),引发错误。

六、wait

wait 和 sleep 的对比(重点)

相同点

  • 线程执行控制:都能让线程放弃执行一段时间,实现线程的 “暂停” 状态。
  • 时间相关特性:均支持设定时间,时间到达后线程解除阻塞,继续执行。
  • 可提前唤醒:两者都能在设定时间未到时被提前唤醒,打破等待状态。

不同点

  1. 功能用途

    • wait:核心用于线程间通信,等待其他线程通过 notify/notifyAll 通知(不要求通知线程执行完)。
    • sleep:单纯让线程阻塞一段时间,无通信目的;join 虽也涉及线程等待,但其逻辑是 “等待另一线程执行完才继续”。
  2. 唤醒机制

    • wait:依赖其他线程调用 notify/notifyAll 唤醒。
    • sleep:通过 interrupt 方法唤醒,且这种唤醒通常意味着程序出现特殊异常情况,非常规业务流程。
  3. 使用场景逻辑

    • wait:适用于 “不知道要等多久” 的场景,超时时间仅作为兜底方案。
    • sleep:用于明确知道等待时间的场景,期望按设定时间准时唤醒。
  4. 语法与搭配

    • 归属类wait 是 Object 类的方法;sleep 是 Thread 类的静态方法。
    • 搭配使用wait 需搭配 synchronized 关键字(在同步代码块中使用);sleep 无此要求,可直接调用。

七、线程池

    为什么使用线程池

    使用多进程确实能够进行并发编程,但是频繁创建销毁进程,成本比较高
    引入了线程(轻量级进程).复用资源的方式,来提高了创建销毁效率


    随着创建销毁线程的频率进一步提升,开销仍然无法忽略不计了,那么就有两个解决方案
    1)协程/纤程(轻量级线程)
    2) 线程池提前把要使用的线程,在线程池中准备好.需要用就从池子里取,用完之后也是还给池子

    线程池最大的好处就是减少每次启动、销毁线程的损耗。

    自身理解

    开了一家餐厅:
    1. 服务员团队 = 线程池里的线程

      • 你提前雇佣了 5 个全职服务员(核心线程)
      • 最多可以再临时招聘 3 个兼职(最大线程数)
    2. 顾客订单 = 需要处理的任务

      • 当顾客进店,服务员会立即接待(直接分配线程处理)
      • 高峰期订单太多时,顾客需要在候餐区排队(任务进入队列)
      • 超过最大服务员数量时,就会提示 "本店已满"(拒绝策略)
    3. 服务员的工作流程

      • 空闲时在休息区待命(线程处于等待状态)
      • 接到任务后立即服务(线程开始执行任务)
      • 服务结束后回到休息区(线程归还线程池)

    二、线程池的核心优势
    1. 节省资源

      • 不用每次来顾客都临时招聘和解雇服务员(避免频繁创建销毁线程)
    2. 高效响应

      • 服务员随时待命,顾客不用等待招聘时间(任务立即执行)
    3. 流量控制

      • 控制服务员数量避免混乱(防止过多线程耗尽资源)

    三、现实中的应用场景
    • 银行柜台:固定窗口数量,顾客取号排队
    • 快递分拣中心:传送带相当于任务队列,工人相当于线程
    • 外卖平台:骑手团队实时接单,高峰期动态调度

    四、总结


    线程池就像一支训练有素的服务团队:

    • 预先准备好固定数量的线程(核心线程)
    • 任务多时动态扩展线程(最大线程数)
    • 任务少时自动收缩线程(节省资源)
    • 通过队列管理等待处理的任务

    线程池的核心参数有哪些?


    答案:线程池的核心参数包括corePoolSizemaximumPoolSizekeepAliveTime
    unit,workQueuethreadFactory 和 handler 等.


    生活例子:线程池的核心参数就像餐厅的运营参数,corePoolSize是固定服务员的数量
    maximumPoolSize 是最多能临时招聘的服务员数量keepAliveTime是临时服务员在没有
    任务时能等待的时间,workQueue:是顾客的订单排队区域threadFactory是招聘服务员
    的方式,handler:是当餐厅太忙无法接待更多顾客时的处理方式。

    什么是CPU/IO密集型的任务

    1. CPU 密集型任务

    定义:任务大部分时间都在 “占用 CPU 做计算”,比如复杂的数学运算、数据处理。就像一个人一直闷头算数学题,离不开大脑(CPU)。
    生活实例
    想象你开了一家饺子店,有个员工专门负责 “快速擀饺子皮”。他一直不停地揉面、擀皮(相当于 CPU 持续计算),几乎没有休息时间,所有精力都花在 “擀皮” 这个核心操作上。这种一直占用 “劳动力核心” 的工作,就是 CPU 密集型任务。


    2. IO 密集型任务

    定义:任务大部分时间在 “等待外部操作完成”,比如读取用户输入、读写文件、网络请求。CPU 在这段时间是空闲的,就像等人送货,等待时啥也不用干。
    生活实例
    还是那家饺子店,另一个员工负责 “等快递送面粉”。他大部分时间都在刷手机等快递(等待 IO 操作),真正干活(用 CPU)的时间很少 —— 只有面粉送到后,搬面粉进店那一会儿需要动动手。这种 “大部分时间在等待,不咋占用核心劳动力” 的工作,就是 IO 密集型任务。

    总结对比
    类型核心特点生活类比
    CPU 密集型疯狂占用 CPU 计算员工一直擀皮,停不下来
    IO 密集型大部分时间在等待外部操作完成员工等快递,闲着没事干

    如何设定线程的数量

    1. CPU 密集型任务的线程数量策略

    文字含义
    如果一个进程里,所有线程都在疯狂占用 CPU 计算(比如算数学题、处理数据),这时线程数量不该超过 CPU 的逻辑核心数。
    类比生活
    想象你有一家手工月饼店,只有 5 台烤箱(相当于 CPU 逻辑核心数)。每个月饼制作(线程)都需要一直占用烤箱烘烤(CPU 计算)。如果同时做 6 个月饼,烤箱不够用,第 6 个月饼只能等着,反而拖慢整体效率。所以,做这种 “一直占用核心资源” 的任务,数量不能超过核心资源数。


    2. IO 密集型任务的线程数量策略

    文字含义
    如果一个进程里,所有线程大部分时间在等待 IO(比如等用户输入、等文件读取),不咋占用 CPU,这时线程数量可以远超过 CPU 逻辑核心数。
    类比生活
    还是那家月饼店,现在服务员负责 “等顾客下单”(IO 等待)。顾客没来时,服务员闲着(不占用烤箱 / CPU)。这时候可以多招服务员(线程),比如招 20 个。因为大部分时间他们在闲着等顾客(IO 等待),不会让烤箱(CPU)忙不过来。即使人多,也不影响核心资源,还能更快服务顾客。


    总结对比
    任务类型线程数量策略核心逻辑生活类比总结
    CPU 密集型不超过 CPU 逻辑核心数避免 CPU 过度竞争,效率降低烤箱有限,月饼多了挤着烤
    IO 密集型可远超过 CPU 逻辑核心数线程多数时间空闲,不占 CPU服务员多等顾客,不占烤箱

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

    当前余额3.43前往充值 >
    需支付:10.00
    成就一亿技术人!
    领取后你会自动成为博主和红包主的粉丝 规则
    hope_wisdom
    发出的红包
    实付
    使用余额支付
    点击重新获取
    扫码支付
    钱包余额 0

    抵扣说明:

    1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
    2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

    余额充值