当前位置: 首页 > news >正文

Java强化:多线程及线程池

一、多线程的基础概念:

  • 多线程的概念:线程是程序内部的一条执行流程,由Thread对象代表;多线程是从软硬件上实现多条执行流程的技术,由CPU负责调度,应用场景包括12306购票、百度网盘的上传下载、消息通信等。
  • 创建多线程的第一种方式:通过继承Thread类实现,步骤为:定义一个子类继承Thread类;重写Thread类的run方法,在该方法中编写线程要执行的任务代码;创建该子类的实例对象;调用实例对象的start方法启动线程。
  • 第一种创建方式的优缺点:优点是编码简单;缺点是线程类已继承Thread类,无法再继承其他类,不利于功能扩展(但该缺点并非绝对,需根据实际需求判断)。
  • 注意事项:启动线程必须调用start方法,直接调用run方法会将线程对象当作普通对象处理,仍为单线程;不要将主线程的任务放在启动子线程之前,否则主线程会先执行完,无法体现多线程同时执行的效果。

二、多线程实现Runnable接口创建:

  • 多线程的第二种创建方式:实现Runnable接口
    步骤为:

    1. 定义一个线程任务类实现Runnable接口;
    2. 重写Runnable接口的run方法,编写线程要执行的任务代码,示例代码如下:
      // 定义线程任务类实现Runnable接口
      class MyRunnable implements Runnable {@Overridepublic void run() {// 线程要执行的任务代码for (int i = 0; i < 5; i++) {System.out.println("子线程输出:" + i);}}
      }
      
    3. 创建该线程任务类的实例对象;
    4. 将该实例对象作为参数交给Thread对象,包装成线程对象;
    5. 调用线程对象的start方法启动线程,示例代码如下:
      public class ThreadDemo2 {public static void main(String[] args) {// 创建线程任务对象Runnable task = new MyRunnable();// 将任务对象包装成线程对象Thread t1 = new Thread(task);// 启动线程t1.start();// 主线程任务for (int i = 0; i < 5; i++) {System.out.println("主线程输出:" + i);}}
      }
      
  • 第二种创建方式的优缺点

    • 优点:线程任务类仅实现接口,可继续继承其他类和实现其他接口,扩展性强,对类的功能阉割少。
    • 缺点:需要额外创建一个任务对象,且线程执行完的run方法不能直接返回结果。
  • 第二种创建方式的简化写法:匿名内部类
    无需定义Runnable接口的实现类,直接创建Runnable匿名内部类对象作为线程任务对象,再包装成线程对象并启动。可进一步简化为链式编程,甚至利用Lambda表达式简化(因Runnable是函数式接口),示例代码如下:

    // 匿名内部类写法
    new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 5; i++) {System.out.println("子线程一输出:" + i);}}
    }).start();// Lambda表达式简化写法
    new Thread(() -> {for (int i = 0; i < 5; i++) {System.out.println("子线程二输出:" + i);}
    }).start();
    

三、多线程实现Callable接口创建:

  • 多线程的第三种创建方式:实现Callable接口:步骤为定义一个线程任务类实现Callable接口,并指定泛型类型(该类型为线程执行完返回的结果类型);重写Callable接口的call方法,编写线程要执行的任务代码,并在方法中返回结果;创建该线程任务类的实例对象;将该实例对象作为参数交给FutureTask对象,用于接收线程执行后的结果;将FutureTask对象作为参数交给Thread对象,包装成线程对象;调用线程对象的start方法启动线程;通过FutureTask对象的get方法获取线程执行后的结果。
  • 第三种创建方式的示例代码
    // 定义线程任务类实现Callable接口
    class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 0; i <= 100; i++) {sum += i;}return sum;}
    }// 测试类
    public class ThreadDemo3 {public static void main(String[] args) throws ExecutionException, InterruptedException {// 创建线程任务对象Callable<Integer> callable = new MyCallable();// 创建FutureTask对象接收结果FutureTask<Integer> futureTask = new FutureTask<>(callable);// 包装成线程对象并启动new Thread(futureTask).start();// 获取线程执行结果Integer result = futureTask.get();System.out.println("结果是:" + result);}
    }
    
  • 第三种创建方式的优缺点:优点是线程任务类仅实现接口,可继续继承其他类和实现其他接口,扩展性强;call方法可以直接返回结果,方便获取线程执行后的结果。缺点是需要额外创建任务对象和FutureTask对象,编码相对复杂;get方法可能会阻塞主线程,需要处理异常。
  • 三种创建方式的对比与选择:继承Thread类方式编码简单,但扩展性差;实现Runnable接口方式扩展性强,但无法直接返回结果;实现Callable接口方式扩展性强且能返回结果,但编码复杂。实际开发中,根据是否需要返回结果和扩展性需求选择,优先考虑实现接口的方式,需要返回结果时选Callable,否则选Runnable。

四、多线程的常用方法

  • 线程名称相关方法

    • 获取线程名称:getName()
    • 设置线程名称:setName()
    • 说明:线程默认名称为“Thread-索引”,主线程默认名称为“main”。设置名称需在启动线程之前,也可通过有参构造器设置。
  • 获取当前线程对象方法

    • 方法:currentThread()
    • 说明:这是Thread类的静态方法,用于获取当前正在执行的线程对象,哪个线程调用该方法,就返回哪个线程的对象。
  • 线程休眠方法

    • 方法:sleep(long millis)
    • 说明:这是Thread类的静态方法,作用是让当前执行的线程进入休眠状态,参数为休眠时间(单位为毫秒),时间到后线程继续执行,需处理异常。
  • 线程插队方法

    • 方法:join()
    • 说明:该方法用于让调用此方法的线程先执行完毕,即实现线程插队,待该线程执行完后,其他线程再继续执行,可用于控制线程执行顺序。
  • 其他线程方法说明

    • 如yield()、interrupt()、线程守护、线程优先级等方法在开发中较少使用,入门阶段掌握上述方法即可,后续用到再详细讲解。
  • 线程安全问题的定义:指多个线程同时操作同一个共享资源,且对该资源进行修改时,可能出现的业务安全问题。

  • 线程安全问题的示例:以小明和小红从共同账户(初始余额10万元)同时取10万元为例,可能出现两人都成功取出10万元,最终账户余额变为-10万元的安全问题,造成银行亏损。

  • 线程安全问题出现的原因

    • 存在多个线程同时执行;
    • 这些线程同时访问同一个共享资源;
    • 线程对共享资源进行了修改(仅读取资源不会产生安全问题)。
  • 模拟线程安全问题的设计思路:采用面向对象思想设计

    • 创建账户类(Account):描述共享账户,包含卡号、余额等信息,提供取钱方法;
    • 创建取钱线程类(DrawThread):继承Thread类,持有账户对象,重写run方法实现取钱逻辑;
    • 创建线程对象:代表小明和小红,传入同一个账户对象并启动,模拟同时取钱场景。
  • 模拟线程安全问题的代码实现

    • 账户类(Account)
      public class Account {private String cardId;private double money;// 构造器、getter、setter方法(略)// 取钱方法public void drawMoney(double money) {// 获取当前取钱的线程名称String name = Thread.currentThread().getName();// 判断余额是否充足if (this.money >= money) {System.out.println(name + "吐出" + money + "元,取钱成功");// 更新余额this.money -= money;System.out.println(name + "取钱后,余额剩余:" + this.money + "元");} else {System.out.println(name + "取钱失败,余额不足");}}
      }
      
    • 取钱线程类(DrawThread)
      public class DrawThread extends Thread {private Account account;// 有参构造器,接收账户对象和线程名称public DrawThread(Account account, String name) {super(name);this.account = account;}@Overridepublic void run() {// 调用账户的取钱方法,取10万元account.drawMoney(100000);}
      }
      
    • 主类(测试类)
      public class ThreadDemo1 {public static void main(String[] args) {// 创建共享账户对象,初始余额10万元Account account = new Account("ICBC-110", 100000);// 创建小明和小红的线程对象,传入同一个账户new DrawThread(account, "小明").start();new DrawThread(account, "小红").start();}
      }
      
  • 模拟结果及分析:代码大概率会出现线程安全问题(两人都取出10万元)。原因是两个线程可能同时判断余额充足(均为10万元),随后先后执行取钱和更新余额操作,导致账户余额异常。设计时将“更新余额”放在“吐出钱”之后,目的是给其他线程留下判断余额的时间窗口,更易模拟出问题。

五、解决线程安全问题:

  • 线程同步的核心思想:让多个线程先后依次访问共享资源,避免线程安全问题。通过加锁机制,每次只允许一个线程加锁并访问共享资源,访问完毕后自动解锁,其他线程才能加锁进入。
  • 同步代码块:使用synchronized关键字,将访问共享资源的核心代码块上锁。需要声明一个锁对象,该对象对于所有竞争的线程必须是同一把锁。实例方法建议用this作为锁,静态方法建议用字节码对象(类名.class)作为锁,以避免锁的范围过大影响性能。
  • 同步方法:在方法上使用synchronized关键字,将整个方法上锁。其原理与同步代码块类似,实例方法默认用this作为锁,静态方法默认用类名.class作为锁。同步方法锁的范围更广,可读性好但性能相对较差,同步代码块锁的范围小,性能更优。
  • Lock锁:JDK5提供的锁定操作,通过ReentrantLock创建具体的锁对象,使用lock()方法上锁,unlock()方法解锁。建议将unlock()放在finally块中,确保无论是否出现异常都能解锁,且锁对象建议用final修饰以防止被篡改。

六、线程池的相关知识:

  • 线程池的概念:线程池是一种可以复用线程的技术,能避免频繁创建线程导致的内存占用过多和CPU负担过重等问题。
  • 线程池的工作原理:线程池创建后,新任务会被放入任务队列,由固定的工作线程处理,线程处理完当前任务后可处理后续任务,从而控制线程数量,实现线程复用,提高系统性能。
  • 创建线程池的方式
    • 第一种是使用ExecutorService的实现类ThreadPoolExecutor,通过其有参构造器指定七个参数创建,七个参数分别是核心线程数量、最大线程数量、临时线程存活时间、时间单位、任务队列、线程工厂、任务拒绝策略。
    • 第二种是使用工具类Executors,调用其静态方法返回不同的线程池对象(后续课程讲解)。
  • 线程池处理Runnable任务:通过execute方法执行Runnable任务,线程池会复用线程处理任务,还介绍了关闭线程池的shutdown(等所有任务执行完毕后关闭)和shutdownNow(立即关闭,不管任务是否执行完毕)方法。
  • 临时线程的创建时机:新任务提交时,核心线程都在忙,任务队列已满,且可以创建临时线程时,才会创建临时线程。
  • 任务拒绝策略:当核心线程、临时线程都在忙,任务队列也满了,新任务过来时会触发拒绝策略,常见的有直接抛异常、丢弃任务不抛异常、丢弃等待最久的任务并加入新任务、由主线程执行任务等。
  • 线程池处理Callable任务:通过submit方法提交Callable任务,会返回一个Future对象,可通过该对象获取任务执行后的结果,线程池同样会复用线程处理任务。

七、Executors 工具类创建线程池

  • Executors工具类创建线程池

    • Executors是Java提供的线程池工具类,通过静态方法返回不同特点的线程池,具体如下:
      • newFixedThreadPool(n):返回固定线程数量的线程池,核心线程数和最大线程数均为n,无临时线程,任务队列不限制长度。
      • newSingleThreadExecutor():返回单个线程的线程池,仅1个核心线程,线程若因异常死亡会自动补充新线程。
      • newCachedThreadPool():返回可伸缩的线程池,线程数量随任务增加而增加,空闲60秒的线程会被回收。
      • newScheduledThreadPool(n):返回可执行定时/延时任务的线程池,核心线程数为n,属于ScheduledExecutorService类型。
  • Executors工具类底层原理

    • 底层通过调用ThreadPoolExecutor的构造器(含7个核心参数)实现线程池创建,本质是对线程池创建过程的封装。
    • 示例:newFixedThreadPool(3)的底层会将核心线程数、最大线程数均设为3,临时线程存活时间为0,任务队列采用无界链表(不限制任务数量)。
  • Executors工具类的风险

    • 阿里巴巴开发手册明确不建议在大型系统中使用,主要风险包括:
      • newFixedThreadPool()newSingleThreadExecutor():任务队列无界,大量任务堆积可能导致OOM(内存溢出)。
      • newCachedThreadPool()newScheduledThreadPool():线程数量可无限增长,可能因线程过多导致资源耗尽。
    • 建议:通过ThreadPoolExecutor手动配置7个参数(核心线程数、最大线程数、空闲时间等),灵活控制资源占用,小型系统可酌情使用Executors。
  • 线程池参数配置依据

    • 核心线程数和最大线程数的配置需结合业务类型:
      • 计算密集型(如大量循环、数学运算):线程数建议为CPU核心数或核心数+1,减少上下文切换开销。
      • IO密集型(如网络请求、磁盘读写):线程数建议为CPU核心数的2倍,利用CPU等待IO的时间提升效率。
  • 进程与线程的关系

    • 进程:正在运行的程序,动态占用CPU、内存、网络等资源。
    • 线程:进程内的执行单元,一个进程可包含多个线程,线程共享进程资源。
  • 并发与并行的概念

    • 并发:CPU通过分时轮询为多个线程服务,因切换速度极快,产生“同时执行”的错觉(如单核CPU处理多线程)。
    • 并行:同一时刻多个线程被CPU调度执行,依赖CPU核心数(如4核CPU可并行处理4个线程)。
    • 实际多线程执行是并发与并行的结合(如20核CPU在6800个线程中,每20个线程并行执行,整体通过并发切换处理所有线程)。

八、抢红包游戏案例

  • 案例需求介绍
    某企业100名员工(工号1-100)参与抢红包活动,需发出200个红包:

    • 小红包(1-30元)占80%(160个),大红包(31-100元)占20%(40个)。
    • 需模拟抢红包过程并输出详情,活动结束时提示“活动结束”,最终可对员工总金额降序排序(视频中主要完成前两步)。
  • 开发核心思路
    100名员工对应100个线程,竞争同一批红包资源(200个红包存于集合中),通过多线程同步机制保证线程安全。

  • 关键步骤与方法实现

    • 步骤1:生成红包集合
      使用Random类生成随机金额,按比例划分小红包和大红包,存入List集合。

      // 生成200个红包的方法
      public static List<Integer> generateRedPackets() {List<Integer> redPackets = new ArrayList<>();Random random = new Random();// 生成160个小红包(1-30元)for (int i = 0; i < 160; i++) {int amount = random.nextInt(30) + 1; // 1-30redPackets.add(amount);}// 生成40个大红包(31-100元)for (int i = 0; i < 40; i++) {int amount = random.nextInt(70) + 31; // 31-100redPackets.add(amount);}return redPackets;
      }
      
    • 步骤2:定义线程类(模拟员工抢红包)
      线程类实现Runnable接口或继承Thread类,通过构造器接收红包集合和员工工号,重写run()方法实现抢红包逻辑。

      // 抢红包线程类
      class GrabRedPacketThread extends Thread {private List<Integer> redPackets; // 共享的红包集合public GrabRedPacketThread(List<Integer> redPackets, String name) {super(name); // 线程名即员工工号this.redPackets = redPackets;}@Overridepublic void run() {while (true) {synchronized (redPackets) { // 同步代码块保证线程安全if (redPackets.isEmpty()) {System.out.println("活动结束");break;}// 随机获取一个红包并移除int index = new Random().nextInt(redPackets.size());int amount = redPackets.remove(index);System.out.println(Thread.currentThread().getName() + "抢到" + amount + "元");}// 休眠10ms模拟抢红包间隔try {Thread.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}
      }
      
    • 步骤3:启动线程与执行流程
      在主方法中生成红包集合,创建100个线程(员工)并启动,线程竞争抢红包直至红包耗尽。

      public static void main(String[] args) {List<Integer> redPackets = generateRedPackets(); // 生成红包// 创建100个员工线程并启动for (int i = 1; i <= 100; i++) {new GrabRedPacketThread(redPackets, "员工" + i).start();}
      }
      
  • 核心技术点

    • 线程安全:通过synchronized同步代码块锁定红包集合,避免同一红包被重复抢夺。
    • 多线程竞争:100个线程并发访问共享集合,模拟真实抢红包的随机性。
    • 流程控制:通过死循环和集合判空,控制抢红包过程直至红包耗尽。
http://www.dtcms.com/a/283147.html

相关文章:

  • 从电子管到CPU
  • 基于MATLAB的决策树DT的数据分类预测方法应用
  • Android CameraX使用
  • [析]Deep reinforcement learning for drone navigation using sensor data
  • CClink IEF Basic设备数据 保存到MySQL数据库项目案例
  • 高德地图MCP服务使用案例
  • 解锁数据交换的魔法工具——Protocol Buffers
  • 矿业自动化破壁者:EtherCAT转PROFIBUS DP网关的井下实战
  • ABP VNext + EF Core 二级缓存:提升查询性能
  • Mysql系列--1、库的相关操作
  • Mybatis-2快速入门
  • @Binds/@IntoMap/@ClassKey的使用
  • C++ shared_ptr 底层实现分析
  • uniapp+vue3+鸿蒙系统的开发
  • WD5018 同步整流降压转换器核心特性与应用,电压12V降5V,2A电流输出
  • MySQL学习——面试版
  • ssl相关命令生成证书
  • LangChain面试内容整理-知识点21:LangSmith 调试与监控平台
  • 职业发展:把工作“玩”成一场“自我升级”的游戏
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘tkinter’问题
  • webpack相关
  • 基于Matlab的四旋翼无人机动力学PID控制仿真
  • 第五届计算机科学与区块链国际学术会议(CCSB 2025)
  • 大模型训练框架对比
  • CTFMisc之隐写基础学习
  • 重学前端007 --- CSS 排版
  • day22 力扣77.组合 力扣216.组合总和III 力扣17.电话号码的字母组合
  • 异常流程进阶 —— 进出异常时的压栈与出栈
  • LVS集群搭建
  • 【Excel】使用vlookup函数快速找出两列数据的差异项