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

java中创建多线程的4种方式

目录

一、继承 Thread 类创建线程

步骤

示例代码

原理

二、实现 Runnable 接口创建线程

步骤

示例代码

原理

三、实现 Callable 接口创建线程

步骤

示例代码

原理

与Runnable接口相比的不同之处

四、使用线程池创建线程

步骤

示例代码(使用 Executors.newFixedThreadPool)

原理

线程池的优势

自定义线程池

五、总结


        在 Java 编程中,多线程是一项非常重要的技术,它能够充分利用计算机的多核处理器资源,提高程序的执行效率和响应性。本文将详细介绍 Java 中创建多线程的四种方式,包括继承 Thread 类、实现 Runnable 接口、实现 Callable 接口以及使用线程池,并对每种方式的原理、代码示例和适用场景进行深入剖析。

一、继承 Thread 类创建线程

步骤

  • 创建一个类继承自 Thread 类。
  • 重写 run 方法,在 run 方法中定义线程要执行的任务。
  • 创建该类的实例,然后调用 start 方法启动线程。

示例代码

class MyThread extends Thread{// Ctrl + o// 展示所有的可以重写的方法@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("数据:"+i);}}
}
public class Demo01 {/***  第一种方案,继承Thread类  重写run方法 实现* @param args*/public static void main(String[] args) {// 在Main方法中,启动了一个子线程,子线程什么时候工作MyThread thread = new MyThread();thread.start();// 启动一个线程,调用start方法,不要调用run方法// 一个线程类,是可以创建多个不同的子线程的MyThread thread2 = new MyThread();thread2.start();// 启动一个线程,调用start方法,不要调用run方法// 主线程,直接运行代码   会出现子线程和主线程抢占资源的情况for (int i = 10; i < 100; i++) {System.err.println("Main:"+i);}}
}

原理

        当调用 start 方法时,会在新的线程中执行 run 方法。需要注意的是,start 方法只是启动线程,不会立即执行 run 方法。线程要等待获取 CPU 资源后才会执行 run 方法,而且在执行过程中可能会被其他线程抢占 CPU 资源。

二、实现 Runnable 接口创建线程

步骤

  • 创建一个类实现 Runnable 接口。
  • 实现 run 方法,在 run 方法中定义线程要执行的任务。
  • 创建 Runnable 接口实现类的实例,将其作为参数传递给 Thread 类的构造函数,然后调用 start 方法启动线程。

示例代码

class A implements Runnable{@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println(Thread.currentThread().getName()+":"+i);}}
}
public class Demo03 {/***  多线程创建的第二种方式,使用 Runnable接口*  该接口还需要传递给Thread类才能启动,否则自己启动不了**  两种方式:推荐使用第二种*   1、Thread类是一个线程类,它只需要管理好线程就行了,不需要管业务怎么写*   2、具体的业务可以交给Runnable接口实现*   3、java是单继承的,继承了Thread,就无法继承别的类了,但是可以实现多个接口。*/public static void main(String[] args) {A a = new A();new Thread(a).start();// Runnable接口本身就是一个函数式接口,就可以使用lambda表达式,代码可以简化为如下:new Thread( ()-> {for (int i = 0; i < 1000; i++) {System.out.println(Thread.currentThread().getName()+":"+i);}}).start();for (int i = 0; i < 1000; i++) {System.err.println(Thread.currentThread().getName()+":"+i);}}
}

原理

        Runnable 接口定义了一个无返回值的 run 方法,用于包含线程要执行的代码。Thread 类的构造函数可以接收一个 Runnable 接口的实现对象,当调用 Thread 的 start 方法时,会在新的线程中执行 Runnable 对象的 run 方法。这种方式比继承 Thread 类更灵活,因为 Java 是单继承的,如果一个类已经继承了其他类,就不能再继承 Thread 类了,但是可以实现 Runnable 接口来实现多线程。

三、实现 Callable 接口创建线程

步骤

  • 创建一个类实现 Callable 接口,该接口是一个函数式接口,有一个泛型参数,用于指定返回值类型。
  • 实现 call 方法,在 call 方法中定义线程要执行的任务,并返回一个结果。
  • 创建 Callable 接口实现类的实例,将其包装在一个 FutureTask 对象中,FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 接口继承了 Runnable 和 Future 接口。
  • 将 FutureTask 对象作为参数传递给 Thread 类的构造函数,然后调用 start 方法启动线程。可以通过 FutureTask 的 get 方法获取 call 方法的返回结果。

示例代码

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;class MyCall implements Callable<Integer>{@Overridepublic Integer call() throws Exception {return 200;}
}class MyRun implements Runnable{@Overridepublic void run() {System.out.println("我是子线程....");}
}
public class Demo08 {public static void main(String[] args) throws ExecutionException, InterruptedException {FutureTask<Integer> futureTask = new FutureTask<>(new MyCall());new Thread(futureTask,"计算线程").start();Integer i = futureTask.get();System.out.println(i);// --------------------------------------new Thread(new MyRun()).start();// ------------------使用callable 模拟 子线程进行大量计算并返回结果------------------FutureTask<Integer> f1 = new FutureTask<>(()->{System.out.println(Thread.currentThread().getName()+"  come in callable");TimeUnit.SECONDS.sleep(4);return 1024;});FutureTask<Integer> f2 = new FutureTask<>(()->{System.out.println(Thread.currentThread().getName()+"  come in callable");TimeUnit.SECONDS.sleep(4);return 2048;});new Thread(f1,"线程一:").start();new Thread(f2,"线程二:").start();while(!f1.isDone()){System.out.println("f1  wait中.....");}while(!f2.isDone()){System.out.println("f2  wait中.....");}// 其实 get 获取不到值会一直阻塞,直到获取到值为止int a = f1.get();int b = f2.get();System.out.println(a+b);}
}

原理

        Callable 接口与 Runnable 接口类似,但是 Callable 接口的 call 方法可以有返回值,并且可以抛出异常。FutureTask 用于包装 Callable 对象,它可以在未来某个时刻获取 call 方法的返回结果。通过这种方式,可以实现有返回值的多线程任务。

与Runnable接口相比的不同之处

(1)是否有返回值

(2)是否抛异常
(3)落地方法不一样,一个是run,一个是call

四、使用线程池创建线程

步骤

  • 通过 Executors 工具类的静态方法(如 newFixedThreadPoolnewCachedThreadPoolnewSingleThreadExecutor)创建一个线程池对象,或者直接使用 ThreadPoolExecutor 类来创建自定义的线程池。
  • 创建 Runnable 或 Callable 接口实现类的实例,作为任务提交给线程池。对于 Runnable 任务,可以使用 execute 方法提交;对于 Callable 任务,可以使用 submit 方法提交。

示例代码(使用 Executors.newFixedThreadPool

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo {public static void main(String[] args) {//       ExecutorService threadPool =  Executors.newFixedThreadPool(5); //一个银行网点,5个受理业务的窗口
//       ExecutorService threadPool =  Executors.newSingleThreadExecutor(); //一个银行网点,1个受理业务的窗口ExecutorService threadPool =  Executors.newCachedThreadPool(); //一个银行网点,可扩展受理业务的窗口//10个顾客请求try {for (int i = 1; i <=10; i++) {threadPool.execute(()->{System.out.println(Thread.currentThread().getName()+"\t 办理业务");});}} catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}}
}

原理

        线程池用于管理和复用线程。当提交一个任务到线程池时,线程池会根据自身的状态和配置来决定如何处理任务。如果线程池中有空闲线程,就会将任务分配给空闲线程执行;如果没有空闲线程且线程数量未达到最大限制,就会创建新的线程来执行任务;如果线程数量达到最大限制且任务队列已满,会根据线程池的拒绝策略来处理任务。这样可以有效地控制线程的数量,提高系统的性能和资源利用率,减少线程创建和销毁的开销。

线程池的优势

线程池做的工作只要是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行。

它的主要特点为:线程复用;控制最大并发数;管理线程。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的销耗。

第二:提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会销耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类。

经常使用的线程池做法

1、Executors.newFixedThreadPool(int)

执行长期任务性能好,创建一个线程池,一池有N个固定的线程,有固定线程数的线程

newFixedThreadPool创建的线程池corePoolSize和maximumPoolSize值是相等的,它使用的是LinkedBlockingQueue

2、Executors.newSingleThreadExecutor()

一个任务一个任务的执行,一池一线程。

newSingleThreadExecutor 创建的线程池corePoolSize和maximumPoolSize值都是1,它使用的是LinkedBlockingQueue

3、Executors.newCachedThreadPool()

执行很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可用时将重用它们。可扩容,遇强则强。

newCachedThreadPool创建的线程池将corePoolSize设置为0,将maximumPoolSize设置为Integer.MAX_VALUE,它使用的是SynchronousQueue,也就是说来了任务就创建线程运行,当线程空闲超过60秒,就销毁线程。

自定义线程池


虽然根据API 我们能很轻松的使用到线程池,但是在实际开发中我们经常自定义线程池,怎么做呢?

参数说明

1、corePoolSize:线程池中的常驻核心线程数
2、maximumPoolSize:线程池中能够容纳同时执行的最大线程数,此值必须大于等于1
3、keepAliveTime:多余的空闲线程的存活时间   
当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时, 多余线程会被销毁直到只剩下corePoolSize个线程为止。
4、unit:keepAliveTime的单位 
5、workQueue:任务队列,被提交但尚未被执行的任务  就是我们之前讲的阻塞队列
6、threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程,一般默认的即可
7、handler:拒绝策略,表示当队列满了,并且工作线程大于等于线程池的最大线程数(maximumPoolSize)时如何来拒绝
请求执行的runnable的策略

线程池的拒绝策略

AbortPolicy(默认):直接抛出RejectedExecutionException异常阻止系统正常运行。
CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,俗称从哪儿来到哪儿去。
DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加人队列中尝试再次提交当前任务。
DiscardPolicy:该策略默默地丢弃无法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种策略。

以上内置拒绝策略均实现了RejectedExecutionHandle接口

1、在创建了线程池后,线程池中的线程数为零。

2、当调用execute()方法添加一个请求任务时,线程池会做出如下判断:

        2.1如果正在运行的线程数量小于corePoolSize,那么马上创建线程运行这个任务;

        2.2如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;

        2.3如果这个时候队列满了且正在运行的线程数量还小于maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

        2.4如果队列满了且正在运行的线程数量大于或等于maximumPoolSize,那么线程池会启动饱和拒绝策略来执行。

3、当一个线程完成任务时,它会从队列中取下一个任务来执行。

4、当一个线程无事可做超过一定的时间(keepAliveTime)时,线程会判断:

        如果当前运行的线程数大于corePoolSize,那么这个线程就被停掉。

        所以线程池的所有任务完成后,它最终会收缩到corePoolSize的大小。

 示例代码

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.*;
/*** 线程池* Arrays* Collections* Executors*/
public class MyThreadPoolDemo {public static void main(String[] args) {ExecutorService threadPool = new ThreadPoolExecutor(2,5,2L,TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(3),Executors.defaultThreadFactory(),//new ThreadPoolExecutor.AbortPolicy()//new ThreadPoolExecutor.CallerRunsPolicy()//new ThreadPoolExecutor.DiscardOldestPolicy()new ThreadPoolExecutor.DiscardOldestPolicy());//10个顾客请求try {for (int i = 1; i <= 10; i++) {threadPool.execute(() -> {System.out.println(Thread.currentThread().getName() + "\t 办理业务");});}} catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}}
}

五、总结

        Java 提供了多种创建多线程的方式,每种方式都有其特点和适用场景。继承 Thread 类简单直接,适用于简单的线程任务;实现 Runnable 接口更加灵活,适合在已有类层次结构中使用多线程;实现 Callable 接口可用于需要获取线程执行结果的场景;使用线程池则可以高效地管理和复用线程,适用于需要大量线程处理任务的情况,并且可以通过合理配置线程池参数来优化系统性能。在实际开发中,需要根据具体的需求和场景选择合适的多线程创建方式,以充分发挥多线程编程的优势,提高程序的质量和效率。

http://www.lryc.cn/news/487211.html

相关文章:

  • MATLAB深度学习(二)——如何训练一个卷积神经网路
  • 删除k8s 或者docker运行失败的脚本
  • 重置docker版本的octoprint管理员账号密码
  • prometheus监控数据远程写入Kafka集群
  • Excel使用-弹窗“此工作簿包含到一个或多个可能不安全的外部源的链接”的发生与处理
  • C++小白实习日记——Day 2 TSCNS怎么读取当前时间
  • 【Pythonr入门第二讲】你好,世界
  • 3D Streaming 在线互动展示系统:NVIDIA RTX 4090 加速实时渲染行业数字化转型
  • Oracle 单机及 RAC 环境 db_files 参数修改
  • 消息中间件分类
  • 讯飞、阿里云、腾讯云:Android 语音合成服务对比选择
  • SpringBoot开发——整合AJ-Captcha实现安全高效的滑动验证码
  • Spring Security 核心组件
  • 聚焦 AUTO TECH 2025华南展:探索新能源汽车发展新趋势
  • Python-简单病毒程序合集(一)
  • [STM32]从零开始的STM32 HAL库环境搭建
  • Docker部署Kafka SASL_SSL认证,并集成到Spring Boot
  • Pr:音频过渡
  • HarmonyOs鸿蒙开发实战(17)=>沉浸式效果第二种方案一组件安全区方案
  • 从 const 到 mutable:C++ 中的优雅妥协与设计智慧
  • CC工具箱使用指南:【CAD导出界址点Excel】
  • 制作图片马常用的五种方法总结
  • 深入解析MySQL中的事务处理
  • TCP Analysis Flags 之 TCP Dup ACK
  • r-and-r——提高长文本质量保证任务的准确性重新提示和上下文搜索的新方法可减轻大规模语言模型中的迷失在中间现象
  • 光伏电站的方案PPT总结
  • 前端pdf预览方案
  • java 深拷贝 浅拷贝 详解
  • 针对git、giteeVSCode连接的使用 || Live Share插件使用
  • 如何解决Ubuntu 20.04中Vim编辑器在按下Ctrl+S时暂停响应的问题