JavaEE之多线程

进程与线程的基础概念

什么是线程与进程

  • 进程(Process)和线程(Thread)是操作系统的概念,简单来说:进程是操作系统进行资源分配 的基本单位,线程是操作系统进行CPU调度执行的基本单位

  • 进程是资源分配的基本单位:

    • 当一个程序(如浏览器)启动时,操作系统会为其创建一个进程。这个进程会获得一套独立的系统资源,包括:
      • 拥有一片内存空间存放程序代码、运行时数据、运行所需堆栈空间
      • 获得系统资源如网络连接、IO设备接口、被程序打开的文件资源等
    • 总的来说操作系统以进程为整体,来分配、管理和保护这些资源。不同的进程之间资源是相互隔离的
  • 线程是CPU调度和执行的基本单位:

    • 一个进程内部可以创建多个线程 。所有这些线程共享 其所属进程的全部资源(内存、文件等)。
      • 每个线程有自己独立的程序计数器、寄存器和栈,用于记录执行到了哪一行代码。

      • CPU真正执行的是线程的指令。操作系统的调度器(Scheduler)决定在某个时刻,将CPU时间片分配给哪个线程来运行。

  • 举一个生动的比喻:

    • 操作就像一个工厂
      • 进程就像一个独立的车间 。建立车间时,工厂会为它分配独立的厂房空间(内存)、专用仓库(资源)和电力配额。车间是分配资源的基本单位。
      • 线程就像车间里的工人 。所有工人在同一个车间里工作,共享车间的空间、设备和材料。工厂调度中心(CPU调度器)直接安排每个工人具体做什么、何时做。工人是具体干活的基本单位。
  • 为什么这样设计?

    • 资源隔离与保护:以进程为单位分配资源,使得系统更稳定、安全。一个程序出错不会"污染"其他程序的内存。
    • 提高并发与效率:以线程为单位进行CPU调度,使得创建、切换、通信的开销远小于进程。这使得程序(特别是服务器、图形界面程序)能够高效地同时处理多个任务,充分利用多核CPU的性能。
  • 总结进程是资源的"容器"线程是容器里的"工作者" 。操作系统为容器(进程)配备资源,然后调度工作者(线程)去使用这些资源完成任务。

CPU的线程并行

  • CPU分为单核CPU和多核CPU: 线程是操作系统进行CPU调度和执行的基本单位,在最早的时候cpu是一个线程占用一个核心(core)的,也就是说单核cpu在同一时间只能运行一个线程,一个八核的多核cpu可以同时运行8个线程。
  • 超线程技术(Hyper-Threading,简称SMT):
    • 可以让一个物理核心能够"同时"处理两个线程。
    • 超线程技术的原理就在于这个核心里多放了一套"门面"(寄存器、架构状态等),但共用同一套"内脏"(算术逻辑单元ALU等执行资源),当一个线程停下来等待数据时,核心立刻无缝切换到另一个线程去执行。因为切换速度极快(几纳秒),在人类看来,就像是这两个线程在真正并行 执行。
    • 利用超线程技术,一个八核心CPU可同时运行16个线程

用户态与内核态(线程的两种运行权限)

  • 这是操作系统的两种CPU运行级别,决定了代码能执行的指令和访问的资源范围。

  • 概念说明:

    概念 说明 典型场景
    用户态 运行普通应用程序 的级别。权限较低,不能直接访问硬件或执行特权指令。如果程序需要这些资源,必须通过系统调用向操作系统申请。 执行你编写的Java、Python等应用程序代码。
    内核态 运行操作系统内核代码的级别。拥有最高权限,可以执行所有CPU指令,直接访问所有硬件和内存空间。 执行系统调用(如文件读写read/write)、处理中断、进行进程调度等。
  • 为什么需要区分?

    • 核心目的是安全与稳定。将应用程序限制在用户态,可以防止一个错误的或恶意的程序直接操控硬件、破坏系统或其他进程,从而保障整个操作系统的稳定运行。
  • 与线程的关系

    • 当你的线程执行普通代码时,它运行在用户态
    • 当线程需要执行如读写文件、申请内存、创建子进程等操作时,会触发系统调用 ,此时CPU会从用户态切换到内核态,由操作系统内核代为完成这些高权限操作,完成后再切换回用户态继续执行线程代码。
    • 这种切换(上下文切换)是有开销的,频繁的系统调用会影响性能。

CUP时间片

  • 在任务管理器查看CPU性能时可以看到,当前操作系统中线程数量是远远大于CUP最大线程数量,如下图所示。这么多线程CPU是怎么做到一起运行的呢?
  • 答案就在于,为各个线程都分配了运行的时间片,各个线程在CPU调度器的指挥下有序运行。
  • CPU时间片:
    • CPU时间片是操作系统进行线程调度的"心跳"和"节拍器"。它通过给每个线程一小段独占CPU的时间,并以极快的速度在它们之间轮转,创造出了整个系统多任务并发的幻觉,并保证了公平与响应速度。
    • 线程获得CPU时间片和失去CPU时间片,是根据当前状态来决定的,线程的状态轮转,在本文的后续部分会详细说明,现在只要知道有这个概念就行
  • CPU时间片关键要点和影响:
    • 上下文切换的开销的影响:每次时间片用完或线程阻塞,操作系统都需要保存当前线程的现场(寄存器值等),并加载下一个线程的现场。这个操作有成本,如果时间片设置得太短,切换过于频繁,系统效率反而会下降,所以说内核态的开销往往是非常大的
    • 时间片大小的权衡
      • 太长:交互性变差,其他线程等待时间过长。
      • 太短:吞吐量下降,大量时间花在切换线程上。
      • 操作系统会根据系统负载和线程类型(是交互式I/O密集型还是计算密集型)进行动态调整。
    • 与多核的关系 :在多核CPU上,每个核心都有自己的计时器和调度队列 ,可以独立地为其上运行的线程分配和管理时间片。因此,多个线程可以在不同的核心上真正并行地消耗各自的时间片。

多线程的概念

多线程是什么有什么好处

  • 线程(Thread)是一个程序内部的一条执行流程,在java中main函数 就是一个线程,一般称之为主线程 ,程序中如果只有一条执行流程,那这个程序就是单线程程序 。通过一些方式在程序中同时启动多条执行流程,那么这个程序就是多线程程序
  • 了解文线程和线程并行的概念后,就可以来体验一下线程了
  • 多线程用在哪里,有什么好处
    • 可以多个用户同时执行任务,比如12306处理多个抢票请求、百度网盘客户端的下载与上传(下载一个线程,上传一个线程可以同时进行)、消息通信、淘宝处理多个购物请求、都离不开多线程技术。

创建多线程

方式1:继承Thread

  • 实现步骤

    • 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
    • 创建MyThread类的对象
    • 调用线程对象的start()方法启动线程(启动后还是执行run方法的)
  • 代码示例:

    java 复制代码
    package com.itheima._01创建线程方式;
    public class Demo011 {
    
        public static void main(String[] args) {
            System.out.println("main主线程运行开始。。。");
    
             //2、创建MyThread类的对象
            MyThread myThread = new MyThread();
    
            // 3、调用线程对象的start方法启动线程(启动后还是执行run方法的)
            myThread.start();
            //注意1:myThread.run() 就是一个普通类调用了一个普通方法,就没有启动子线程运行
            //注意2:main线程任务要放在子线程启动后执行,这样就是多线程做任务。
            System.out.println("main主线程运行结束。。。");
                }
            }
    
            // 1、定义一个子类MyThread继承线程类java.lang.Thread,重写runO方法
            class MyThread extends Thread{
                @Override
                public void run() {
                    //线程里面执行的流程
            System.out.println("MyThread线程运行了。。。");
        }
    }
    • 注意事项:
      • myThread.run() 就是一个普通类调用了一个普通方法,就没有启动子线程运行,这样只会按顺序运行run方法,是单线程程序
      • main线程任务要放在子线程启动后执行,这样可以尽早启动子线程,而不是一直运行主线程。
  • 方式一优缺点:

    • 优点:编码简单
    • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。(不满足地耦合,高内聚的架构范式)

方式2:实现Runnable接口

实现步骤
  • 定义一个线程任务类MyRunnable实现Runnable接口,重写run方法

  • 创建MyRunnable任务对象

  • 把MyRunnable任务对象交给Thread处理。

    Thread类提供的构造器 说明
    public Thread(Runnable target) 封装Runnable对象成为线程对象
  • 调用线程对象的start方法启动线程

代码示例
java 复制代码
public class Demo012 {

    public static void main(String[] args) {
        System.out.println("main主线程运行开始。。。");

         // 2、创建MyRunnable任务对象
        MyRunnable myRunnable = new MyRunnable();

        //3.把MyRunnable任务对象交给Thread处理。
        Thread thread = new Thread(myRunnable);

        // 4、调用线程对象的start方法启动线程
        thread.start();
        // 注意这里不可以调用myThread.run()否则只是调用方法,不会启动线程
        // 目前是两这个线程同时运行的,一个是main主线程,一个是thread线程,
        // 如果cpu核心不够有,则是多个线程轮片执行,也有可能分配的核心是够的,
        //则是在不同的核心并行执行,具体情况要看当前cpu的占有有多大,
        //cpu分配时间片的机制是怎样的,但无论如何都可以看作thread线程和main主线程在并行执行,
        //因为即使是轮片执行感知也很小的
        System.out.println("main主线程继续开始。。。");
        //使用匿名内部类方式
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Runnable匿名内部类线程执行了。。。");
            }
        });
        thread1.start();

        //使用lambda方式
        Thread thread2 = new Thread(() -> System.out.println("Runnable lambda 线程执行了。。。"));
        thread2.start();
        System.out.println("main主线程运行结束。。。");
    }
}

// 1、定义一个线程任务类MyRunnable实现Runnable接口,重写runO方法
class MyRunnable implements Runnable{

    @Override
    public void run() {
        System.out.println("MyRunnable线程执行了。。。");
    }
}
优缺点
  • 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强。
  • 缺点:需要多一个Runnable对象。

体验主线程和子线程并发运行

  • 代码:主线程启动一个子线程,两个线程各跑一个循环查看效果

    java 复制代码
    public class Demo012 {
        public static void main(String[] args) {
            new Thread(() -> {
                for (int i = 0; i < 10; i++){
                    System.out.println("子线程运行,第" + i + "次");
                    try {
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }).start();
            System.out.println("子线程创建完毕。。。");
    
            System.out.println("main主线程开始运行。。。");
            for (int i = 0; i < 10; i++) {
                System.out.println("main主线程运行,第" + i + "次");
                try {
                    Thread.sleep(200);
                }catch (InterruptedException e){
                    throw new RuntimeException(e);
                }
            }
            System.out.println("main主线程运行结束。。。");
        }
    }
  • 运行后结果如下图所示,可以看到主线程启动子线程后两个线程共同运行,并争夺控制台输出

方式3:实现Callable接口和FutrueTask类

实现步骤
  • 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
  • 创建Callable接口实现类对象
  • 创建任务对象,把Callable类型的对象封装成FutureTask(线程任务对象)。
  • 把线程任务对象交给Thread对象。
  • 调用Thread对象的start方法启动线程。
语法
  • FutureTask的API

    • 构造器
    FutureTask提供的构造器 说明
    public FutureTask<>(Callable call) 把Callable对象封装成FutureTask对象。
    • 方法
    FutureTask提供的方法 说明
    public V get() throws Exception 获取线程执行call方法返回的结果。
  • 代码

    • 为了简化实体类可以引入Lombok
    • 实现Callable接口,封装好线程要做的事情和返回的数据
    java 复制代码
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    
    @Data //生成getter/setter/toString/hashCode/equals等方法
    @AllArgsConstructor //生成全参构造器,注意一定要有成员属性
    @NoArgsConstructor //生成无参构造器
    class MyCallable implements Callable<Long>{
    
        private Long start; //计算开始值
        private Long end; //计算结束值
    
        @Override
        public Long call() throws Exception {
            System.out.printf("MyCallable线程开始计算,从%d到%d的求和计算开始了%n",start,end);
            Long sum =0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            System.out.printf("MyCallable线程开始计算,从%d到%d的求和计算结束了%n",start,end);
            return sum;
        }
    }
    • 创建线程
    java 复制代码
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    
    
    public class Demo013 {
    
        public static void main(String[] args) throws Exception {
    
            System.out.println("main主线程运行开始。。。");          
            // 2、创建Callable接口实现类对象
            MyCallable myCallable = new MyCallable(1L, 100L);
    
            //3、创建任务对象,把Callable类型的对象封装成FutureTask(线程任务对象)。
            FutureTask<Long> longFutureTask = new FutureTask<>(myCallable);
    
            // 4、把线程任务对象交给Thread对象。
            Thread thread = new Thread(longFutureTask);
    
            // 5、调用Thread对象的start方法启动线程。
            thread.start();
    
            //6.获取结果
            Long result = longFutureTask.get(); 
            //这个get方法会阻塞等待子线程运行完成后才会往下执行。
            System.out.println("子线程计算结果:"+result);
            System.out.println("main主线程运行结束。。。");
        }
    }
  • 优缺点

    • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
    • 缺点:编码复杂一点。

三种方式对比

方式 优点 缺点
继承Thread类 编程比较简单,可以直接使用Thread类中的方法 扩展性较差,不能再继承其他的类,不能返回线程执行的结果
实现Runnable接口 扩展性强,实现该接口的同时还可以继承其他的类。 编程相对复杂,不能返回线程执行的结果
实现Callable接口--->包装为FutureTask对象 扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果 编程相对复杂

多线程运行大任务量计算对比

  • 实现Callable

    java 复制代码
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.util.concurrent.Callable;
    import java.util.concurrent.FutureTask;
    
    @Data //生成getter/setter/toString/hashCode/equals等方法
    @AllArgsConstructor //生成全参构造器,注意一定要有成员属性
    @NoArgsConstructor //生成无参构造器
    class MyCallable implements Callable<Long>{
    
        private Long start; //计算开始值
        private Long end; //计算结束值
    
        @Override
        public Long call() throws Exception {
            System.out.printf("MyCallable线程开始计算,从%d到%d的求和计算开始了%n",start,end);
            Long sum =0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            System.out.printf("MyCallable线程开始计算,从%d到%d的求和计算结束了%n",start,end);
            return sum;
        }
    }
  • 累加1~9000000,分别在一个线程内做完,和按照数字区间拆分为三个线程做完

    java 复制代码
    import java.util.concurrent.FutureTask;
    public class Demo014 {
        public static void main(String[] args) throws Exception {
            //目标:实现多线程任务:计算1~9000000累加的和
             //使用1个线程计算1~9000000
            Long startTime0 = System.currentTimeMillis(); 
            //获取当前时间毫秒数(当前时间-1970年1月1号时间毫秒数差)
    
             //单个线程
            MyCallable mc = new MyCallable(1l, 9000000l);
                    FutureTask<Long> futureTask = new FutureTask(mc);
                    Thread thread = new Thread(futureTask);
                    thread.start();
                    Long sum = futureTask.get();
                    System.out.println("1个线程求和结果:"+sum);
                    Long endTime0 = System.currentTimeMillis();
                    System.out.println("单线程完成任务耗时:"+(endTime0-startTime0)+"毫秒");
    
             //使用3个线程计算
             //线程1:计算1~3000000累加的和
             //线程2:计算3000001~6000000累加的和
             //线程3:计算6000001~9000000累加的和
    
            Long startTime = System.currentTimeMillis();
    
            //线程1
            MyCallable mc1 = new MyCallable(1l, 3000000l);
                    FutureTask<Long> futureTask1 = new FutureTask(mc1);
                    Thread thread1 = new Thread(futureTask1);
                    thread1.start();
            //线程2
            MyCallable mc2 = new MyCallable(3000001l, 6000000l);
                    FutureTask<Long> futureTask2 = new FutureTask(mc2);
                    Thread thread2 = new Thread(futureTask2);
                    thread2.start();
            //线程3
            MyCallable mc3 = new MyCallable(6000001l, 9000000l);
                    FutureTask<Long> futureTask3 = new FutureTask(mc3);
                    Thread thread3 = new Thread(futureTask3);
                    thread3.start();
    
            //7.获取线程执行结果
            Long sum1 = futureTask1.get(); //阻塞等待当前线程执行完毕后获取结果
            Long sum2 = futureTask2.get();
                    Long sum3 = futureTask3.get();
                    System.out.println("子线程求和结果:"+(sum1+sum2+sum3));
                    Long endTime = System.currentTimeMillis();
                    System.out.println("3个线程完成任务耗时:"+(endTime-startTime)+"毫秒");
        }
    }
  • 运行结果如下图所示,单线程花费139毫秒,多线程花费了72毫秒,多线程比单线程减少了将近一半的时间。

线程状态(线程生命周期)

  • 基于线程生命周期的演变,可以将其划分为以下六种核心状态。这些状态定义了线程在不同阶段的行为模式和资源占用情况,如下图所示。
1. NEW (新建)
  • 状态描述 :线程对象已经被创建(例如通过 new Thread()),但还没有调用 start()方法。此时线程仅仅是一个普通的Java对象,并没有被操作系统真正视为一个执行单元,因此不占用CPU资源。
  • 状态转换 :一旦调用了 start()方法,线程便进入 READY (就绪) 状态。
2. RUNNABLE (可运行):
  • 这是一个复合状态,包含两个子状态。处于此状态的线程已经准备好执行,正在等待操作系统的调度以获得CPU时间片。
  • Java线程状态是JVM层面的抽象,它将底层操作系统中的"运行"、"就绪"甚至"IO阻塞"都统一视为RUNNABLE,因此你看到线程"就绪时候"时,它在内核线程可能正在内核态被阻塞等待IO、网络资源,当然也有可能资源获取完毕在用户态等待CPU轮片。
  • READY (就绪)
    • 特征:线程已经具备了运行的所有条件,存在于系统的线程队列中。
    • 转换条件 :当操作系统的线程调度器分配给该线程CPU核心使用权(即获得CPU时间片)且IO、网络等资源准备完毕时,线程进入 RUNNING状态。
  • RUNNING (运行)
    • 特征:线程正在CPU上执行代码。
    • 转换条件
      • 主动让出 :线程执行完毕(run()方法正常结束),或因发生异常/错误而终止,进入 TERMINATED 状态。
      • 被动切换 :由于时间片用完或被更高优先级的线程抢占,失去CPU核心使用权或IO、网络等资源出现异常,重新回到 READY 状态。
      • 进入等待 :线程在执行过程中遇到阻塞操作,进入 BLOCKED , WAITINGTIMED_WAITING 状态。
3. BLOCKED (阻塞)
  • 状态描述 :线程正在等待获取一个 (例如 synchronized代码块或方法)。
  • 典型场景:当线程试图进入一个被其他线程持有的同步代码块时,它会被放入锁的等待队列中,暂停执行并释放CPU。
  • 转换条件 :当持有锁的线程释放了该锁,且当前线程成功抢占到这个锁时,线程会重新回到 READY (就绪) 状态。
4. WAITING (无限期等待)
  • 状态描述:线程处于一种"死等"的状态,必须依赖其他线程的特定动作才能被唤醒,否则会一直等待下去。
  • 触发方式
    • 调用无超时的 Object.wait()方法。
    • 调用无超时的 Thread.join()方法(等待目标线程终止)。
  • 转换条件 :必须由其他线程调用 Object.notify()Object.notifyAll()来唤醒,或者等待的目标线程执行完毕(针对join),唤醒后进入 READY (就绪) 状态。
5. TIMED_WAITING (限期等待 / 定时等待)
  • 状态描述:与 WAITING 类似,但这种等待是有时间限制的。线程在指定的时间段内等待某个条件满足。

  • 触发方式 :调用带有超时参数的方法,如 Thread.sleep(timeout)Object.wait(timeout)Thread.join(timeout)

  • 转换条件被动唤醒:在设定的超时时间到达之前,被其他线程唤醒(notify/notifyAll)。

  • 超时自动唤醒:设定的时间到期后,线程会自动醒来。

  • 无论哪种情况,唤醒后都会进入 READY (就绪) 状态。

6. TERMINATED (终止)
  • 状态描述:线程的生命周期已经彻底结束。
  • 特征run()方法内的逻辑已经完全执行完毕。处于此状态的线程不可再次启动,如果尝试再次调用 start()方法,会抛出 IllegalThreadStateException异常。

线程常用的方法

  • 语法

    • Thread常用构造器
    Thread提供的常见构造器 说明
    public Thread(String name) 可以为当前线程指定名称
    public Thread(Runnable target) 封装Runnable对象成为线程对象
    public Thread(Runnable target, String name) 封装Runnable对象成为线程对象,并指定线程名称
    • Thread常用方法
    Thread提供的常用方法 说明
    public void run() 线程的任务方法
    public void start() 启动线程
    public String getName() 获取当前线程的名称,线程名称默认是Thread-索引
    public void setName(String name) 为线程设置名称
    public static Thread currentThread() 获取当前执行的线程对象
    public static void sleep(long time) 让当前执行的线程休眠多少毫秒后,再继续执行
    public final void join()... 让调用当前这个方法的线程先执行完!
  • 为线程命名代码示例

    java 复制代码
    public class Demo {
        public static void main(String[] args) {
            // 创建线程,继承Thread类
            new MyThread("aa").start();
            Thread myThread = new MyThread();
            myThread.setName("aa2");
            myThread.start();
            // 创建线程,通过实现Runnable接口
            new Thread(()-> System.out.println(Thread.currentThread().getName()+"子线程"), "bb").start();
            // 创建线程,通过实现Callable接口
            new Thread(new FutureTask(() -> {
                System.out.println(Thread.currentThread().getName() + "子线程");
                return null;// 无返回值直接返回基类的空指针就好了
            }),"cc").start();
            System.out.println(Thread.currentThread().getName()+"主线程");
        }
    }
    
    class MyThread extends Thread {
        public MyThread() {
        }
        public MyThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "子线程");
        }
    }
  • join示例

    • 使用join前,两线程并发执行

      java 复制代码
      public class Demo2 {
          public static void main(String[] args) {
              new Thread(()->{
                  for (int i = 0; i < 10; i++){
                      System.out.println("子线程运行,第" + i + "次");
                      try{
                          Thread.sleep(20);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                  }
              }).start();
      
              for (int i = 0; i < 10; i++) {
                  System.out.println("主要线程运行,第" + i + "次");
                  try{
                      Thread.sleep(20);
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }
              }
          }
      }
    • 使用join后

      java 复制代码
      public class Demo2 {
          public static void main(String[] args) throws InterruptedException {
              Thread thread = new Thread(()->{
                  for (int i = 0; i < 10; i++){
                      System.out.println("子线程运行,第" + i + "次");
                      try{
                          Thread.sleep(20);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                  }
              });
              thread.start();
      
              for (int i = 0; i < 10; i++) {
                  if (i == 3){
                      thread.join();// 阻塞当前线程,等待子线程运行完毕
                  }
                  System.out.println("主要线程运行,第" + i + "次");
                  try{
                      Thread.sleep(20);
                  } catch (InterruptedException e) {
                      throw new RuntimeException(e);
                  }
              }
          }
      }
    • 可以看到当main线程循环第三次后满足条件,子线程调用join,强行阻塞 main线程,直到子线程运行完毕。

  • Thread的其他方法说明

    • Thread类还提供了诸如:yield、interrupt、守护线程、线程优先级等线程的控制方法,在开发中很少使用,这些方法会后续需要用到的时候再讲解。

线程安全(锁)

线程安全问题介绍

什么是线程安全问题?

  • 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
  • 取钱的线程安全问题场景 :小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?
    • 可以把两个取钱动作都看作并发线程,它们都会判断余额是否充足,并向共享账户(共享的数据资源)进行扣款操作,如下图先进行判断余额是否充足
    • 因为是共享资源,所以这两个并发的线程是同时操作并修改这个共享的资源的,由于cpu轮片执行有的线程操作的块则会先减去
    • 获得cpu轮片较慢的则会后减去,当然同时减去也是一样的都会通过业务逻辑校验并做两次减去操作(如果恰巧同时操作,即调用的cpu不同核但轮片时间相同,那么余额返回的是零,因为同时操作减去1000这个动作和同时打入内存中,两个操作会合并,最后余额同时变为零或-10000)

模拟取钱多线程不安全

  • 模拟线程安全问题的场景:取钱

    • 需求:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,用程序模拟两人同时取钱10万元。
    • 分析:
      • 怎样描述小明和小红的共同账户呢?设计一个账户类,创建一个账户对象,代表2人的共享账户
      • 怎样模拟两人同时取钱?设计一个线程类,创建并启动两个线程,在线程的run方法中调用账户的取钱方法
    • 创建账户类
    java 复制代码
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    
    import java.math.BigDecimal;
    
    /**
     * @Description 银行账户
     */
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Account {
        private String cardNo;//账号
        private BigDecimal money;//银行卡余额,在金融领域(和钱相关)推荐使用BigDecimal类型,数据精准
    
    }
    • 创建ATM实体类,,为了让另一个人余额判断成功,先不要立即更新余额,使用Thread.sleep(200);休眠一会
    java 复制代码
    package src.bank;
    import java.math.BigDecimal;
    public class ATM {
        public void drawMoney(Account account, BigDecimal money){
            if(account.getMoney().compareTo(money)<0){
                System.out.println(Thread.currentThread().getName()+":取钱失败,余额不足!");
            }else{
                try {
                    //为了让另一个人余额判断成功,先不要立减更新余额,休眠一会
                    Thread.sleep(200);
                    // Thread.sleep(new Random().nextInt(1000));
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                //余额充足,更新余额
                BigDecimal balance = account.getMoney().subtract(money);
                //余额-取钱金额
                account.setMoney(balance);
                //打印:线程名字:取钱成功,最新余额xxx
                System.out.println(Thread.currentThread().getName()+":取钱成功,最新余额 "+account.getMoney()+" 元");
            }
        }
    }
    • 在主类中创建两个并发的线程,会发生并发问题
    java 复制代码
    package src.bank;
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.math.BigDecimal;
    
    public class Main {
        public static void main(String[] args) {
            Account account = new Account("ICBC-128665", BigDecimal.valueOf(100000.0));
            ATM atm1 = new ATM();
            ATM atm2 = new ATM();
            new Thread(new atmRunnable(account,atm1),"小明").start();
            new Thread(new atmRunnable(account,atm2), "小红").start();
        }
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    class atmRunnable implements Runnable{
        private Account account;
        private ATM atm;
        @Override
        public void run() {
            atm.drawMoney(account, BigDecimal.valueOf(100000.0));
        }
    }
    • 运行结果,我的CPU比较好,两个线程都申请到了不同CUP核心的时间片,不需要再同一个CPU轮片,减1000的操作同时被打出去了,所以余额为零,但这也是一个错误的运行结果,其中一个线程本因再逻辑校验时扣款失败
    • 可以用生成随机数让两个线程运行速度不一致,来达成另一种运行效果,当然这仍然是错误的结果
    java 复制代码
     import java.math.BigDecimal;
     import java.util.Random;
     public class ATM {
         public void drawMoney(Account account, BigDecimal money){
             if(account.getMoney().compareTo(money)<0){
                 System.out.println(Thread.currentThread().getName()+":取钱失败,余额不足!");
             }else{
                 try {
                     //为了让另一个人余额判断成功,先不要立减更新余额,休眠一会
                     // Thread.sleep(200);
                     Thread.sleep(new Random().nextInt(1000));
                 } catch (InterruptedException e) {
                     throw new RuntimeException(e);
                 }
                 //余额充足,更新余额
                 BigDecimal balance = account.getMoney().subtract(money);//余额-取钱金额
                 account.setMoney(balance);
                 //打印:线程名字:取钱成功,最新余额xxx
                 System.out.println(Thread.currentThread().getName()+":取钱成功,最新余额 "+account.getMoney()+" 元");
             }
         }
     }

线程同步解决线程安全问题

线程同步介绍介绍

  • 线程同步是线程安全问题的解决方案。
  • 线程同步的核心思想在于让多个线程先后依次访问共享资源,这样就可以避免出现线程安全问题。(关键资源的代码多线程不同时执行)
  • 如下图所示,只有小明线程执行完毕后,小红线程才能执行。
  • 线程同步的常见方案
    • 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

三种线程同步的方式

线程同步:方式1-同步代码块

锁对象
  • 作用

    • 把访问共享资源的核心代码给上锁,以此保证线程安全
  • 语法

    java 复制代码
    synchronized(同步锁) {
              访问共享资源的核心代码
     }
  • 原理

    • 每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
  • 同步锁的注意事项

    • 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
  • 代码

    • 只需改动ATM类,其他无需改动
    java 复制代码
    import java.math.BigDecimal;
    import java.util.Random;
    public class ATM {
        public void drawMoney(Account account, BigDecimal money){
            //解决多线程并发安全问题,方式1:同步代码块
            //语法:synchronized(同步锁){存在不安全的代码}
            //同步锁说明:
            //      共享资源对象(推荐)
            //      this,调用当前方法的对象,当前为ATM类对象(在这个例子中是锁不住的,必须改造)
            //      类名.class,类对象,全局只有一份,
            synchronized (account){
                if(account.getMoney().compareTo(money)<0){
                    System.out.println(Thread.currentThread().getName()+":取钱失败,余额不足!");
                }else{
                    try {
                        //为了让另一个人余额判断成功,先不要立减更新余额,休眠一会
                        // Thread.sleep(200);
                        Thread.sleep(new Random().nextInt(1000));
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    //余额充足,更新余额
                    BigDecimal balance = account.getMoney().subtract(money);//余额-取钱金额
                    account.setMoney(balance);
                    //打印:线程名字:取钱成功,最新余额xxx
                    System.out.println(Thread.currentThread().getName()+":取钱成功,最新余额 "+account.getMoney()+" 元");
                }
            }
    
        }
    }
  • 锁不同对象的区别

    • 锁共享资源对象 ,最推荐,同一个资源对象会被锁上,共享同一个资源的进程会进入锁队列,不同资源对象的进程的锁队列是并发运行的比如,下方代码所示,在锁Accout对象的情况下Thread-小明Thread-小红在一个锁队列中 并和 Thread-小李Thread-小张组成的所队列 并发运行。
    java 复制代码
     public class ATM {
       public void drawMoney(Account account, BigDecimal money){
        synchronized (account){
                ...
        }
    java 复制代码
    public class Main {
        public static void main(String[] args) {
            // 资源1
            Account account = new Account("ICBC-128665", BigDecimal.valueOf(100000.0));
            ATM atm1 = new ATM();
            ATM atm2 = new ATM();
            new Thread(new atmRunnable(account,atm1),"小明").start();
            new Thread(new atmRunnable(account,atm2), "小红").start();
            
            // 资源2
            Account account2 = new Account("ICBC-128665", BigDecimal.valueOf(100000.0));
            ATM atm3 = new ATM();
            ATM atm4 = new ATM();
            new Thread(new atmRunnable(account2,atm3),"小李").start();
            new Thread(new atmRunnable(account2,atm4), "小张").start();
        }
    }
    • 锁类 ,即为锁类名.class,这种情况会把所有对该类对象的操作都锁住,如下方代码所示,这种情况下所有线程都会共用一把锁,不会并发
    java 复制代码
    public class ATM {
        public void drawMoney(Account account, BigDecimal money){
            synchronized (){
                 ...
            }
      }
    java 复制代码
    public class Main {
        public static void main(String[] args) {
            // 资源1
            Account account = new Account("ICBC-128665", BigDecimal.valueOf(100000.0));
            ATM atm1 = new ATM();
            ATM atm2 = new ATM();
            new Thread(new atmRunnable(account,atm1),"小明").start();
            new Thread(new atmRunnable(account,atm2), "小红").start();
            
            // 资源2
            Account account2 = new Account("ICBC-128665", BigDecimal.valueOf(100000.0));
            ATM atm3 = new ATM();
            ATM atm4 = new ATM();
            new Thread(new atmRunnable(account2,atm3),"小李").start();
            new Thread(new atmRunnable(account2,atm4), "小张").start();
        }
    }
    • 锁this ,即锁当前调用者,在我们现在的例子中this为ATM对象,每个ATM对象都对应一个线程,相当于没锁,因此锁this再这个例子中是失效的 ,如果要用锁this方式,需要重构代码,将危险操作和危险资源类耦合。
重构-锁this
  • 先前的例子中我们无法将this设置为同步锁,因为this指向的是ATM这个操作资源的工具类,而不是account资源类,固我们要将代码进行重构,将对于共享资源操作函数从工具类ATM中移除(解耦),再将对共享资源操作的函数移入到共享资源实体类中作为成员方法封装(耦合)
  • 以下是重构的代码,本文后面的例子都使用这个重构后的代码了(生产中也推荐这样重构,危险资源-危险操作进行耦合,这也一种编码范式)
  • ATM类
java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
* @Description ATM
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ATM implements Runnable{

    private Account account; //银行卡账户
    private BigDecimal drawMoney; //取钱的金额

    @Override
    public void run() {
        //调用取钱方法
        account.drawMoney(drawMoney);//drawMoney移动到共享资源中
        // 存钱方法
    }
}
  • Account类
java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;

/**
* @Description 银行账户
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Account {

    private String cardNo; //账号
    private BigDecimal money; 
    //银行卡余额,在金融领域(和钱相关)推荐使用BigDecimal类型,数据精准

     /**
    * 取钱方法
    * @param drawMoney
    */
    public void drawMoney(BigDecimal drawMoney){

        // 解决多线程并发安全问题方式1:同步代码块
        // 语法:synchronized (同步锁对象){存在不安全的代码}
        // 同步锁对象说明:执行竞争的线程的同步锁对象必须是共享的资源对象
        // 方式1:特定对象, 例如new Object()对象,小红和小明不共享object,所以不适用当前场景
        // 方式2:this, 调用当前方法的对象,这里就是Account,并且小红和小明共享同一个账户,推荐,锁的范围小
        // 假设:小明和小红共享一个账户 Account1对象,在Account1这个锁里面小明和小红必须依次执行,但是小明和小张或小李是可以并发执行的
        // 假设:小张和小李共享一个账户 Account2对象,在Account2这个所里面小张和小李必须依次执行,但是小张和小明或小红是可以并发执行的
        // 方式3:类.class, 类字节码对象,全局只有一份,所有线程都共享这一个对象,都竞争这一个锁,锁范围大
        // 如果使用字节码对象,上面4个人都不可以并发,小明和小张或小李不可以并发,小张和小明或小红部可以并发
        synchronized (this) {
             //getMoney().compareTo(drawMoney) 返回整数 代表余额大
             //getMoney().compareTo(drawMoney) 返回0 代表相等
             //getMoney().compareTo(drawMoney) 返回负数 代表余额小
            if(getMoney().compareTo(drawMoney)<0){
                        //输出:线程名字:取钱失败,余额不足!
            System.out.println(Thread.currentThread().getName()+":取钱失败,余额不足!");
                    }else{
                        try {
                            //为了让另一个人余额判断成功,先不要立减更新余额,休眠一会
                            Thread.sleep(200);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }

            //余额充足,更新余额
            BigDecimal balance = getMoney().subtract(drawMoney); //余额-取钱金额
            setMoney(balance);
            //打印:线程名字:取钱成功,最新余额xxx
            System.out.println(Thread.currentThread().getName()+":取钱成功,最新余额 "+getMoney()+" 元");
            }
        }
    }
  • 主类
java 复制代码
import java.math.BigDecimal;
/**
* @Description Main
*/
public class Main {

    public static void main(String[] args) {
        //创建一个共享账户account1
        Account account1 = new Account("ICBC-128665", new BigDecimal("100000"));

        //创建ATM机对象,并且启动线程(运行里面取钱的方法)
        //小明线程:取钱执行流程
        new Thread(new ATM(account1,new BigDecimal("100000")),"小明").start();
        //小红线程:取钱执行流程
        new Thread(new ATM(account1,new BigDecimal("100000")),"小红").start();
        
        //创建一个共享账户account2
        Account account2 = new Account("ICBC-128665", new BigDecimal("100000")
        //创建ATM机对象,并且启动线程(运行里面取钱的方法)
        //小张线程:取钱执行流程
        new Thread(new ATM(account1,new BigDecimal("100000")),"小张").start();
        //小李线程:取钱执行流程
        new Thread(new ATM(account1,new BigDecimal("100000")),"小李").start();
        
        
    }
}
  • 这种写法被执行危险操作的共享资源对象就是类本身,而操作共享资源的方法就是共享资源类的成员方法,所以锁this生效,而锁对象则需要从方法中传入Account对象才能生效(一般不会这么做)
    • this, 调用当前方法的对象,这里就是Account,并且小红和小明共享同一个账户,推荐,锁的范围小

      • 假设:小明和小红共享一个账户 Account1对象,在Account1这个锁里面小明和小红必须依次执行,但是小明和小张或小李是可以并发执行的
      • 假设:小张和小李共享一个账户 Account2对象,在Account2这个所里面小张和小李必须依次执行,但是小张和小明或小红是可以并发执行的*
    • 类.class, 类字节码对象,全局只有一份,所有线程都共享这一个对象,都竞争这一个锁,锁范围大

      • 如果使用字节码对象,上面4个人都不可以并发,小明和小张或小李不可以并发,小张和小明或小红部可以并发
    • 锁对象,需要传入共享的Account对象,但drwMoney就在Account中,这个对象就是this,所以硬要写的话就是传入调用的Account对象,但这种写法非常诡异 ,最好不要写,会被同事枪毙的 :) ...

      java 复制代码
      import lombok.AllArgsConstructor;
      import lombok.Data;
      import lombok.NoArgsConstructor;
      import java.math.BigDecimal;
      @Data
      @NoArgsConstructor
      @AllArgsConstructor
      public class Account {
          private String cardNo;
          private BigDecimal money;
          public void drawMoney(BigDecimal drawMoney,Account account){
              synchronized (account){
                  if(getMoney().compareTo(drawMoney)<0){
                      //输出:线程名字:取钱失败,余额不足!
                      System.out.println(Thread.currentThread().getName()+":取钱失败,余额不足!");
                  }else{
                      try {
                          Thread.sleep(200);
                      } catch (InterruptedException e) {
                          throw new RuntimeException(e);
                      }
                      //余额充足,更新余额
                      BigDecimal balance = getMoney().subtract(drawMoney);//余额-取钱金额
                      setMoney(balance);
                      //打印:线程名字:取钱成功,最新余额xxx
                      System.out.println(Thread.currentThread().getName()+":取钱成功,最新余额 "+getMoney()+" 元");
                  }
              }
          }
      }
总结
  • 同步代码块是如何实现线程安全的?
    • 对出现问题的核心代码使用synchronized进行加锁
    • 每次只能一个线程占锁进入访问
  • 同步代码块的同步锁对象有什么要求?
    • 对于共享对象外的方法建议使用锁共享资源对象(锁对象)
    • 对于共享对象成员方法建议使用this作为锁对象。(锁this)
    • 对于静态方法静态资源建议使用字节码(类名.class)对象作为锁对象。

线程同步:方式2-同步方法

  • 作用

    • 把访问共享资源的核心代码给上锁,以此保证线程安全
  • 原理

    • 每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。
    • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码。
    • 如果方法是实例方法:同步方法默认用this作为的锁对象。
    • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象。
  • 语法

    java 复制代码
    修饰符 synchronized 返回值类型 方法名称(形参列表) {
      操作共享资源的代码
     }
  • 代码示例

    java 复制代码
    package com.itheima._08解决多线程不安全_方式2同步方法;
    
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.math.BigDecimal;
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public class Account {
        private String cardNo; //账号
        private BigDecimal money; //银行卡余额,,在金融领域(和钱相关)推荐使用BigDecimal类型,数据精准
         /**
        * 取钱方法
        * @param drawMoney
        */
        public synchronized void drawMoney(BigDecimal drawMoney){
    
          //解决多线程并发安全问题方式2:同步方法
          //语法: public  [static ] synchronized  方法名(形参列表){存在不安全的代码} 
          //同步锁说明:执行竞争的线程的同步锁必须是共享的资源对象(竞争线程操作的资源对象只有1个) 
          //    synchronized放在实例方法, 同步锁对象就是this
          //    synchronized放在静态方法, 同步锁对象就是类对象(字节码对象),这里是ATM.class,全局只有一份,所有线程都竞争这一个锁,锁范围大
          //Shift+Tab 向左缩进
          if(getMoney().compareTo(drawMoney)<0){
                    //输出:线程名字:取钱失败,余额不足!
                    System.out.println(Thread.currentThread().getName()+":取钱失败,余额不足!");
              }else{
                    try {
                        //为了让另一个人余额判断成功,先不要立减更新余额,休眠一会
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
              }
                //余额充足,更新余额
                BigDecimal balance = getMoney().subtract(drawMoney); //余额-取钱金额
                setMoney(balance);
                //打印:线程名字:取钱成功,最新余额xxx
                System.out.println(Thread.currentThread().getName()+":取钱成功,最新余额 "+getMoney()+" 元");
            }
    
        }
    }

同步代码块好还是同步方法好?

  • 范围上:同步代码块锁的范围更小,同步方法锁的范围更大
  • 可读性:同步方法更好

线程同步:方式3-Lock锁

  • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。
语法
  • 构造器

    构造器 说明
    public ReentrantLock() 获得Lock锁的实现类对象
  • Lock的常用方法

    方法名称 说明
    void lock() 获得锁
    void unlock() 释放锁
  • 锁this还是类还是对象?

    • Lock只能在危险操作作为共享资源类成员方法的情况下使用
    • 一定要使用final修饰Lock对象,否则锁可能被别人篡改
    • 如果使用final static修饰Lock锁的实现类对象,则默认锁类.class
    • 如果不使用static则默认锁this
代码示例
java 复制代码
package com.itheima._09解决多线程不安全_方式3Lock锁;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.math.BigDecimal;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
* @Description 银行账户
* @Author songyu
* @Date 2025-12-05  16:35
*/
@Data
@NoArgsConstructor
 @AllArgsConstructor  //final成员不会生成构造器参数,所以下面手动参数构造器注释即可
public class Account {

    private String cardNo; //账号
    private BigDecimal money; //银行卡余额,,在金融领域(和钱相关)推荐使用BigDecimal类型,数据精准
    private final  Lock lock = new ReentrantLock();
    //使用final、static修饰的成员变量lombok(小辣椒)不会在生成全参构造器的时候不会考虑这个成员变量。

     /** 
     * 取钱方法
     *  @param drawMoney
     */ 
     public void drawMoney(BigDecimal drawMoney){
        try {
            lock.lock(); //上锁
            if(getMoney().compareTo(drawMoney)<0){
                //输出:线程名字:取钱失败,余额不足!
                System.out.println(Thread.currentThread().getName()+":取钱失败,余额不足!");
                        }else{
                 try {
                    //为了让另一个人余额判断成功,先不要立减更新余额,休眠一会
                    Thread.sleep(200);
                 } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                 }
            //余额充足,更新余额
            BigDecimal balance = getMoney().subtract(drawMoney); //余额-取钱金额
            setMoney(balance);
            //打印:线程名字:取钱成功,最新余额xxx
             System.out.println(Thread.currentThread().getName()+":取钱成功,最新余额 "+getMoney()+" 元");
            }
        } finally {
            lock.unlock(); //释放锁
        }
    }
}
lock锁功能说明(公平锁、锁终断、锁超时):
  • 公平锁

    • 会有一个线程队列根据先后获取锁的顺序依次让对应线程获取锁资源 (new ReentrantLock(true) 公平锁,如果没有参数就是非公平锁),synchronized没有这个功能
  • 锁中断

    • 中断锁的目的是不希望线程阻塞等待太久可以进行中断,让队列中下一个线程获得锁并运行,获取锁不用 l.lock()而是使用lock.lockInterruptibly();最后通过thread对象.interrupt();
  • 锁超时

    • 获取锁成功或失败立刻返回,如果不成功可以执行不成功的逻辑(替代方案),获取锁不用 l.lock()而是使用l.tryLock()
    • lock.tryLock(10,TimeUnit.SECONDS) 等待10秒获取锁,如果获取不到锁,则返回false,如果获取到锁,则返回true
    • lock.tryLock() 立刻获取锁,如果获取不到锁,则返回false,如果获取到锁,则返回true
    java 复制代码
    if(lock.tryLock(10,TimeUnit.SECONDS)){
        try{  
            //线程不安全的代码
        }finally{
            l.unlock();
    }else{
        //替代方案的代码
    }
  • 同步锁说明:

    • 执行竞争的线程的同步锁必须是共享的资源对象(竞争线程操作的资源对象只有1个)
      • 一定要在finally中释放锁
      • l是实例对象, 同步锁对象就是this
      • l是静态对象, 同步锁对象就是类对象(字节码对象),全局只有一份,所有线程都竞争这一个锁,锁范围大

线程池

什么是线程池

  • 为什么要有线程池
    • 计算机不能无限创建线程,因为线程相当于创建一个对象,需要开辟内存空间,创建线程对象过多会内存泄漏。所以当程序有过多任务时,要怎样为这些任务分配线程就成了问题。
    • 一些程序功能较多,当用户切换不同功能时候,需要有不同的线程来执行这些功能的任务,频繁的创建和销毁线程非常占用操作系统资源,尤其是涉及资源调动的线程(用户态和内核态之间频繁切换占用系统上下文)
    • 所以需要有一个线程池批量创建线程,给各种任务使用,并管理各个任务有序使用线程池中的线程
  • 线程池是什么:线程池就是一个可以复用线程的技术。

线程池的工作原理

  • 线程池通过维护固定数量的核心线程处理任务,超出时进入队列缓冲,队列满启用临时线程,仍超负荷则触发拒绝策略,实现线程复用和资源管控。

线程池执行流程

  1. 当有第一个任务的时候,线程池会创建第一个核心线程来处理这个任务
  2. 继续添加任务,不管已有核心线程是否忙,只要核心线程没有满,都会创建新的核心线程处理,直到所有核心线程数都创建完成
  3. 核心线程都在忙,新来的任务就会加入工作队列
  4. 核心线程都在忙,工作队列存储的任务已满,再来的新任务会创建临时线程进行处理(因为出队运行队列中的线程再让新线程入队开销比较大,计算机比较喜欢偷懒,所以先让新线程使用临时线程先运行,从而时间复杂度可以降低)
  5. 核心线程都在忙,工作队列已满,临时线程都在忙,再来的新任务会触发决绝策略。

创建线程池

  • JDK 5.0起提供了代表线程池的接口:ExecutorService。
  • 如何创建线程池对象?
    • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
    • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。
方式1-ThreadPoolExecutor实现类创建
  • 语法

    • ThreadPoolExecutor构造器

      • 参数一:corePoolSize : 指定线程池的核心线程的数量。------正式工:3
      • 参数二:maximumPoolSize:指定线程池的最大线程数量。------大员工数:5;临时工:2
      • 参数三:keepAliveTime :指定临时线程的存活时间。------临时工空闲多久被开除
      • 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天)
      • 参数五:workQueue:指定线程池的任务队列。------客人排队的地方
      • 参数六:threadFactory:指定线程池的线程工厂。------负责招聘员工的(hr)
      • 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理)------忙不过来咋办?
      ThreadPoolExecutor类提供的构造器 作用
      public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler ) 使用指定的初始化参数创建一个新的线程池对象
    • ExecutorService方法

      方法名称 说明
      void execute(Runnable command) 执行 Runnable 任务
      Future submit(Callable task) 执行 Callable 任务,返回未来任务对象,用于获取线程返回的结果
  • Runnable接口代码示例

    java 复制代码
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.util.concurrent.*;
    public class Demo071 {
    
        public static void main(String[] args) {
            ExecutorService executorService = new ThreadPoolExecutor(
                    3,//参数一:corePoolSize
                    5,//参数二:maximumPoolSize
                    10,//参数三:keepAliveTime
                    TimeUnit.SECONDS,//参数四:unit
                    new ArrayBlockingQueue<>(3),//参数五:workQueue,这里设置容量3,可以存储3个任务
                    Executors.defaultThreadFactory(),// 参数六:threadFactory
                    new ThreadPoolExecutor.AbortPolicy() //数七:handler:指定线程池的任务拒绝策略
            );
            //使用线程池执行任务
            executorService.submit(new MyRunnable(1));//核心线程处理
            executorService.submit(new MyRunnable(2));//核心线程处理
            executorService.submit(new MyRunnable(3));//核心线程处理
            executorService.submit(new MyRunnable(4));//加入队列
            executorService.submit(new MyRunnable(5));//加入队列
            executorService.submit(new MyRunnable(6));//加入队列
            executorService.submit(new MyRunnable(7));//临时线程处理
            executorService.submit(new MyRunnable(8));//临时线程处理
            executorService.submit(new MyRunnable(9));//触发拒绝策略
        }
    }
    
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    class MyRunnable implements Runnable{
        private Integer index;
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+":执行任务"+index);
            try {
                Thread.sleep(Integer.MAX_VALUE);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
  • 实现Callable接口代码示例

    java 复制代码
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import java.util.concurrent.*;
    
    public class Demo072 {
    
        public static void main(String[] args) throws Exception {
            //目标:使用线程池执行计算1~9000000累加的和
            ExecutorService executorService = new ThreadPoolExecutor(
                    3,//参数一:corePoolSize
                    5,//参数二:maximumPoolSize
                    10,//参数三:keepAliveTime
                    TimeUnit.SECONDS,//参数四:unit
                    new ArrayBlockingQueue<>(3),//参数五:workQueue,这里设置容量3,可以存储3个任务
                    Executors.defaultThreadFactory(),// 参数六:threadFactory
                    new ThreadPoolExecutor.AbortPolicy() //数七:handler:指定线程池的任务拒绝策略
            );
    
    
    
            //使用3个线程计算
            //线程1:计算1~3000000累加的和
            //线程2:计算3000001~6000000累加的和
            //线程3:计算6000001~9000000累加的和
    
            Long startTime = System.currentTimeMillis();
    
            //线程1
            Future<Long> futureTask1 = executorService.submit(new MyCallable(1L, 3000000L));
            //线程2
            Future<Long> futureTask2 = executorService.submit(new MyCallable(3000001L, 6000000L));
            //线程3
            Future<Long> futureTask3 = executorService.submit(new MyCallable(6000001L, 9000000L));
    
    
            //7.获取线程执行结果
            Long sum1 = futureTask1.get();//阻塞等待当前线程执行完毕后获取结果
            Long sum2 = futureTask2.get();
            Long sum3 = futureTask3.get();
            System.out.println("子线程求和结果:"+(sum1+sum2+sum3));
            Long endTime = System.currentTimeMillis();
            System.out.println("3个线程完成任务耗时:"+(endTime-startTime)+"毫秒");
        }
    }
    //1、定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
    @Data //生成getter/setter/toString/hashCode/equals等方法
    @AllArgsConstructor//生成全参构造器,注意一定要有成员属性
    @NoArgsConstructor//生成无参构造器
    class MyCallable implements Callable<Long>{
    
        private Long start;//计算开始值
        private Long end;//计算结束值
    
        @Override
        public Long call() throws Exception {
            System.out.printf("MyCallable线程开始计算,从%d到%d的求和计算开始了%n",start,end);
            Long sum =0L;
            for (Long i = start; i <= end; i++) {
                sum += i;
            }
            System.out.printf("MyCallable线程开始计算,从%d到%d的求和计算结束了%n",start,end);
            return sum;
        }
    }
代码运行实验
  • 线程池一旦创建就不会停止运行,因为线程池的线程会在创建线程池后自动启动启动就会常驻,要停止线程需要调用线程池对象的shutdown()方法

  • 验证线程队列开启前6个任务,cpu性能比较好的话,线程速度比较快则任务不会进入到任务队列,而是直接在核心线程执行完成。

    java 复制代码
    // 关闭拖延运行时间的线程睡眠函数
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    class MyRunnable implements Runnable{
        private Integer index;
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+":执行任务"+index);
            // try {
            //     Thread.sleep(Integer.MAX_VALUE);
            // } catch (InterruptedException e) {
            //     throw new RuntimeException(e);
            // }
        }
    }
    java 复制代码
    //开启前6个任务
    //使用线程池执行任务
    executorService.submit(new MyRunnable(1));//核心线程处理
    executorService.submit(new MyRunnable(2));//核心线程处理
    executorService.submit(new MyRunnable(3));//核心线程处理
    executorService.submit(new MyRunnable(4));//加入队列
    executorService.submit(new MyRunnable(5));//加入队列
    executorService.submit(new MyRunnable(6));//加入队列
    // executorService.submit(new MyRunnable(7));//临时线程处理
    // executorService.submit(new MyRunnable(8));//临时线程处理
    // executorService.submit(new MyRunnable(9));//触发拒绝策略
    • 运行结果:所有任务都获得核心线程并运行完毕
  • 验证线程队列启动前6个线程,核心线程被占用,后三个任务进入任务队列

    java 复制代码
    // 打开拖延运行时间的线程睡眠函数(后续实验都为打开状态)
    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    class MyRunnable implements Runnable{
        private Integer index;
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName()+":执行任务"+index);
             try {
                 Thread.sleep(Integer.MAX_VALUE);
             } catch (InterruptedException e) {
                 throw new RuntimeException(e);
             }
        }
    }
    java 复制代码
    //开启前6个任务
    //使用线程池执行任务
    executorService.submit(new MyRunnable(1));//核心线程处理
    executorService.submit(new MyRunnable(2));//核心线程处理
    executorService.submit(new MyRunnable(3));//核心线程处理
    executorService.submit(new MyRunnable(4));//加入队列
    executorService.submit(new MyRunnable(5));//加入队列
    executorService.submit(new MyRunnable(6));//加入队列
    // executorService.submit(new MyRunnable(7));//临时线程处理
    // executorService.submit(new MyRunnable(8));//临时线程处理
    // executorService.submit(new MyRunnable(9));//触发拒绝策略
    • 运行结果
  • 验证队列满后任务直接使用临时线程

    java 复制代码
    //开启前6个任务
    //使用线程池执行任务
    executorService.submit(new MyRunnable(1));//核心线程处理
    executorService.submit(new MyRunnable(2));//核心线程处理
    executorService.submit(new MyRunnable(3));//核心线程处理
    executorService.submit(new MyRunnable(4));//加入队列
    executorService.submit(new MyRunnable(5));//加入队列
    executorService.submit(new MyRunnable(6));//加入队列
    executorService.submit(new MyRunnable(7));//临时线程处理
    executorService.submit(new MyRunnable(8));//临时线程处理
    // executorService.submit(new MyRunnable(9));//触发拒绝策略
    • 运行结果:使用了4,5临时线程
  • 验证任务过多,触发拒绝策略

    java 复制代码
    //开启前6个任务
    //使用线程池执行任务
    executorService.submit(new MyRunnable(1));//核心线程处理
    executorService.submit(new MyRunnable(2));//核心线程处理
    executorService.submit(new MyRunnable(3));//核心线程处理
    executorService.submit(new MyRunnable(4));//加入队列
    executorService.submit(new MyRunnable(5));//加入队列
    executorService.submit(new MyRunnable(6));//加入队列
    executorService.submit(new MyRunnable(7));//临时线程处理
    executorService.submit(new MyRunnable(8));//临时线程处理
    executorService.submit(new MyRunnable(9));//触发拒绝策略
    • 运行结果:触发默认拒绝策略(抛出异常)

拒绝策略

  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。
策略 说明
ThreadPoolExecutor.AbortPolicy() 丢弃任务并抛出RejectedExecutionException异常。是默认的策略
ThreadPoolExecutor. DiscardPolicy() 丢弃任务,但是不抛出异常,这是不推荐的做法
ThreadPoolExecutor. DiscardOldestPolicy() 抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor. CallerRunsPolicy() 由提交任务的线程来执行被拒绝的任务,调用任务的run()方法从而绕过线程池直接执行
java 复制代码
import java.util.concurrent.*;
public interface Demo073 {
    public static void main(String[] args) {
        //目标:演示不同拒绝策略
        ExecutorService executorService = new ThreadPoolExecutor(
                3, //参数一:corePoolSize
                5, //参数二:maximumPoolSize
                10, //参数三:keepAliveTime
                TimeUnit.SECONDS, //参数四:unit
                new ArrayBlockingQueue<>(3), //参数五:workQueue,这里设置容量3,可以存储3个任务
                Executors.defaultThreadFactory(), // 参数六:threadFactory
                 // new ThreadPoolExecutor.AbortPolicy() //默认拒绝策略,新任务会抛出异常
                 // new ThreadPoolExecutor.DiscardPolicy() //直接丢失,不推荐使用
                 // new ThreadPoolExecutor.DiscardOldestPolicy() //丢失工作队列中等待最久的任务,加入新任务到队列中
                new ThreadPoolExecutor.CallerRunsPolicy() //当前线程拒绝执行,交给父线程执行(一般是main线程)
);

        //使用线程池执行任务
        executorService.submit(new MyRunnable(1)); //核心线程处理
        executorService.submit(new MyRunnable(2)); //核心线程处理
        executorService.submit(new MyRunnable(3)); //核心线程处理
        executorService.submit(new MyRunnable(4)); //加入队列
        executorService.submit(new MyRunnable(5)); //加入队列
        executorService.submit(new MyRunnable(6)); //加入队列
        executorService.submit(new MyRunnable(7)); //临时线程处理
        executorService.submit(new MyRunnable(8)); //临时线程处理
        executorService.submit(new MyRunnable(9)); //触发拒绝策略
}
}
  • 使用CallerRunsPolicy()从父线程执行

方式2-Executors实现类创建

  • 代码示例(现在都不推荐了)
java 复制代码
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Demo074 {
    public static void main(String[] args) {
        //目标:了解使用工具类Executors创建线程池

        //创建固定数量线程的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);
        //不推荐,因为里面工作队列容量整型最大值,容易造成内存溢出

        //创建1个线程数量的线程池
        ExecutorService executorService1 = Executors.newSingleThreadExecutor();
        //不推荐,因为里面工作队列容量整型最大值,容易造成内存溢出

        //创建缓存线程池
        ExecutorService executorService2 = Executors.newCachedThreadPool();
        //不推荐,随着任务量变多,创建的线程也多,导致对线程数量没有限制,,容易造成内存溢出

    }
}
相关推荐
Java编程爱好者6 小时前
JVM GC调优实战:从线上频繁Full GC到RT降低80%的全过程
后端
阿丰资源6 小时前
基于Spring Boot的酒店客房管理系统
java·spring boot·后端
无籽西瓜a6 小时前
【西瓜带你学Kafka | 第八期】 Kafka的主从同步、消息可靠性、流处理与顺序消费(文含图解)
java·分布式·后端·kafka·消息队列·mq
zzqssliu6 小时前
SpringBoot框架搭建跨境独立站|Taocarts代购系统订单模块深度开发
java·spring boot·后端
Loo国昌6 小时前
从 Agent 编排到 Skill Runtime:企业 AI 工程化的下一层抽象
大数据·人工智能·后端·python·自然语言处理
小羊在睡觉6 小时前
力扣239. 滑动窗口最大值
数据结构·后端·算法·leetcode·go
RainCityLucky7 小时前
Java Swing 自定义组件库分享(七)
java·笔记·后端
_Evan_Yao7 小时前
如何搭建属于自己的技术博客(CSDN / GitHub Pages)
后端·学习·github
嘟嘟MD7 小时前
Storybound 产品进度分享,6月公测很快啦
后端·ai编程·创业