Java | 线程 - 基础概念、创建方式

0 阅读9分钟

在现代软件开发中,多线程并发编程是构建高性能、高响应系统的核心能力。无论是 Web 服务器【消息队列、大数据处理、游戏服务器等】处理海量请求,还是大数据并行计算,亦或是移动端流畅的 UI 交互,都离不开多线程的支持,几乎无处不在。

一、基础概念

1.1 进程 vs 线程

  • 进程 (Process):拥有独立内存空间、资源,进程间通信成本高(IPC)

    操作系统资源分配的最小单位。比如你打开一个 Chrome 浏览器,就是一个进程。它拥有独立的内存空间和系统资源。

    image-20260226104445567

  • 线程 (Thread):CPU 调度的最小单位,是进程内部的执行流。

    一个进程可以包含多个线程,它们共享进程的内存和资源(如堆内存),但拥有独立的栈内存(栈、本地方法栈)和程序计数器(PC)

比喻:进程好比一个“工厂”,线程就是工厂里的“工人”。工厂提供场地和原料(内存资源),工人们(线程)各自独立工作,但共享工厂的资源。

1.2 并发 vs 并行

1.2.1 并发

并发(Concurrency):任务之间的组织与协调。 - "响应性问题" - 设计问题(怎么把事拆开)

目的:提高系统的响应能力,避免阻塞,更好地管理复杂性。

并发是同时处理多个事情。它涉及构建程序以同时处理多个任务,任务可以在重叠的时间段内开始、运行和完成,但不一定在同一时刻。

宏观上同时运行,微观上交替执行。看上去同时在跑,但可能在同一个 CPU 核心上通过时间片切换完成。

一个人接多个电话,快速在几个电话之间切换——每个电话都在推进,但任何时刻这个人只在说一个电话。

  • Thread、Runnable、Callable

  • 线程池(ExecutorService)、任务队列

  • 锁、同步机制(synchronized、Lock)

    结构(design / composition)的: 把程序分解 / 组织成多个独立推进的执行流(可以是进程、线程、协程、任务、actor 等),让它们在逻辑时间上有重叠(overlapping in time),从而应对(deal with) 多个事情。

核心是“同时管理 / 处理多件事的能力”,不要求它们在物理上同时执行。

1.2.2 并行

并行(Parallelism):利用硬件资源提高吞吐/性能 - 吞吐量问题 - 运行问题(怎么把事跑快)。

目的:提高系统的吞吐量,加快计算速度。

并行性则指同时执行多个计算。它是同时运行两个或多个任务或计算的技术,利用计算机内的多个处理器或核心同时执行多个作。

并行性需要配备多个处理单元的硬件,其主要目标是提高系统的吞吐量和计算速度。

真正的同时运行。只有在多核 CPU 上,多个线程才能分配到不同的核心上真正同时执行。

几个人各拿一个电话,同时打给不同的人说话,真正“一起干”。

  • 多核 CPU 上多个线程真正同时运行

  • ForkJoinPool、parallelStream()、并行排序等

    执行(execution / runtime)的: 多个计算任务在物理同一时刻真正同时推进(simultaneous / at the same physical instant),通常需要多个独立的执行单元(多核、多处理器、GPU、SIMD 向量单元等)。

核心是“同时做多件事”,目的是加速或提升吞吐。

1.2.3 总结

参考链接:《Concurrency vs Parallelism: Understanding the Difference》

https://www.linkedin.com/posts/alexxubyte_systemdesign-coding-interviewtips-activity-7397664946857345024-UPNU

Concurrency is about dealing with lots of things at once.

翻译:并发是关于同时应对很多事情

Parallelism is about doing lots of things at once.

翻译:并行是关于同时做很多事情。

  1. 并发是程序设计层面的能力:让多个独立的任务逻辑上重叠推进,以更好地应对复杂、多变的 workload;
  2. 并行是程序运行层面的现象:多个任务在物理同一时刻被不同执行单元同时推进,以加速计算。

请添加图片描述

二、Java 创建线程的四种方式

  • 不需要结果:优先用 Runnable + 线程池
  • 需要结果:优先用 Callable + 线程池(submit 返回 Future
  • 尽量避免直接频繁 new Thread(),线上服务基本都应该用线程池统一管理线程。

参考代码链接:

https://github.com/lzyzy1214/LearnThread.git
方式实现方式是否有返回值异常处理对继承的影响资源利用典型使用场景
继承 Thread继承 Thread,重写 run()
new Thread().start()
内部捕获占用唯一继承链、
不灵活
频繁 new Thread,性能一般教学示例、
小脚本、
一次性简单任务
实现 Runnable接口实现 Runnable
作为参数传给 Thread 或池
内部捕获不影响继承层次(接口)可以和线程池搭配,较好无返回值的异步任务,
多用于线程池
实现 Callable
接口+ Future
实现 Callable
通过 FutureTasksubmit 提交
可抛出受检异常,
Future.get() 统一处理
接口,不影响继承结合线程池效果最好需要结果的异步计算、
任务并行汇总
线程池 ExecutorService通过 ExecutorService
/ThreadPoolExecutor 管理线程
取决于任务类型
(支持有/无返回值)
通常在任务内部/Future 层处理不相关线程复用,
资源可控,性能最好
高并发、生产服务、
长期运行应用的首选

2.1 方式 1:继承 Thread 类

实现方式

class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行体
    }
}

new MyThread().start();
  • 通过继承 Thread,重写 run() 方法。
  • 通过调用 start() 启动线程。
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 执行中...");
    }
}

// 使用
public static void main(String[] args) {
    new MyThread().start();
}

特点

  • 返回值: 无法直接得到任务执行结果(只能通过共享变量、回调等方式间接获取)。
  • 异常: run() 中异常不会向外抛出,只能在内部捕获处理。
  • 多继承限制: Java 单继承,如果一个类已经继承了其他父类,就不能再继承 Thread
类型具体点说明
优点实现简单直观对初学者友好。
语义直接把“线程”与“任务”绑在一起,概念上比较直观。
缺点扩展性差由于 Java 单继承特性,无法再继承其他类。
不利于复用与管理任务与线程耦合,无法分离,导致代码复用性低。
功能受限不支持返回值;不利于统一调度(通常难以与线程池良好整合)。

适用场景

  • 教学/演示,简单的小示例。
  • 临时快速写一个很简单的异步任务,对结构要求不高的情况。

image-20260226164425344


2.2 方式2:实现 Runnable 接口

实现方式

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行体
    }
}

// 直接使用 Thread
Thread t = new Thread(new MyRunnable());
t.start();

image-20260226173159579

或使用 Lambda:

new Thread(() -> {
    // 线程执行体
}).start();

image-20260226173250156

class MyTask implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " 执行任务...");
    }
}

// 使用(推荐)
Thread t = new Thread(new MyTask(), "业务线程-1");
t.start();

特点

  • 返回值: 同样无法直接返回结果(可以通过共享变量、回调等方式)。
  • 异常: run() 无受检异常声明,异常需在内部捕获。
  • 多继承: 不再受单继承限制,可以继承其他类的同时实现 Runnable
类型具体点说明
优点任务与线程解耦Runnable 仅表示“要做的事”,Thread 表示“谁来做”,职责分离清晰。
利于线程池整合天然适配 ExecutorService.submit(Runnable) 等线程池接口,便于统一管理。
结构灵活避免了 Java 单继承的限制,实现类可以同时继承其他父类。
缺点无返回值能力无法直接获取执行结果(需借助共享变量或回调机制变通实现)。
异常处理不直观异常必须在 run() 方法内部捕获处理,无法像普通方法调用那样向外抛出。

适用场景

  • 大部分不需要返回值的异步任务。
  • 推荐用于:需要和线程池配合的任务,或需要保持类继承层次灵活的场景。

2.3 方式3: 实现 Callable + FutureTask / 线程池提交

实现方式

方式 A:Callable + FutureTask + Thread

import java.util.concurrent.*;

class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 有返回值,可抛出异常
        return 42;
    }
}

Callable<Integer> task = new MyCallable();
FutureTask<Integer> futureTask = new FutureTask<>(task);
new Thread(futureTask).start();

Integer result = futureTask.get(); // 阻塞获得结果

方式 B:配合线程池(更常见)

ExecutorService pool = Executors.newFixedThreadPool(2);

Future<Integer> future = pool.submit(() -> {
    // call() 的语义
    return 42;
});

Integer result = future.get();
class ComputeTask implements Callable<Long> {
    @Override
    public Long call() throws Exception {
        long sum = 0;
        for (long i = 1; i <= 100_0000L; i++) sum += i;
        return sum;
    }
}

// 使用
FutureTask<Long> task = new FutureTask<>(new ComputeTask());
new Thread(task, "计算线程").start();

Long result = task.get();   // 阻塞等待结果

特点

  • 返回值: Callable<V>call() 方法可以返回结果。
  • 异常: call() 可以抛出受检异常,异常会封装在 Future.get() 抛出的 ExecutionException 中。
  • 通常通过 Future/FutureTask 来获取结果、判断是否完成、取消任务等。

优点

  • 可以获取任务结果,支持返回值
  • 支持在接口层面抛出受检异常,异常处理路径清晰。
  • 适合计算型任务/需要结果的异步调用。
  • 和线程池配合非常自然。

缺点

  • 相比 Runnable/Thread实现和使用略复杂(多了 Future/FutureTask 的概念)。
  • 如果大量直接 new Thread(new FutureTask(...)),依然会有频繁创建销毁线程的问题(因此最好配合线程池)。

适用场景

  • 需要线程执行结果的任务(如异步计算、远程调用结果返回等)。
  • 需要未来某个时刻再取结果(Future.get())的场景。
  • 异步执行 + 结果聚合(如并行计算后合并结果)。

image-20260226173741535

2.4 方式4:使用线程池(ExecutorService / ThreadPoolExecutor 等)

实现方式(典型)

import java.util.concurrent.*;

ExecutorService pool = Executors.newFixedThreadPool(4);

// 提交无返回值任务(Runnable)
pool.execute(() -> {
    // 任务内容
});

// 提交有返回值任务(Callable)
Future<Integer> future = pool.submit(() -> {
    return 42;
});

Integer result = future.get();

// 不再使用时关闭线程池
pool.shutdown();
ExecutorService executor = new ThreadPoolExecutor(
    4, 20, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(500),
    new ThreadFactoryBuilder().setNameFormat("pool-task-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);

executor.submit(() -> {
    // 业务逻辑
});

特点

  • 线程复用:线程由池统一管理,任务来了就投递到池中,由空闲线程执行。
  • 同时兼容 RunnableCallable
  • 可配置核心线程数、最大线程数、队列类型、拒绝策略等。

优点

  • 性能 & 资源利用好:避免频繁创建/销毁线程的开销,减少内存和 CPU 切换开销。
  • 可控性强:可以统一管理线程数量,防止“开太多线程”把系统拖垮。
  • 统一管理:方便统一设置线程工厂、命名、异常处理逻辑等。
  • 更适合高并发、生产环境,几乎所有中大型项目都会使用线程池而不是裸 new Thread()

缺点

  • 需要设计和配置:池大小、队列、拒绝策略等配置不当可能导致任务堆积、拒绝、OOM 等问题。
  • 相比简单 new Thread()上手成本略高
  • 需要关注池的生命周期(何时 shutdown() / shutdownNow())。

适用场景

  • 高并发 / 频繁提交任务的场景(Web 服务器、RPC 服务、定时任务调度等)。
  • 延迟任务、周期性任务(配合 ScheduledThreadPoolExecutor)。
  • 几乎所有生产环境长期运行的服务器端程序,推荐首选线程池而不是手动创建线程。

3个池子,计算平方