Java-01-源码篇-并发编程-多线程基础

目录

一,多线程概述

1.1 什么是多线程

1.2 线程与进程

二,多线程基本使用

2.1 Thread 介绍

2.2 面试题: 方式一和方式二的调用区别是什么?

三 线程的生命周期

四,守护线程 (Daemon Thread)

五,常用线程API的讲解

4.1 start()和run() 方法

4.2 sleep(),join(), wait()方法

4.2.1 sleep(): 让当前线程暂停执行一段时间,不释放锁 。

4.2.2 join(): 让当前线程等待另一个线程执行完毕再继续。

4.2.3 wait() 让当前线程释放锁并进入等待状态,等待 notify() 或 notifyAll() 唤醒。

4.2.4 【总结】

六 中断异常 InterruptedException

6.1 任务超时处理案例

6.2 多线程文件处理

6.3 stop() 和 interrupt()

6.3.1 stop() 方法

6.3.2 interrupt() 方法


        语雀笔记:https://www.yuque.com/yuquexiansheng/znisb2/kyn79ku9ixwzq8xu?singleDoc# 《多线程》

一,多线程概述

1.1 什么是多线程

        多线程(Multithreading)是一种并发编程技术,它允许一个程序同时运行多个线程,每个线程执行不同的任务,从而提高程序的执行效率。

        为什么要使用多线程,因为在现代计算机中,所有电脑基本上是多核CUP,每一个CUP都可以独立调用一条线程。这样就可以充分硬件资源,从而提高程序的执行效率。

多线程的主要优势包括:

  • 提高执行效率:可以让多个任务同时运行,充分利用 CPU 资源。
  • 增强程序响应能力:避免主线程被阻塞,提升用户体验(如 GUI 程序)。
  • 更好地处理 I/O 任务:比如爬虫、文件下载等,使用多线程可以避免等待时间浪费。

1.2 线程与进程

        从结构组成上讲,一个进程由至少一个或者多个线程组成。从系统资源分配的角度来讲,进程之间内存资源互相独立,互不打扰。同一进程之间内存资源共享,因为都来源于同一个进程。

        此外,线程之间也有自己的专属工作内存。这个工作内存线程之间是不共享。

        而且线程是 CPU 资源调用的基本单位,一个 CPU 轮询调用线程。多核CPU 自然就可以并行处理多线。

资源类型

进程(Process)

线程(Thread)

内存(Memory)

拥有独立的地址空间,每个进程的内存互不影响

共享进程的地址空间(代码段、数据段、堆、全局变量)

CPU(调度)

由操作系统进行调度,一个进程至少有一个线程运行

线程是 CPU 调度的基本单位,多个线程可以并行执行

文件句柄

进程拥有自己的文件描述符(FD),文件独立打开

线程共享进程的文件描述符

全局变量

进程的全局变量独立,互不干扰

线程共享进程的全局变量,可能引发竞争问题

资源开销

进程创建、切换、销毁的开销较大,需要操作系统分配独立的资源

线程创建、切换、销毁的开销较小,共享进程资源

通信方式

需要使用 IPC(管道、消息队列、共享内存等),通信复杂

共享内存,直接修改变量,通信简单但需保证同步

二,多线程基本使用

2.1 Thread 介绍

        Thread 是线程的实体类,想要将其他任务进行分给其他线程帮忙分担,可以通过Thread来实现。下面是一个工单打印的任务功能如下, 代码如下:

class EmployeeThread extends Thread { // 定义线程主体类
    /** 自定义一个线程名称 */
    private String name ;

    public EmployeeThread(String name) {
        this.name = name ;
    }

    @Override
    public void run() {
        for (int x = 1 ; x < 3 ; x ++) {
            System.out.println("【流水工单】" + this.name + "生成工单号:" + this.generateJobId());
        }
    }

    private String generateJobId() {
        return "JOB-" + UUID.randomUUID().toString().substring(0, 8).toUpperCase();
    }
}


public class ThreadTest {
    public static void main(String[] args) {
        EmployeeThread thread1 = new EmployeeThread("张三");
        EmployeeThread thread2 = new EmployeeThread("李四");
        EmployeeThread thread3 = new EmployeeThread("王五");
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

输出内容如下

【流水工单】王五生成工单号:JOB-CD4FE818
【流水工单】李四生成工单号:JOB-D2F81B09
【流水工单】张三生成工单号:JOB-BCD2771A
【流水工单】王五生成工单号:JOB-5CF5D7A6
【流水工单】李四生成工单号:JOB-39D61E6C
【流水工单】张三生成工单号:JOB-E728CF50

        将流水工单的业务功能放在EmployeeThread 类之中,并重写run方法。通过 start() 开启一条线程。

2.2 面试题: 方式一和方式二的调用区别是什么?

    public static void main(String[] args) {
        EmployeeThread thread1 = new EmployeeThread("张三");
        EmployeeThread thread2 = new EmployeeThread("李四");
        EmployeeThread thread3 = new EmployeeThread("王五");
        // 方式一
        thread1.start();
        thread2.start();
        thread3.start();
        // 方式二
        thread1.run();
        thread2.run();
        thread3.run();
    }

        方式一,才是真正的开启一条新线程执行。thread1,2,3分别都开启一条新线程执行,所有仅仅上面的代码 (不考虑JVM内部相关业务) 而然是有四条线程分别是main方法主线程,thread1,2,3 各一条。方式二仅仅只是普通的调用,其执行的线程还是main方法的主线程。

三 线程的生命周期

        通过上面的案例,我们可以得知,线程的开启/创建是通过start()方法执行的。那线程的任务执行完了之后,什么时候被销毁?线程的整个生命周期流程是怎么样

    【文字总结】:

    • 创建(new)后必须调用 start(),才能进入调度状态
    • 阻塞状态不会自动恢复,需要被唤醒notify() / sleep() 睡眠时间到)。
    • 线程最终都会进入 Terminated 状态,等待垃圾回收。

    四,守护线程 (Daemon Thread)

            在 Java 中,守护线程 (Daemon Thread) 是一种辅助性线程,它的生命周期依赖于主线程或其他非守护线程。当所有非守护线程都执行完毕后,JVM 会自动终止所有守护线程,而不会等待它们执行完毕。

            从上面的描述,可以发现其特点:线程可以在创建辅助线程(多条),而辅助线程不可以在创建辅助线程。其二一旦主线程消亡,其辅助线程也会随之消亡。

    class HeartbeatDaemon extends Thread {
        public HeartbeatDaemon() {
            setDaemon(true);
        }
    
        @Override
        public void run() {
            while (true) {
                System.out.println("检查服务器状态...【存活】");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class HeartbeatDaemonTest {
        public static void main(String[] args) throws InterruptedException {
            HeartbeatDaemon heartbeatThread = new HeartbeatDaemon();
            heartbeatThread.start();
    
            TimeUnit.SECONDS.sleep(5); // 睡眠5秒,模拟服务器运行
            System.out.println("服务器已停止运行,退出程序...");
    
    
        }
    }
    /**
    输出结果:
    检查服务器状态...【存活】
    检查服务器状态...【存活】
    检查服务器状态...【存活】
    检查服务器状态...【存活】
    检查服务器状态...【存活】
    检查服务器状态...【存活】
    服务器已停止运行,退出程序...
    */

    五,常用线程API的讲解

            假如我们把线程比作是流水线里的一个执行流程线,线程从开始运行到结束,整个过程的流程管理。都提供哪些对应的业务API接口。

    官方文档:Thread (Java SE 23 & JDK 23)

    4.1 start()和run() 方法

    start()

    让线程进入就绪状态等待CPU轮询调用。并且线程只能调用一次。线程终止结束也不能重新再次调用。那线程池的线程复用是怎么实现的?

    run()

    run()是线程执行任务/业务逻辑的入口。并且run方法还抽离成统一的标准接口Runnable 接口。Thread的run方法是实现弗雷Runnable而来。为什么要统一标准接口?

    【好处如下】

            一,分离“线程”“任务”,如果直接继承 Thread ,则任务和线程是绑定的,不利于代码复用。并且也避免了java继承的局限性。java是单继承多实现。

            二,统一 run() 方法的标准定义,从而保证了所有的线程任务的执行逻辑是一致的。不同的任务(计算任务,I/O任务)可以通过统一标准接口Runable 方式组织,实现标准化和解耦

    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("线程执行任务:" + Thread.currentThread().getName());
        }
    }
    // 这种方式会导致:
    // 任务逻辑和线程管理耦合,不利于扩展
    // 无法共享任务,因为Thread 不是复用的
    
    Runnable task = new MyTask();
    Thread thread1 = new Thread(task);
    Thread thread2 = new Thread(task);
    thread1.start();
    thread2.start();
    // 使用 Runnable 让任务可复用,同一个任务被多个线程执行,增强复用性

            三,线程池的线程复用也是基于 Runnable(或 Callable),而不是 Thread 重新调用start()。 在 Java 中,一个 Thread 对象的 start() 方法只能调用一次,如果试图重复调用,会抛出异常

    Thread thread = new Thread(() -> System.out.println("任务执行"));
    thread.start();
    thread.start();  // ❌ java.lang.IllegalThreadStateException

            所以 Thread 本身不能被复用,每次都要创建新的对象,这样开销大,性能差

    ExecutorService executor = Executors.newFixedThreadPool(2);
    
    executor.submit(() -> System.out.println(Thread.currentThread().getName() + " 执行任务1"));
    executor.submit(() -> System.out.println(Thread.currentThread().getName() + " 执行任务2"));
    executor.submit(() -> System.out.println(Thread.currentThread().getName() + " 执行任务3"));
    
    executor.shutdown();
    // 线程池取出一个空闲的 Thread。
    // 这个 Thread 执行新的 Runnable 任务。
    // 任务执行完毕后,线程不会销毁,而是继续等待下一个任务。

            ✅ 同一个线程可以被多次使用,执行不同的 Runnable 任务!

    4.2 sleep(),join(), wait()方法

            线程在任务执行的过程之中,还提供一些中间处理的操作,比如多线程并发执行任务,需要保持线程的有序执行等等。

    4.2.1 sleep(): 让当前线程暂停执行一段时间,不释放锁 。

    public class SleepExample {
        public static void main(String[] args) {
            System.out.println("Start");
            try {
                Thread.sleep(2000); // 当前线程暂停 2 秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("End");
        }
    }
    

    4.2.2 join(): 让当前线程等待另一个线程执行完毕再继续。

            需要确保某个线程先执行完(如主线程等待子线程完成任务)。

    public class JoinExample {
        public static void main(String[] args) throws InterruptedException {
            Thread thread = new Thread(() -> {
                try {
                    Thread.sleep(3000);
                    System.out.println("子线程执行完毕");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
    
            thread.start();
            thread.join(); // 主线程等待子线程执行完毕
            System.out.println("主线程继续执行");
        }
    }
    // 执行结果
    // 子线程执行完毕
    // 主线程继续执行

      join()让当前线程等待 另一个线程执行完毕后再继续。 适用于线程间的同步,防止主线程过早结束 。

    4.2.3 wait() 让当前线程释放锁并进入等待状态,等待 notify()notifyAll() 唤醒。

             线程间通信(生产者-消费者模型)

    class WaitExample {
        private static final Object lock = new Object();
    
        public static void main(String[] args) {
            Thread thread1 = new Thread(() -> {
                synchronized (lock) {
                    System.out.println("Thread1: 等待中...");
                    try {
                        lock.wait(); // 释放锁,并等待唤醒
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Thread1: 被唤醒,继续执行");
                }
            });
    
            Thread thread2 = new Thread(() -> {
                synchronized (lock) {
                    System.out.println("Thread2: 唤醒 Thread1");
                    lock.notify(); // 唤醒等待的线程
                }
            });
    
            thread1.start();
            try {
                Thread.sleep(1000); // 确保 Thread1 先执行
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            thread2.start();
        }
    }
    /**
     * Thread1: 等待中...
       (1 秒后)
       Thread2: 唤醒 Thread1
       Thread1: 被唤醒,继续执行
    */

            wait() 必须在 synchronized 代码块 内使用,否则报错。

            wait() 释放锁,等待 notify()/notifyAll() 唤醒

    4.2.4 【总结】

    方法

    释放锁?

    让哪个线程等待?

    何时继续执行?

    适用场景

    sleep(ms)

    不释放

    当前线程

    时间结束

    限流、模拟延迟

    join()

    不释放

    当前线程

    目标线程结束

    等待某个线程执行完

    wait()

    释放锁

    当前线程

    notify()

    /notifyAll()

    线程间通信

    场景分类:

    • 简单延迟 👉 sleep()
    • 等待另一个线程执行完 👉 join()
    • 线程间通信(生产者-消费者) 👉 wait() + notify()

    六 中断异常 InterruptedException

      InterruptedException用于处理中断请求,它通常在线程调用 sleep()wait()join() 等方法时抛出。

         在实际开发中,中断异常(InterruptedException)常用于任务超时处理、定时任务、文件处理、数据库查询超时等。以下是几个常见的业务案例,帮助理解如何在真实场景中正确处理中断异常。

    6.1 任务超时处理案例

            在企业应用中,经常需要执行定时任务,如 数据同步、爬取网页、文件处理 等。如果某个任务执行时间过长,我们希望它能够超时退出,而不是一直阻塞。

    public class TaskTimeoutExample {
        public static void main(String[] args) {
            ExecutorService executor = Executors.newSingleThreadExecutor();
            Future<?> future = executor.submit(() -> {
                try {
                    System.out.println("任务开始...");
                    Thread.sleep(5000); // 模拟长时间运行任务
                    System.out.println("任务完成...");
                } catch (InterruptedException e) {
                    System.out.println("任务超时,被中断...");
                }
            });
    
            try {
                future.get(3, TimeUnit.SECONDS); // 限制任务执行时间最多 3 秒
            } catch (TimeoutException e) {
                System.out.println("超时,取消任务...");
                future.cancel(true); // 发送中断信号
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            } finally {
                executor.shutdown();
            }
        }
    }

    输出

    任务开始...
    超时,取消任务...
    任务超时,被中断...

    6.2 多线程文件处理

            假设我们有一个任务需要批量处理多个大文件,每个线程负责解析一个文件。如果某个文件处理时间过长,我们希望它超时退出,不影响整体进度。

    public class FileProcessingExample {
        public static void main(String[] args) {
            ExecutorService executor = Executors.newFixedThreadPool(2);
            File[] files = {new File("file1.txt"), new File("file2.txt")};
    
            for (File file : files) {
                Future<?> future = executor.submit(() -> {
                    try {
                        System.out.println("处理文件: " + file.getName());
                        Thread.sleep(5000); // 模拟文件处理时间
                        System.out.println("文件 " + file.getName() + " 处理完成");
                    } catch (InterruptedException e) {
                        System.out.println("文件 " + file.getName() + " 处理超时,被中断...");
                    }
                });
    
                try {
                    future.get(3, TimeUnit.SECONDS); // 限制单个文件处理时间最多 3 秒
                } catch (TimeoutException e) {
                    System.out.println("文件 " + file.getName() + " 处理超时,取消任务...");
                    future.cancel(true); // 取消超时任务
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            }
    
            executor.shutdown();
        }
    }
    

    输出

    处理文件: file1.txt
    文件 file1.txt 处理超时,取消任务...
    文件 file1.txt 处理超时,被中断...
    处理文件: file2.txt
    文件 file2.txt 处理超时,取消任务...
    文件 file2.txt 处理超时,被中断...

    6.3 stop() 和 interrupt()

            在线程当中,有两种方式进行线程中断,一种是强制性的stop(), 另一种是比较温和的interrupt();

    6.3.1 stop() 方法

    • 已过时Thread.stop() 方法已经在 JDK 1.2 中被标记为过时(deprecated),并且 不推荐使用。这是因为它会强制终止线程,直接抛出 ThreadDeath 错误并清理线程的资源,可能导致 数据不一致、资源泄漏死锁 等问题。
    • 实现机制:当调用 stop() 时,JVM 会立即终止目标线程,不管该线程是否在执行关键代码或释放资源。这种方式比较“粗暴”,可能会打断线程中正在进行的任务,导致系统的不稳定。

    • 不安全:会直接强制停止线程,而不允许线程自己释放资源或进行清理。
    • 可能导致数据不一致:例如,在更新共享数据时,线程被强制停止可能导致数据处于不一致的状态。
    • 破坏线程的正常执行流程:调用 stop() 后,线程不能继续执行任何代码,可能导致一些逻辑错误。

    6.3.2 interrupt() 方法

    • 推荐使用Thread.interrupt() 是正确的方式来停止线程,它不会直接停止线程,而是 发送一个中断信号,让线程有机会去响应并做适当的清理工作。
    • 实现机制:当调用 interrupt() 方法时,目标线程会收到一个中断信号。线程可以通过检测 Thread.interrupted()isInterrupted() 方法来知道自己是否被中断。如果线程正在执行阻塞操作(如 sleep()wait()join()),这些操作会抛出 InterruptedException 异常,并允许线程处理异常并退出或采取其他适当的行为。

    优点

    • 优雅退出:线程可以在适当的时机退出,处理异常或清理资源,避免资源泄漏和死锁。
    • 更安全interrupt() 是通过异常或检查标志来通知线程,因此线程有机会按自己的方式响应中断。

    最后,如果这篇文章对你有帮助,欢迎 点赞👍、收藏📌、关注👀
    我会持续分享 Java、Spring Boot、MyBatis-Plus、微服务架构 相关的实战经验,记得关注,第一时间获取最新文章!🚀

    系列文章推荐

    这篇文章是 【Java SE 17源码】系列 的一部分,详细地址:

    java SE 17 源码篇_吐司呐的博客-CSDN博客

    记得 关注我,后续还会更新更多高质量技术文章!

    你在实际开发中遇到过类似的问题吗?
    欢迎在评论区留言交流,一起探讨 Java 开发的最佳实践! 🚀

    评论
    添加红包

    请填写红包祝福语或标题

    红包个数最小为10个

    红包金额最低5元

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

    抵扣说明:

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

    余额充值