Java多线程理解及实现
一、什么是线程?
线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。简单理解就好比我们坐高铁、飞机过安检一样,过安检的时候一个入口只有一个安检口,而多线程就是为安检开启了多个安检口。Java在语言层面对多线程提供了卓越的支持。
二、线程和进程有什么区别?
线程是进程的子集,一个进程可以有很多线程,每条线程并行执行不同的任务。不同的进程使用不同的内存空间,而所有的线程共享一片相同的内存空间。每个线程都拥有单独的栈内存用来存储本地数据。
三、 如何在Java中实现线程?
线程实现的三种方式:
- 继承Thread类创建线程类
步骤:
===> 创建一个继承于Thread类的子类
===> 重写Thread类的run()方法
===> 创建Thread类的子类的对象
===> 通过此对象调用start()方法启动线程
public class FirstThreadTest extends Thread{ //重写run方法,run方法内就是线程要执行的业务逻辑代码 public void run(){ for(int i = 0;i<100;i++){ System.out.println(getName()+" "+i); } } public static void main(String[] args){ for(int i = 0;i< 100;i++){ // Thread.currentThread()方法返回当前正在执行的线程对象,getName()方法返回线程名称,此处线程暂未开启,所以打印的是Main线程名 System.out.println(Thread.currentThread().getName()+" :"+i); if(i==20){ // 启动连个线程 new FirstThreadTest().start(); new FirstThreadTest().start(); } } } }
- 通过Runnable接口创建线程类 (常用)
步骤:
===> 定义Runnable接口实现类,并重写run()方法【和Thread类中的run()方法是一样的,都是来自Runnable接口的】
===> 创建实现类的对象,将实现类的对象作为参数传递到Thread类的构造器中
===> 通过Thread类对象调用start()方法启动线程
public class RunnableThreadTest implements Runnable { public void run() { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName() + " " + i); if (i == 20) { // 创建实现类对象 RunnableThreadTest rtt = new RunnableThreadTest(); // 以参数的形式传入Thread类的构造器中,并给当前线程重命名 new Thread(rtt, "新线程1").start(); new Thread(rtt, "新线程2").start(); } } } }
- 通过线程池实现多线程 (最常用且最实用)
在Java中juc包中有一个Executors工具类,可以为我们创建一个线程池,其本质就是new了一个ThreadPoolExecutor对象。
- Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)
- Executors.newFixedThreadPool(int)(固定大小线程池)
- Executors.newSingleThreadExecutor()(单个后台线程)
- Executors.newScheduledThreadPool() (创建固定大小的线程,可以延迟或定时的执行任务)
线程池体系结构简化图:
Tips:蓝色实线箭头是类的继承;绿色虚线箭头是接口的实现,绿色实线箭头是接口的继承
Executors 工具类通过提供一系列的工厂方法来创建线程池,返回的线程池(就是ThreadPoolExecutor对象)都实现了 ExecutorService 接口,ExecutorService接口继承了Executor接口,提供了更丰富的方法来管理线程池,ExecutorService对象可以通过execute(Runnable)或 submit(Callable)方法来开始执行新的方法。
ExecutorService(其实就是线程池)的生命周期包括三种状态,运行,关闭,终止。创建后便进入运行状态。调用shutdown() 方法就进入关闭状态。此时ExecutorService不再接受新的任务,但是它还在执行已经提交的任务,等到所有的任务都执行完毕后,就到了终止状态。
示例代码:
public class ScheduledThreadTest { public static void main(String[] args) { System.out.println("当前线程: " + Thread.currentThread().getName() + "===========》start"); // 创建支持计划任务的线程池 ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2); // delay:延迟 延迟两秒执行任务 threadPool.schedule(() -> System.out.println("定时任务"),2, TimeUnit.SECONDS); System.out.println("当前线程: " + Thread.currentThread().getName() + "===========》end"); threadPool.shutdown(); } }
输出结果:(通过实际输出,可以看到 ,两秒钟之后,才开始执行任务当中的打印 语句)
=============================
周期性的定时任务示例代码:
public class ScheduledThreadTest2 { public static void main(String[] args) { System.out.println("当前线程: " + Thread.currentThread().getName() + "===========》start"); // 创建支持计划任务的线程池 ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(2); /** * scheduleWithFixedDelay(Runnable command,long initialDelay, long delay,TimeUnit unit) * 第一个command参数是任务实例, * 第二个initialDelay参数是初始化延迟时间, * 第三个delay参数是延迟间隔时间, * 第四个unit参数是时间单元。 */ threadPool.scheduleWithFixedDelay(new Runnable() { int count = 0; @Override public void run() { System.out.println("count = " + count++); } }, 3, 2, TimeUnit.SECONDS); System.out.println("当前线程: " + Thread.currentThread().getName() + "===========》end"); // threadPool.shutdown(); // 不能调用此方法,否则定时任务无法启动(延期3秒启动,但是线程池已经关闭了) } }
输出结果:
…..后面每间隔2s,输出一次。
Callable接口
现在我们知道了怎么创建线程:一种是通过创建Thread类,另一种是通过使用Runnable创建线程。但是,Runnable缺少的一项功能是,当线程终止时(即run()完成时),我们无法使线程返回结果。为了支持此功能,Java中提供了Callable,Callable接口使用泛型去定义它的返回类型。
- 为了实现Runnable,需要实现不返回任何内容的run()方法,而对于Callable,需要实现在完成时返回结果的call()方法。请注意,不能使用Callable创建线程,只能使用Runnable创建线程。
- 另一个区别是call()方法允许抛出异常,而run()则不能。
- 为实现Callable而必须重写call方法。
Future接口
当call()方法完成时,结果必须存储在主线程已知的对象中,以便主线程可以知道该线程返回的结果。为此,可以使用Future对象。将Future视为保存结果的对象–它可能暂时不保存结果,但将来会保存(一旦Callable返回)。因此,Future基本上是主线程可以跟踪进度以及其他线程的结果的一种方式。
Executors工具类提供了一些有用的方法在线程池中执行Callable内的任务。由于Callable任务是并行的(并行就是整体看上去是并行的,其实在某个时间点只有一个线程在执行),我们必须等待它返回的结果。 java.util.concurrent.Future对象为我们解决了这个问题。在线程池提交Callable任务后返回了一个Future对象,使用它可以知道Callable任务的状态和得到Callable返回的执行结果。Future提供了get()方法让我们可以等待Callable结束并获取它的执行结果。
要实现此接口,必须重写5种方法:
要创建线程,需要Runnable。为了获得结果,需要future。Java库具有具体的FutureTask类型,该类型实现Runnable和Future,并将这两种功能组合在一起。可以通过为其构造函数提供Callable来创建FutureTask。然后,将FutureTask对象提供给Thread的构造函数以创建Thread对象。因此,间接地使用Callable创建线程。
使用Callable和Future的完整示例:
import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.concurrent.*; public class TestCallable implements Callable<Object> { private int taskNum; public TestCallable(int taskNum) { this.taskNum = taskNum; } //1,2主要区别是创建线程的方式 public static void main(String[] args) throws ExecutionException, InterruptedException { test1(); test2(); } /** * 使用Executors.newFixedThreadPool创建线程池 * @throws InterruptedException * @throws ExecutionException */ private static void test1() throws InterruptedException, ExecutionException { System.out.println("----程序开始运行----"); Date date1 = new Date(); int taskSize = 5; ExecutorService pool = Executors.newFixedThreadPool(taskSize); List<Future> list = new ArrayList<Future>(); for (int i = 0; i < taskSize; i++) { Callable c = new TestCallable(i); // 执行任务并获取Future对象 Future f = pool.submit(c); list.add(f); } // 关闭线程池 pool.shutdown(); // 获取所有并发任务的运行结果 for (Future f : list) { // 从Future对象上获取任务的返回值,并输出到控制台 System.out.println(">>>" + f.get().toString()); //OPTION + return 抛异常 } Date date2 = new Date(); System.out.println("----程序结束运行----,程序运行时间【" + (date2.getTime() - date1.getTime()) + "毫秒】"); } /** * 线程直接使用new Thread来创建 * @throws ExecutionException * @throws InterruptedException */ private static void test2() throws ExecutionException, InterruptedException { System.out.println("----程序开始运行----"); Date date1 = new Date(); int taskSize = 5; FutureTask[] randomNumberTasks = new FutureTask[5]; List<Future> list = new ArrayList<Future>(); for (int i = 0; i < randomNumberTasks.length; i++) { Callable c = new TestCallable(i); // 执行任务并获取Future对象 randomNumberTasks[i] = new FutureTask(c); Thread t = new Thread(randomNumberTasks[i]); t.start(); } // 获取所有并发任务的运行结果 for (Future f : randomNumberTasks) { // 从Future对象上获取任务的返回值,并输 System.out.println(">>>" + f.get().toString()); //OPTION + return 抛异常 } Date date2 = new Date(); System.out.println("----程序结束运行----,程序运行时间【" + (date2.getTime() - date1.getTime()) + "毫秒】"); } /** * call方法的实现,主要用于执行线程的具体实现,并返回结果 * @return * @throws Exception */ @Override public Object call() throws Exception { System.out.println(">>>" + taskNum + "任务启动"); Date dateTmp1 = new Date(); Thread.sleep(1000); Date dateTmp2 = new Date(); long time = dateTmp2.getTime() - dateTmp1.getTime(); System.out.println(">>>" + taskNum + "任务终止"); return taskNum + "任务返回运行结果,当前任务时间【" + time + "毫秒】"; } }
运行结果:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?