0

0

有哪几种方式可以创建一个线程?哪种方式更好?

紅蓮之龍

紅蓮之龍

发布时间:2025-09-04 17:44:01

|

514人浏览过

|

来源于php中文网

原创

使用ExecutorService线程池是创建线程的最佳方式,因其能有效管理资源、控制并发、复用线程并提供任务队列和高级抽象,避免频繁创建线程带来的性能开销与系统风险,同时支持Callable返回结果和统一生命周期管理,适用于绝大多数生产场景。

有哪几种方式可以创建一个线程?哪种方式更好?

在Java中,创建线程主要有三种方式:继承

Thread
类、实现
Runnable
接口,以及利用
ExecutorService
线程池。如果问哪种方式更好,我个人会毫不犹豫地推荐使用
ExecutorService
线程池。它不仅提供了更高级的抽象,能够更好地管理线程资源,还能有效避免直接创建和管理线程带来的诸多问题。当然,理解前两种基础方式是深入掌握并发编程的基石,它们各有适用场景,但对于大多数实际的生产环境应用,线程池无疑是更健壮、更高效的选择。

解决方案

创建线程的几种核心方式各有其哲学和应用场景。我们来逐一剖析。

1. 继承

Thread

这是最直观的方式之一。你创建一个新类,让它继承自

java.lang.Thread
,然后重写
run()
方法,将线程的执行逻辑放在这个方法里。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Hello from a custom Thread!");
        // 线程执行的业务逻辑
    }
}

// 使用
MyThread thread = new MyThread();
thread.start(); // 启动线程

这种方式的优点是简单明了,代码量少。但缺点也很明显:Java是单继承的,如果你的业务类已经继承了其他类,就无法再继承

Thread
了。这在实际项目中是很大的限制。此外,将任务逻辑与线程本身紧密耦合,使得代码复用性变差。

2. 实现

Runnable
接口

这是更常用也更推荐的基础方式。你创建一个类实现

java.lang.Runnable
接口,然后实现
run()
方法。这个
Runnable
对象可以作为参数传递给
Thread
类的构造器,再由
Thread
对象来启动。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Hello from a Runnable task!");
        // 线程执行的业务逻辑
    }
}

// 使用
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start(); // 启动线程

相较于继承

Thread
,实现
Runnable
的优势在于:

  • 解耦: 任务(
    Runnable
    )与线程(
    Thread
    )是分离的,任务可以独立于任何线程类存在。
  • 单继承限制: 你的业务类可以自由继承其他类,因为接口可以多实现。
  • 资源共享: 多个线程可以共享同一个
    Runnable
    实例,这对于需要共享数据的场景很有用。

3. 使用

ExecutorService
(线程池)

这是现代Java并发编程的主流方式,也是我个人认为“更好”的方式。

ExecutorService
java.util.concurrent
包提供的高级并发工具,它管理着一个线程池,负责线程的创建、销毁和复用。你只需将任务提交给
ExecutorService
,它会自行安排线程来执行。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// 任务仍然是实现 Runnable 接口
class MyPooledTask implements Runnable {
    private String taskName;

    public MyPooledTask(String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        System.out.println("Executing task: " + taskName + " on thread: " + Thread.currentThread().getName());
        // 模拟耗时操作
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

// 使用
// 创建一个固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(5);

for (int i = 0; i < 10; i++) {
    executor.submit(new MyPooledTask("Task-" + i)); // 提交任务
}

executor.shutdown(); // 关闭线程池,等待所有任务执行完毕

使用线程池的好处是压倒性的:

  • 资源管理: 避免了频繁创建和销毁线程的开销,提高了系统性能。
  • 控制并发: 可以限制并发线程的数量,防止资源耗尽。
  • 任务队列: 未执行的任务会排队等待,平滑处理突发流量。
  • 更高级的抽象: 提供了
    Callable
    接口,可以返回执行结果(通过
    Future
    对象)并抛出受检异常,这是
    Runnable
    不具备的。
  • 统一管理: 方便对线程进行监控、统计和关闭。

为什么说使用线程池管理线程是更优的选择?

在我看来,选择线程池并非仅仅是“好一点”,而是现代并发编程的基石。它解决了直接创建线程时面临的诸多痛点,从系统设计的角度看,是一种更成熟、更健壮的方案。

首先,资源开销是首要考量。每次

new Thread()
都会涉及操作系统层面的资源分配和销毁,这开销不小。想象一下,如果你的应用每秒需要处理几百个短生命周期的任务,每次都创建新线程,系统的负担会迅速飙升,最终可能导致性能瓶颈甚至崩溃。线程池通过线程复用机制,将这些开销降到最低。它预先创建好一定数量的线程,当有任务到来时,直接从池中取出空闲线程执行,任务完成后线程归还池中,等待下一个任务。这就像一个高效的工人团队,无需每次都招聘新员工,而是让现有员工轮流处理工作。

其次,并发控制是线程池的另一个核心价值。直接创建线程时,你很难有效控制同时运行的线程数量。如果任务量激增,无限创建线程可能迅速耗尽系统资源(如内存、CPU),导致“线程爆炸”。线程池允许你设定最大并发线程数,未执行的任务会进入等待队列。这提供了一个天然的流量削峰机制,保证系统在面对高并发时依然能够稳定运行,不至于瞬间崩溃。比如,一个Web服务器,如果每个请求都创建一个新线程,很容易在高负载下变得不稳定,而使用线程池就能优雅地处理并发请求

再者,任务管理与扩展性

ExecutorService
不仅支持
Runnable
这种“只管执行,不关心结果”的任务,还引入了
Callable
接口。
Callable
任务可以返回一个结果,并且可以抛出异常,这使得异步任务的处理更加灵活和强大。配合
Future
对象,我们甚至可以取消任务、检查任务是否完成,或者阻塞等待任务结果。这对于需要进行复杂计算并获取结果的场景非常有用。例如,你可能需要并行计算多个子任务,然后汇总它们的结果,
Callable
Future
就是为此而生的。

最后,统一的生命周期管理和可观测性。直接创建的线程,其生命周期管理相对分散,你很难统一关闭所有线程或监控它们的运行状态。而线程池提供了一套标准的生命周期管理API(如

shutdown()
shutdownNow()
awaitTermination()
),可以优雅地关闭所有线程,确保资源被正确释放。同时,许多监控工具和框架也能更好地与线程池集成,提供更细粒度的性能指标和状态报告。这对于生产环境的运维和故障排查至关重要。

在实际开发中,如何根据任务特性选择合适的线程创建方式?

选择合适的线程创建方式,实际上是在权衡简单性、性能、资源管理和复杂性。这需要我们深入理解任务本身的特性以及系统对并发的需求。

1. 简单、一次性的独立任务(通常不推荐直接使用)

如果你有一个非常简单、生命周期极短、且确定只运行一次的任务,理论上你可以选择继承

Thread
或实现
Runnable
。例如,一个简单的后台日志清理任务,或者一个启动时进行初始化检查的线程。

  • 继承
    Thread
    如果你的任务类不需要继承其他类,且任务逻辑与线程本身耦合度高(或者说,你觉得这样写最直观),可以考虑。但即便如此,我也倾向于用
    Runnable
  • 实现
    Runnable
    这是更好的选择,因为它将任务逻辑与线程执行机制分离。即使是简单任务,这种解耦也能带来更好的代码组织和复用性。

然而,我的建议是:即使是这种场景,也应该考虑使用

Executors.newSingleThreadExecutor()
它能提供一个单线程的线程池,既能保证任务按序执行,又能享受线程池的资源管理和生命周期控制,避免了手动创建
Thread
带来的潜在问题。

MediPro企业网站管理系统
MediPro企业网站管理系统

一款基于PHP+MYSQL开发的企业网站管理软件,具有灵活的栏目内容管理功能和丰富的网站模版,可用于创建各种企业网站。v5.1版本支持了PHP5+MYSQL5环境,前台网站插件开放源码,更利于个性化的网站开发。具有以下功能特点和优越性:[>]模版精美实用具有百款适合企业网站的精美模版,并在不断增加中[>]多语言支持独立语言包,支持GBK,UTF8编码方式,可用于创建各种语言的网站[&g

下载

2. 大多数业务场景:需要高效管理、复用线程,控制并发的任务

这几乎涵盖了所有生产环境中的并发场景,包括但不限于:Web服务器的请求处理、异步消息处理、后台批处理任务、并行计算、定时任务等。

  • ExecutorService
    是不二之选。
    在这种情况下,关键在于选择合适的线程池类型:
    • Executors.newFixedThreadPool(int nThreads)
      适用于CPU密集型任务。线程数通常设置为CPU核心数或核心数+1。它会创建固定数量的线程,如果任务多于线程数,任务会进入队列等待。
      • 示例场景: 大量需要进行复杂计算、数据处理的任务。
    • Executors.newCachedThreadPool()
      适用于I/O密集型任务任务数量波动大的场景。它会根据需要创建新线程,如果线程空闲时间超过一定阈值(60秒),则会被回收。线程数没有上限。
      • 示例场景: 网络请求、数据库操作、文件读写等,这些任务大部分时间在等待I/O操作完成,CPU占用不高。
    • Executors.newSingleThreadExecutor()
      适用于需要保证所有任务按提交顺序依次执行的场景。它内部只有一个工作线程。
      • 示例场景: 顺序日志写入、资源访问需要严格串行化的任务。
    • Executors.newScheduledThreadPool(int corePoolSize)
      适用于需要定时执行或周期性执行任务的场景。
      • 示例场景: 定时数据同步、周期性报告生成。

3. 需要获取任务执行结果或处理异常的任务

如果你提交的任务不仅需要执行,还需要返回一个结果,或者你希望能够捕获任务执行过程中抛出的异常。

  • ExecutorService
    配合
    Callable
    Future
    Callable
    接口允许
    call()
    方法返回一个值,并抛出异常。
    ExecutorService.submit(Callable task)
    会返回一个
    Future
    对象,你可以通过
    future.get()
    获取任务结果或捕获异常。
import java.util.concurrent.*;

class MyCallableTask implements Callable {
    private String name;

    public MyCallableTask(String name) {
        this.name = name;
    }

    @Override
    public String call() throws Exception {
        System.out.println("Callable task " + name + " started.");
        Thread.sleep(200); // 模拟耗时操作
        if (Math.random() < 0.2) {
            throw new RuntimeException("Error in task " + name);
        }
        return "Result from " + name;
    }
}

// 使用
ExecutorService executor = Executors.newFixedThreadPool(3);
Future future1 = executor.submit(new MyCallableTask("Task A"));
Future future2 = executor.submit(new MyCallableTask("Task B"));

try {
    System.out.println(future1.get()); // 阻塞直到任务完成并获取结果
    System.out.println(future2.get());
} catch (InterruptedException | ExecutionException e) {
    System.err.println("Task failed: " + e.getMessage());
} finally {
    executor.shutdown();
}

总结一下,在实际开发中,除非有非常特殊的理由(例如,你正在编写一个底层的并发库,需要对

Thread
有极致的控制),否则我几乎总是推荐使用
ExecutorService
。它将线程管理的复杂性从你的业务逻辑中抽象出来,让你能够专注于任务本身,同时提供了强大的性能和稳定性保证。

创建线程时常见的误区有哪些,以及如何避免?

在并发编程中,创建和管理线程远非表面看起来那么简单。许多开发者在实践中会踩到一些坑,这些误区如果不加以注意,轻则影响性能,重则导致系统崩溃或数据错误。

1. 无限制地创建新线程(线程爆炸)

这是最常见也最危险的误区之一。当每个请求或每个任务都

new Thread()
时,系统很快就会因为创建过多线程而耗尽资源。每个线程都需要占用一定的内存(线程栈),并且CPU在大量线程之间切换(上下文切换)也会带来巨大的开销。

  • 如何避免: 几乎所有场景都应该使用线程池(
    ExecutorService
    。通过线程池,你可以限制并发线程的数量,将超出的任务放入队列等待,从而保护系统资源。选择合适的线程池类型和大小是关键。

2. 忽视线程安全问题

当多个线程访问和修改共享数据时,如果没有适当的同步机制,就可能出现竞态条件(Race Condition)、数据不一致等问题。这通常是并发编程中最难调试的bug。

  • 如何避免:
    • 最小化共享数据: 尽可能让每个任务处理自己的数据,减少对共享状态的依赖。
    • 使用同步机制: 对共享资源的访问进行同步。Java提供了多种工具:
      • synchronized
        关键字(方法或代码块)。
      • java.util.concurrent.locks.Lock
        接口及其实现类(如
        ReentrantLock
        ),提供更灵活的锁定机制。
      • java.util.concurrent.atomic
        包下的原子类(如
        AtomicInteger
        ),用于对基本类型或引用进行原子操作。
      • java.util.concurrent
        包下的并发集合(如
        ConcurrentHashMap
        ),它们是线程安全的。
    • 使用不可变对象: 不可变对象一旦创建就不能修改,天然线程安全。

3. 错误地终止线程(使用

Thread.stop()
等废弃方法)

Thread.stop()
Thread.suspend()
Thread.resume()
等方法已经被标记为废弃(deprecated)并且非常危险。它们可能导致线程在执行到一半时突然停止,从而释放未完成的锁,造成数据不一致或死锁。

  • 如何避免: 应该使用协作式中断机制
    • 通过
      Thread.interrupt()
      方法向线程发送中断请求。
    • 在线程的
      run()
      call()
      方法中,定期检查
      Thread.currentThread().isInterrupted()
      标志。
    • 当捕获到
      InterruptedException
      时,通常应该重新设置中断标志 (
      Thread.currentThread().interrupt();
      ) 并决定如何响应(例如,优雅地退出任务)。

4. 线程中未捕获的异常

如果一个线程在执行

run()
call()
方法时抛出了一个未捕获的异常,并且没有设置
UncaughtExceptionHandler
,那么这个异常会直接导致线程终止,但不会传播到主线程,可能会默默地导致程序状态异常。

  • 如何避免:
    • run()
      call()
      方法内部使用
      try-catch
      块捕获所有可能的异常。
    • 为线程设置
      Thread.UncaughtExceptionHandler
      ,以便在线程因未捕获异常而终止时进行处理(例如,记录日志、重启任务)。
    • 对于
      Callable
      任务,通过
      future.get()
      获取结果时,如果任务抛出异常,
      get()
      方法会抛出
      ExecutionException
      ,可以从中获取原始异常。

5. 混淆

Thread.start()
Thread.run()

新手常犯的错误是调用

Thread
对象的
run()
方法而不是
start()
方法。调用
run()
方法只会把
run()
方法当作一个普通方法在当前线程中执行,并不会启动一个新的线程。

  • 如何避免: 始终调用
    Thread.start()
    方法来启动新线程。

6. 不正确地关闭线程池

如果应用程序退出时没有正确关闭线程池,可能会导致线程资源泄露,或者应用程序无法正常退出,因为后台线程池还在运行。

  • 如何避免: 在应用程序关闭时,调用
    ExecutorService
    shutdown()
    方法。
    shutdown()
    会阻止新任务提交,并等待已提交任务执行完成。如果需要立即停止所有任务,可以使用
    shutdownNow()
    。通常会结合
    awaitTermination()
    来等待线程池中的任务完成。
executor.shutdown(); // 拒绝新任务,等待已提交任务完成
try {
    if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { // 等待60秒
        executor.shutdownNow(); // 强制关闭
        if (!executor.awaitTermination(60, TimeUnit.SECONDS))
            System.err.println("Pool did not terminate");
    }
} catch (InterruptedException ie) {
    executor.shutdownNow();
    Thread.currentThread().interrupt();
}

理解并避免这些误区,是写出健壮、高效并发程序的关键。并发编程的复杂性在于其非确定性,因此在设计和实现时,需要格外小心和细致。

相关专题

更多
java
java

Java是一个通用术语,用于表示Java软件及其组件,包括“Java运行时环境 (JRE)”、“Java虚拟机 (JVM)”以及“插件”。php中文网还为大家带了Java相关下载资源、相关课程以及相关文章等内容,供大家免费下载使用。

835

2023.06.15

java正则表达式语法
java正则表达式语法

java正则表达式语法是一种模式匹配工具,它非常有用,可以在处理文本和字符串时快速地查找、替换、验证和提取特定的模式和数据。本专题提供java正则表达式语法的相关文章、下载和专题,供大家免费下载体验。

741

2023.07.05

java自学难吗
java自学难吗

Java自学并不难。Java语言相对于其他一些编程语言而言,有着较为简洁和易读的语法,本专题为大家提供java自学难吗相关的文章,大家可以免费体验。

736

2023.07.31

java配置jdk环境变量
java配置jdk环境变量

Java是一种广泛使用的高级编程语言,用于开发各种类型的应用程序。为了能够在计算机上正确运行和编译Java代码,需要正确配置Java Development Kit(JDK)环境变量。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

397

2023.08.01

java保留两位小数
java保留两位小数

Java是一种广泛应用于编程领域的高级编程语言。在Java中,保留两位小数是指在进行数值计算或输出时,限制小数部分只有两位有效数字,并将多余的位数进行四舍五入或截取。php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

399

2023.08.02

java基本数据类型
java基本数据类型

java基本数据类型有:1、byte;2、short;3、int;4、long;5、float;6、double;7、char;8、boolean。本专题为大家提供java基本数据类型的相关的文章、下载、课程内容,供大家免费下载体验。

446

2023.08.02

java有什么用
java有什么用

java可以开发应用程序、移动应用、Web应用、企业级应用、嵌入式系统等方面。本专题为大家提供java有什么用的相关的文章、下载、课程内容,供大家免费下载体验。

430

2023.08.02

java在线网站
java在线网站

Java在线网站是指提供Java编程学习、实践和交流平台的网络服务。近年来,随着Java语言在软件开发领域的广泛应用,越来越多的人对Java编程感兴趣,并希望能够通过在线网站来学习和提高自己的Java编程技能。php中文网给大家带来了相关的视频、教程以及文章,欢迎大家前来学习阅读和下载。

16926

2023.08.03

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

68

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Django 教程
Django 教程

共28课时 | 3.2万人学习

React 教程
React 教程

共58课时 | 3.8万人学习

SciPy 教程
SciPy 教程

共10课时 | 1.2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号