不是怎么还在转啊...

004-多线程

多线程

  • 什么是线程?

    • 线程(Thread)是一个程序内部的一条执行流程、

    • 如果程序中只有一条执行流程,那这个程序就是单线程程序

      image-20250805200159287

  • 什么是多线程?

    • 多线程是指从软硬件上实现的多条执行流程的技术(多条线程由cpu负责调度执行)
    • 多线程用在哪里?
      • 抢票系统、上传和下载并行......

创建线程

  • 方法一:实现Thread类

    • 步骤

      • 定义一个子类继承Thread类,成为一个线程类

      • 重写Thread类的run方法

      • 在run方法中编写线程的任务代码(线程要干的活)

      • 创建线程类对象代表线程

      • 调用线程类对象的start方法,启动线程。(自动调用run方法)

        image-20250805202256333

    • 优缺点

      • 优点:编码简单
      • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展
    • 注意事项

      • 启动线程必须是调用start方法,不是调用run方法
        • 直接调用run方法会当成普通方法执行,此时相当于还是单线程执行
        • 只有调用start方法才是启动一个新的线程
      • 不要把主线程任务放在启动子线程之前
        • 直到启动线程,线程才开始启动,这样没有和主线程同时执行的感觉
  • 方法二:实现Runnable接口

    • 步骤

      • 定义一个线程任务类实现Runnable接口

      • 重写run方法,设置线程任务

      • 创建线程任务类对象代表一个线程任务

      • 把线程任务对象交给一个线程对象来处理

        image-20250805202606803

      • 调用线程对象的start方法启动线程

        image-20250805202506027

    • 优缺点

      • 优点:人物类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
      • 缺点:需要多一个Runnable对象
    • 匿名内部类写法

      • 可以创建Runnable的匿名内部类对象

      • 再交给Thread线程对象

      • 再调用线程对象的start启动线程

        image-20250805203034239

  • 方法三:利用Callable接口、FutureTask类来实现

    • 前两种方法存在的问题:加入线程执行完毕后有一些数据要返回,他们重写的run方法均不能直接接返回结果

      • 一个错误的解决方法:用一个静态变量去记住run方法里的数据。
        • 错误原因是无法断定此时线程中的内容已经执行完了,线程还在和主线程同时运行的话,此时再主线程里是取不到线程结果的
      • 如何解决?
        • JDK5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)
        • 这种方式的最大优点:可以返回线程执行完毕后的结果
    • 步骤

      • 创建任务对象

        • 定义一个类实现Callable接口,重写call方法,封装要做的事情和要返回的数据

        • 把Callable类型的对象封装成FutureTask(线程任务对象),FutureTask实现了Runnable,FutureTask对象本质上就是Runnable对象,可以交给Thread线程对象处理

          image-20250805211938999

      • 把线程任务对象交给Thread对象

      • 调用Thread对象的start方法启动线程

      • 线程执行完毕后,通过FutureTask对象的get方法去获取线程任务执行的结果

    • 注意事项

      • 多个线程分开try-catch,这样一个线程异常不影响其他线程跑
      • 调用某个线程任务的get方法时,如果发现这个线程还没执行完毕,就会让出CPU,等该线程执行完毕后才会往下执行
    • 优缺点

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

    image-20250805212107146

线程常用方法

image-20250805212206301

  • join方法调用时,线程插队,全部跑完后再执行主线程

线程安全

  • 什么是线程安全问题?

    • 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题

      image-20250805213829482

线程同步

  • 线程同步是线程安全问题的解决方案

  • 线程同步的核心思想

    • 让多个线程先后一次访问共享资源,这样就可以避免出现线程安全问题
  • 线程同步的常见方案

    • 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来

方式一:同步代码块

  • 作用:把访问共享资源的核心代码都上锁,以此保证线程安全

    image-20250806180736827

  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行

  • 同步锁的注意事项

    • 对于当前同步执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug
    • 括号里的同步锁必须对于线程来说是唯一对象,例如可以填一个双引号字符串,而不能是new对象
  • 快捷键:ctrl+alt+t有将选中代码包入同步锁块中的选项

  • 随便使用一个唯一对象真的好吗(例如使用双引号字符串)?

    • 不好!会影响其他无关线程的执行

    • 例如取钱示例中,小明和小红有一个公共账户,小黑和小白也有个公共账户,那么原本只想在小明小红账户上加的锁实际上加到所有账户上了,这导致其他用户想访问自己账户的时候也要被这个锁卡住

    • 锁对象的使用规范:

      • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象

        image-20250806182340271

      • 对于静态方法建议使用字节码(类名.class)对象作为锁对象

        image-20250806182316547

    • 示例

    image-20250806181431251

    image-20250806181445692

方式二:同步方法

  • 作用:把访问共享资源的核心方法给上锁,以此保证线程安全

    image-20250806182521955

  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行

  • 示例

    image-20250806183100797

  • 同步方法底层原理

    • 同步方法其实底层也是隐式锁对象的,只是锁的范围是整个方法代码
    • 如果方法时实例方法:同步方法默认用this作为锁对象
    • 如果方法时静态方法:同步方法默认用类名.class作为锁对象
  • 同步代码块好还是同步方法好?

    • 同步代码块性能好,但现在的计算机不在乎这点性能
    • 在可读性方面,同步方法更好

方式三:Lock锁

  • Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。

  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock对象

    image-20250806183511495

  • Lock的常用方法

    image-20250806183530908

  • 示例

    image-20250806183834126

    • 锁对象不能用static,不然无法保证每个账户一把锁,这样所有账户就公用一把锁了
    • 使用final是保证锁不会被人撬了
    • 解锁操作一般放在finally中,用try包裹要锁的代码操作,在方法中一般把异常抛给上层,不用写catch

线程池

  • 线程池十一个可以复用线程的技术

  • 不使用线程池的问题

    • 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程来处理,创建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统性能
  • 线程池工作原理

    image-20250806184735854

    • 工作线程在任务队列中处理任务,处理完后再到后面找任务去执行

创建线程池

  • JDK5.0起提供了代表线程池的接口:ExecutorService

方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

image-20250806185148012

image-20250806185406776

  • 示例

    image-20250806185754444

方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

  • Executors是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象

    image-20250806192124274

  • 注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象

  • Executors使用可能存在的陷阱

    • 大型并发系统环境中使用Executors如果不注意可能会出现系统风险

      image-20250806192845239

      OOM是溢出的意思

处理Runnable任务

  • ExecutorService的常用方法

    image-20250806190029004

线程池的注意事项

  • 什么时候开始创建临时线程?

    • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程
  • 什么时候会拒绝新任务?

    • 核心线程和临时线程都在忙,任务队列也满了,新任务过来的时候才会开始拒绝任务
  • 任务拒绝策略

    image-20250806190803208

处理Callable任务

  • ExecutorService常用方法

    image-20250806191342026

并发、并行

  • 进程
    • 正在运行的程序(软件)就是一个独立的进程
    • 线程是属于进程的,一个进程中可以同时运行很多个线程
    • 进程中的多个线程其实是并发和并行执行的
  • 并发的含义
    • 进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行。这就是并发
  • 并行的理解
    • 在同一个时刻上,同时有多个线程在被CPU调度执行
posted @ 2025-08-06 20:03  Quirkygbl  阅读(19)  评论(0)    收藏  举报