一、进程与线程

1. 进程

        核心定义进程是操作系统进行资源分配和调度的基本单位。

        进程是操作系统中的一个执行实例,是系统进行资源分配和调度的基本单位。每个进程拥有独立的虚拟地址空间、执行环境(如打开的文件句柄、安全上下文等)和系统资源。从技术角度看,进程由程序代码、数据段、堆栈段、进程控制块(PCB)和系统资源组成。

  • 独立性:每个进程拥有自己独立的虚拟内存空间,一个进程崩溃通常不会影响其他进程。这是进程最核心的优势。
  • 资源开销大:创建、销毁或切换进程需要分配或回收大量资源(如内存),成本很高。
  • 通信复杂:因为内存空间独立,进程间通信需要采用特定的IPC机制,如管道、消息队列、共享内存等,相对麻烦。

例子:在电脑上同时打开Chrome浏览器和Word文档,它们就是两个不同的进程。

2. 线程

        核心定义线程是进程内部的一个执行流,是CPU调度和执行的最小单位。

        线程进程内的一个执行单元,是处理器调度和执行的最小单位。线程属于创建它的进程,共享该进程的所有资源和内存空间,但拥有独立的执行栈、寄存器状态和线程控制块(TCB)。同一进程内的多个线程可以并发执行。

  • 共享资源:同一进程下的所有线程共享相同的内存和资源,这使得数据共享变得非常高效和简单。
  • 开销小:创建、销毁或切换线程的成本远低于进程,因为不需要分配新的内存空间等资源。
  • 通信简单:由于共享内存,线程间可以直接读写同一份数据。
  • 缺乏独立性:一个线程崩溃,会导致整个进程崩溃,从而影响该进程下的所有其他线程。
特性进程线程
基本单位资源分配的基本单位CPU调度和执行的基本单位
资源开销大(独立内存空间,创建/销毁/切换成本高)小(共享内存空间,创建/销毁/切换成本低)
数据共享复杂,需要IPC(进程间通信)机制简单,可直接读写共享的进程数据
内存隔离拥有独立的虚拟内存空间,互不干扰,安全共享其所属进程的内存和资源,缺乏隔离
健壮性。一个进程崩溃通常不会影响其他进程。一个线程崩溃会导致整个进程崩溃
包含关系一个进程可以包含多个线程线程是进程的一部分,必须依赖于进程存在

3.多线程

(1).多线程的优势

提升性能(并发执行)

        I/O密集型任务:当一个线程在等待网络响应或磁盘读写时(这种等待很常见),CPU可以立刻切换到另一个线程去执行计算任务,大大提高了CPU的利用率和程序的响应速度。例如,在Word中,一个线程处理你的输入,另一个线程在后台进行拼写检查。

        计算密集型任务:在多核CPU上,可以将计算任务拆分给多个线程,让它们在不同的核心上真正并行执行,从而缩短总计算时间。例如,视频渲染、科学计算。

简化通信和资源共享

        线程间共享内存非常简单,无需像进程通信那样调用复杂的IPC机制。对于需要频繁交换数据的任务(如游戏引擎、数据库连接池),多线程模型更自然、高效。

更轻量

        创建和销毁线程比进程快得多,上下文切换(保存当前状态,加载下一个状态)的代价也更小。

二、并发和并行

1.并发

  • 定义并发指的是系统具有处理多个任务的能力。这些任务在时间上是重叠的,但在任意给定时刻,可能只有一个任务正在被执行。它强调的是结构设计和问题分解。
  • 核心交替执行。通过快速切换上下文,在宏观上看起来多个任务“同时”在前进,但在微观上(极短的时间片内)是串行的。
  • 实现条件:并发可以在单核处理器上实现。

2. 并行

  • 定义并行指的是系统具有同时执行多个任务的能力。这些任务在同一时刻确实在不同的计算单元上一起执行。
  • 核心同时执行。它需要硬件支持,是真正的“同时发生”。
  • 实现条件:并行必须在多核处理器(多CPU核心)、或多个处理器(如GPU、多路CPU)上才能实现。

        进程/线程是实现并发和并行的载体。多线程是并发编程的常见模型。在单核CPU上,多个线程是并发执行的(交替)。在多核CPU上,多个线程可以并行执行(真正同时)。多进程:同样既可以用于并发,也可以用于并行,取决于硬件资源。

三、多线程的实现方式

Java 提供了丰富且不断演进的并发 API,其多线程实现方式主要可以分为以下三大类:

方式一:继承 Thread 类

这是最直接的方式,但也是最不推荐的方式,因为它限制了类的继承结构(Java 是单继承)。

步骤:

  1. 创建一个继承自 Thread 类的新类。
  2. 重写 Thread 类的 run() 方法,将需要并发执行的任务代码放入其中。
  3. 创建该子类的实例。
  4. 调用实例的 start() 方法启动线程(注意:切勿直接调用 run() 方法)。
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程要执行的任务
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            try {
                Thread.sleep(500); // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        
        thread1.start(); // 启动第一个线程
        thread2.start(); // 启动第二个线程
        // thread1.run(); // 错误!这会在主线程中同步执行run(),而不是启动新线程。
    }
}

方式二:实现 Runnable 接口

        这是更常用和更灵活的方式。因为它允许类继承其他类,同时实现了 Runnable 接口,符合面向接口编程的原则。Thread 类本身的构造函数也接受一个 Runnable 对象。

步骤:

  • 创建一个实现了 Runnable 接口的类。
  • 实现 run() 方法。
  • 创建该实现类的实例,并将其作为 Target 传递给 Thread 类的构造函数。
  • 调用 Thread 实例的 start() 方法。
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        
        // 将MyRunnable实例作为参数传递给Thread构造函数
        Thread thread1 = new Thread(myRunnable, "MyThread-1"); // 可以指定线程名
        Thread thread2 = new Thread(myRunnable, "MyThread-2");
        
        thread1.start();
        thread2.start();

        // 使用Lambda表达式简化(Java 8+)
        Runnable r = () -> {
            System.out.println("Lambda thread is running");
        };
        new Thread(r).start();

        // 更简洁的Lambda写法
        new Thread(() -> System.out.println("Very concise lambda thread")).start();
    }
}

优势:

  • 解耦:任务(Runnable)与执行线程的机制(Thread)分离。
  • 灵活:实现类可以继承其他父类。
  • 共享资源:多个线程可以更容易地共享同一个 Runnable 实例的资源(但需注意线程安全!)。

方式三:实现 Callable 接口 + Future / FutureTask

Runnable 的 run() 方法没有返回值,也不能抛出受检异常。Callable 接口解决了这两个问题。

步骤:

  1. 创建一个实现了 Callable<V> 接口的类,泛型 V 是返回值的类型。
  2. 实现 call() 方法,该方法有返回值,并且可以抛出异常。
  3. 创建该实现类的实例。
  4. 使用 FutureTask<V> 来包装 Callable 对象。
  5. 将 FutureTask 对象作为 Runnable 传递给 Thread 构造函数并启动线程。
  6. 通过 FutureTask.get() 方法获取线程执行后的返回值(此方法会阻塞,直到线程计算完成)。
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<Integer> {
    private int count;

    @Override
    public Integer call() throws Exception {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + ": " + i);
            count += i;
            Thread.sleep(500);
        }
        return count; // 返回计算结果
    }
}

public class CallableExample {
    public static void main(String[] args) {
        MyCallable callable = new MyCallable();
        
        // 用FutureTask包装Callable
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        
        // FutureTask实现了RunnableFuture,而RunnableFuture继承了Runnable
        Thread thread = new Thread(futureTask, "CallableThread");
        thread.start();

        try {
            // do something else in main thread...
            
            // 获取子线程的返回值,会阻塞主线程直到计算完成
            Integer result = futureTask.get(); 
            System.out.println("Final count is: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

总结与最佳实践

方式优点缺点适用场景
继承 Thread编码简单无法继承其他类,不够灵活不推荐使用
实现 Runnable灵活,可继承其他类,可共享资源无法直接获取返回值实现无返回值异步任务的基础
实现 Callable+Future可获取返回值,可抛出异常使用稍复杂,get()会阻塞需要获取异步任务结果的场景

更多推荐