我将从基础概念讲起,逐步深入介绍Java多线程的创建方式,通过代码示例帮助你理解,并阐述每种方式的适用场景。
Java多线程新手指南:从零开始学习多线程创建
多线程已是众人眼里老生常谈的话题之一了,在日常项目开发中它也是最为熟知且常用的技术之一,毕竟它能够允许程序同时执行多个任务,从而提高程序的响应速度和资源利用率,顾名思义,就是让计算机的多个核心同时工作,就像多个人同时做不同的任务一样。但在Java这种流行的编程语言中,多线程的使用尤为重要,因为它能帮助我们写出更高效、更流畅的程序,毕竟Java作为一种广泛应用于开发领域的语言,提供了强大的多线程支持,也提供了丰富的多线程支持,使得开发者可以轻松地实现并发编程。所以今天我继续来聊聊它,本文将重点介绍及演示如何使用Java来创建多线程,及衍生穿插知识点,带着大家能零基础轻松入门多线程编辑。
在Java多线程编程中,掌握线程的创建和管理是至关重要的。本文将介绍Java中多线程的创建方法,包括继承Thread
类和实现Runnable
接口两种方式,并通过源代码解析、应用场景案例、优缺点分析等来详细讲解每一种方法的使用和特点,最后通过一个测试案例来完整的复盘一遍,理论+实践 = 百分百掌握。
一、为什么需要多线程?
可能很多刚入门的小伙伴就会产生疑问,为啥需要多线程?这里我先就给大家进行分享下个人的理解,无妨大家可以这样想象一下,你在厨房里准备晚餐,如果只有你一个人,你可能需要先切菜,然后煮饭,接着炒菜;但如果你有帮手,你们可以同时进行这些任务,这样晚餐就能更快准备好。同样,在计算机程序中,如果我们能让不同的任务同时进行,程序就能更快地完成工作,也就是所谓的并行处理,同一时间多人进行,从而加快程序的执行速度。
多线程非常重要,它就像是在厨房里多请了几个帮手,每个人都能同时做不同的活儿,这样饭菜就能更快上桌。在电脑程序里,多线程能让程序同时做很多事,就像多个人一起工作一样,效率自然就高了。其具体优势如下:
- 提高效率:想象一下,如果电脑里只有一个程序在运行,它就只能做一件事。但有了多线程,电脑就能像多核大脑一样,同时处理好几个任务,速度自然就快了。
- 改善响应性:就像你在玩游戏时,如果电脑还在下载东西,游戏可能会卡顿。但如果用多线程,下载和游戏可以同时进行,互不影响,游戏就能流畅多了。
- 资源利用率:多线程就像是把电脑的CPU和内存都用起来,不让它们闲着。这样,电脑的每个部分都能发挥最大作用。
- 更好的用户体验:用户就像是顾客,他们希望点菜后能快速上菜。多线程能让程序快速响应用户的操作,就像快速上菜一样,让顾客满意。
- 并行处理:有些任务可以分成很多小块,每块都可以同时处理。多线程就像是有很多工人同时工作,这样完成任务的速度就快多了。
- 简化设计:有时候,程序设计得太复杂,就像厨房里有很多复杂的机器。多线程可以让程序设计更简单,就像用简单的工具就能做出好菜。
- 避免阻塞:如果一个任务卡住了,整个程序可能就会停下来。多线程可以避免这种情况,就像一个厨师忙不过来,其他厨师可以接手继续做。
- 利用现代硬件:现在的电脑越来越强大,多线程就像是让这些强大的电脑发挥出它们的最大能力。
- 适应性:在一些需要快速反应的场合,比如在线游戏或者股票交易,多线程能让程序更加灵活,快速适应变化。
- 错误隔离:如果程序中的一个部分出了问题,多线程可以保证其他部分不受影响,就像一个厨师出错了,其他厨师还可以继续工作。
二、Java中的多线程是如何工作的?
Java提供了两种主要的方式来创建线程,这两种方式各有特点,适用于不同的应用场景:
- 继承Thread类:就像你创建一个新食谱,基于一个已有的食谱进行修改。在Java中,你可以通过创建一个新的类,继承自
Thread
类,并重写它的run
方法来定义你的任务。 - 实现Runnable接口:这就像是按照食谱做菜,你不需要自己从头开始创造食谱,只需要按照已有的步骤来。在Java中,你可以创建一个实现了
Runnable
接口的类,并实现它的run
方法,然后将其传递给Thread
对象。
三、如何创建和启动线程?
创建和启动线程是多线程编程中的基础操作,它们确保了程序能够并行执行多个任务。以下是创建和启动线程的详细步骤,以及它们之间的逻辑联系:
- 定义任务:在多线程编程中,每个线程都需要执行一个特定的任务。任务的定义通常通过编写一个
run
方法来实现。这个方法是线程执行的入口点,包含了线程要执行的所有操作。 - 选择线程创建方式:根据你的程序需求,你可以选择继承
Thread
类或实现Runnable
接口来定义你的任务。继承Thread
类意味着你的类直接扩展了线程的功能,而实现Runnable
接口则需要将你的任务类传递给一个Thread
对象。 - 创建线程对象:一旦你定义了任务,接下来就是创建线程对象。如果你选择继承
Thread
类,你将创建该类的实例;如果实现Runnable
接口,你需要创建一个Runnable
对象,并将该对象作为参数传递给Thread
类的构造函数,然后创建Thread
对象的实例。 - 启动线程:创建线程对象后,你需要调用
start()
方法来启动线程。这个方法会触发线程的执行,使其进入就绪状态,并最终运行。重要的是要注意,start()
方法会隐式地调用你的run
方法,因此无需手动调用run
。 - 线程的执行:一旦线程启动,它将按照定义的
run
方法中的指令执行任务。线程的执行是由Java运行时环境的线程调度器控制的,它会根据系统的线程调度策略来决定哪个线程何时执行。
四、Java多线程创建方式详解
4.1 继承Thread类
这是创建线程最直接的方式之一。通过继承Thread
类,并重写其run
方法,来定义线程的执行逻辑。
4.1.1 创建步骤
- 定义线程类:创建一个类并继承自
Thread
类。在这个类中,重写run()
方法,run()
方法中的代码就是这个线程要执行的任务。class MyThread extends Thread { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("线程 " + getName() + " 执行,i = " + i); } } }
创建并启动线程:在主程序中创建线程对象,并调用
start()
方法来启动线程。start()
方法会自动调用线程对象的run()
方法,使得线程开始执行。例如:public class Main { public static void main(String[] args) { MyThread thread1 = new MyThread(); thread1.start(); MyThread thread2 = new MyThread(); thread2.start(); } }
4.1.2 优缺点分析
- 优点:简单直接,符合面向对象的编程思想,初学者容易理解和掌握。
- 缺点:由于Java是单继承的,如果一个类已经继承了其他类,就无法再继承
Thread
类来创建线程,这限制了类的扩展性。同时,线程与任务逻辑耦合在一起,不符合“组合优于继承”的设计原则。
4.1.3 应用场景案例
适合一些简单的、独立的任务,不需要继承其他类的情况。比如,在一个小型的测试程序中,需要创建几个简单的线程来模拟并发操作,此时使用继承Thread
类的方式就比较合适。
4.2 实现Runnable接口
这是另一种常见的创建线程的方式,通过实现Runnable
接口来定义任务,然后将其传递给Thread
对象。
4.2.1 创建步骤
- 定义实现Runnable接口的类:创建一个类实现
Runnable
接口,然后实现接口中的run()
方法,将线程要执行的任务写在run()
方法中。例如:class MyRunnable implements Runnable { @Override public void run() { for (int i = 0; i < 5; i++) { System.out.println("线程 " + Thread.currentThread().getName() + " 执行,i = " + i); } } }
创建并启动线程:首先创建实现了
Runnable
接口的类的对象,然后将这个对象作为参数传递给Thread
类的构造函数来创建线程对象,最后调用start()
方法启动线程。例如:public class Main { public static void main(String[] args) { MyRunnable runnable1 = new MyRunnable(); Thread thread1 = new Thread(runnable1); thread1.start(); MyRunnable runnable2 = new MyRunnable(); Thread thread2 = new Thread(runnable2); thread2.start(); } }
4.2.2 优缺点分析
- 优点:避免了单继承的限制,因为一个类可以实现多个接口。同时,多个线程可以共享同一个
Runnable
对象,适合多个线程执行相同任务的情况,提高了代码的复用性。任务与线程分离,更符合“组合优于继承”的设计原则,使得代码的结构更加清晰。 - 缺点:相比继承
Thread
类,代码稍微复杂一些,需要创建Runnable
对象并将其传递给Thread
对象。
4.2.3 应用场景案例
在多线程的场景中,如果有多个线程需要执行相同的任务逻辑,例如在一个服务器程序中,多个客户端请求可能需要执行相同的业务逻辑,此时使用实现Runnable
接口的方式就非常合适。可以创建一个Runnable
实现类,然后为每个客户端请求创建一个新的线程来执行该Runnable
任务。
4.3 使用Callable和Future接口(适用于有返回值的线程)
前面两种方式创建的线程,其执行结果是无法直接获取的。如果需要获取线程执行后的返回值,可以使用Callable
和Future
接口。
4.3.1 创建步骤
- 定义实现Callable接口的类:创建一个类实现
Callable
接口,需要指定返回值类型。实现call()
方法,在这个方法中编写线程要执行的任务,并返回一个结果。例如,计算1到100的整数和:
```java
import java.util.concurrent.Callable;
class MyCallable implements Callable {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
2. **创建并执行线程,获取结果**:通过`ExecutorService`来管理线程的执行。首先创建`Callable`对象,然后将其提交给`ExecutorService`,返回一个`Future`对象。通过`Future`对象可以获取线程执行后的返回值。例如:
```java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
public class Main {
public static void main(String[] args) throws Exception {
MyCallable callable = new MyCallable();
// 创建一个线程池,这里只使用一个线程来执行任务,实际可以根据需求调整线程数量
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<Integer> future = executor.submit(callable);
// 获取线程执行后的结果
Integer result = future.get();
System.out.println("1到100的整数和为:" + result);
// 关闭线程池
executor.shutdown();
}
}
4.3.2 优缺点分析
- 优点:可以获取线程执行后的返回值,并且可以通过
ExecutorService
来更好地管理线程,如设置线程池大小、控制线程的执行顺序等,提高了线程的管理和控制能力。 - 缺点:相比前两种方式,代码更加复杂,需要理解
ExecutorService
、Callable
和Future
等多个接口和类的使用,对初学者来说难度较大。
4.3.3 应用场景案例
在一些需要异步计算并获取结果的场景中非常有用。比如,在一个数据分析程序中,需要启动一个线程去执行复杂的数据计算任务,计算完成后获取计算结果进行后续处理。此时使用Callable
和Future
接口就可以满足需求。
4.4 使用线程池创建多线程
线程池是一种管理和复用线程的机制,可以避免频繁创建和销毁线程带来的开销,提高系统性能。
4.4.1 使用ExecutorService和Executors创建线程池
- 创建线程池:可以使用
Executors
工厂类来创建不同类型的线程池。例如,创建一个固定大小的线程池:// 创建一个包含5个线程的固定大小线程池 ExecutorService executorService = Executors.newFixedThreadPool(5);
Executors
还可以创建其他类型的线程池,如CachedThreadPool
(根据需要创建线程,线程空闲一段时间后会被回收)、SingleThreadExecutor
(只有一个线程的线程池)等。 - 定义任务(可以是实现Runnable接口或Callable接口的类):例如,定义一个实现
Runnable
接口的任务类:class MyTask implements Runnable { @Override public void run() { System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行任务"); } }
- 提交任务到线程池:将任务提交给线程池执行。对于实现
Runnable
接口的任务,可以使用execute()
方法提交;对于实现Callable
接口的任务(用于有返回值的情况),可以使用submit()
方法提交。以下是提交Runnable
任务的示例:ExecutorService executorService = Executors.newFixedThreadPool(5); // 创建任务对象 MyTask task = new MyTask(); // 提交任务到线程池,这个任务会被线程池中的某个线程执行 executorService.execute(task); // 关闭线程池,不再接受新任务,但会等待已提交的任务执行完毕 executorService.shutdown();
- 关闭线程池(可选):当任务都提交完毕后,可以关闭线程池。
shutdown()
方法会平滑地关闭线程池,它会等待所有已提交的任务执行完毕后再关闭线程池。如果希望立即关闭线程池,不等待未完成的任务,可以使用shutdownNow()
方法,但这种方式可能会导致正在执行的任务被中断。
4.4.2 自定义线程池(使用ThreadPoolExecutor类)
除了使用Executors
工厂类创建线程池外,还可以通过ThreadPoolExecutor
类来自定义线程池的参数,以满足更复杂的需求。
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Main {
public static void main(String[] args) {
// 核心线程数
int corePoolSize = 2;
// 最大线程数
int maximumPoolSize = 4;
// 线程空闲时间
long keepAliveTime = 10;
// 时间单位
TimeUnit unit = TimeUnit.SECONDS;
// 任务队列
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(10);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue);
for (int i = 0; i < 15; i++) {
final int taskNumber = i;
executor.submit(() -> {
System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行任务 " + taskNumber);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
executor.shutdown();
}
}
在这个示例中,我们创建了一个自定义的线程池,通过ThreadPoolExecutor
的构造函数设置了核心线程数、最大线程数、线程空闲时间、任务队列等参数。
4.4.3 线程池的优缺点分析
- 优点:
- 提高性能:避免了频繁创建和销毁线程带来的开销,线程池中的线程可以复用,减少了线程创建和销毁的时间和资源消耗。
- 控制并发数:可以通过设置线程池的大小来控制并发执行的线程数量,避免过多的线程竞争
Java, 多线程,线程创建,零基础入门,新手教程,并发编程,线程池,Thread 类,Runnable 接口,Callable 接口,Future, 多线程同步,volatile, 锁机制,Java 多线程实战
资源地址:
https://pan.quark.cn/s/14fcf913bae6