从零起步学习并发编程 || 第一章:初步认识进程与线程

一、进程 vs 线程

进程
  • 操作系统分配资源(内存、文件句柄、堆栈、地址空间等)的基本单位,可以理解为 "一个正在运行的程序"。一个进程至少包含一个线程(主线程)。进程间相互隔离,默认不能直接访问彼此内存(需要IPC:管道、Socket、共享内存等)。

线程
  • CPU 调度的基本单位,是进程内的执行流,是进程的 "最小执行单元"。线程共享进程的地址空间(堆、字符串常量池),但有自己的栈(本地方法栈和虚拟机栈)和程序计数器。线程创建开销比进程小,切换成本也低,但需要处理共享数据的一致性(同步问题)。

  • 总结:进程=资源隔离单位,线程=执行单位。多线程比多进程轻量,但并发需要注意竞态、可见性和原子性问题。

关键区别(表格对比)
维度 进程 线程
资源分配 操作系统分配资源的单位 共享所属进程的资源
独立性 进程间完全独立 线程间共享内存 / 变量等
开销 创建 / 销毁 / 切换开销大 创建 / 销毁 / 切换开销小
通信 需借助 IPC(管道 / 套接字) 直接共享变量(需同步)
稳定性 一个进程崩溃不影响其他 一个线程崩溃可能导致进程崩溃
举例
  • 进程:一家餐厅(独立的资源,如场地、厨具、食材)。
  • 线程:餐厅里的厨师、服务员、收银员(共享餐厅资源,各自执行不同任务)。

二、创建线程的 3 种方式(完整代码示例)

Java 中创建线程有 3 种标准方式,核心是最终都要关联可执行的任务逻辑

方式 1:继承 Thread 类(重写 run () 方法)
java 复制代码
// 步骤1:继承Thread类
class MyThread extends Thread {
    // 步骤2:重写run()方法,定义线程执行逻辑
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            // Thread.currentThread().getName() 获取当前线程名称
            System.out.println(Thread.currentThread().getName() + " 执行:" + i);
            try {
                // 模拟耗时操作
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试类
public class ThreadCreateDemo1 {
    public static void main(String[] args) {
        // 步骤3:创建线程对象
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        
        // 步骤4:启动线程(必须调用start(),而非直接run())
        thread1.setName("线程1");
        thread2.setName("线程2");
        thread1.start();
        thread2.start();
    }
}
  • 优点:直观,能直接调用 this.getName()Thread 方法。

  • 缺点:不能再继承其他类(Java 单继承),职责耦合较高,不够灵活。

方式 2:实现 Runnable 接口(推荐,解耦任务与线程)
java 复制代码
// 步骤1:实现Runnable接口
class MyRunnable implements Runnable {
    // 步骤2:实现run()方法,定义任务逻辑
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 执行:" + i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试类
public class ThreadCreateDemo2 {
    public static void main(String[] args) {
        // 步骤3:创建任务对象
        MyRunnable task = new MyRunnable();
        
        // 步骤4:将任务传入Thread对象
        Thread thread1 = new Thread(task, "线程A");
        Thread thread2 = new Thread(task, "线程B");
        
        // 步骤5:启动线程
        thread1.start();
        thread2.start();
    }
}
  • 优点:和业务逻辑解耦,可复用对象(实现单一职责),可以传递给线程池等。

  • 缺点:不能直接返回结果(但可通过共享变量 / Future 等方式)。

方式 3:实现 Callable 接口(带返回值 + 可抛异常)
java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

// 步骤1:实现Callable接口(指定返回值类型)
class MyCallable implements Callable<Integer> {
    // 步骤2:实现call()方法(带返回值、可抛异常)
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
            Thread.sleep(10);
        }
        return sum; // 返回计算结果
    }
}

// 测试类
public class ThreadCreateDemo3 {
    public static void main(String[] args) {
        // 步骤3:创建Callable任务对象
        MyCallable callable = new MyCallable();
        
        // 步骤4:通过FutureTask包装Callable(用于接收返回值)
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        
        // 步骤5:将FutureTask传入Thread
        Thread thread = new Thread(futureTask, "计算线程");
        thread.start();
        
        // 步骤6:获取返回值(get()会阻塞,直到线程执行完成)
        try {
            Integer result = futureTask.get();
            System.out.println("线程执行结果:1-100求和 = " + result);
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }
}
  • 优点:可以返回结果、抛出异常;与线程池配合能高效管理线程资源(推荐用于生产代码)。

  • 缺点:需要管理线程池生命周期(shutdown 等)。

三、start () vs run () 核心区别

  • start():由 JVM 启动一个新的 操作系统/虚拟机线程 ,并在新线程中调用 run()。这是创建并运行新线程的正确方式。

  • run():只是普通方法调用,在当前调用线程中执行,不会创建新线程。

java 复制代码
public class StartVsRun {
    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            System.out.println("Inside run(): " + Thread.currentThread().getName());
        }, "worker");

        t.start(); // 输出:Inside run(): worker  (在新线程)
        // vs
        t.run();   // 输出:Inside run(): main    (在当前线程,未创建新线程)
    }
}
方法 核心行为 是否创建新线程 调用方式
start() 告诉 JVM 启动线程,由 JVM 调用 run () 是(真正并发) 只能调用 1 次(多次抛异常)
run() 直接执行线程的任务逻辑 否(主线程执行) 可重复调用

四、线程生命周期(6 个状态)

Java 线程的生命周期由Thread.State枚举定义,共 6 个状态,状态流转如下:

  • NEW --- 线程已创建,但尚未调用 start()

  • RUNNABLE --- 线程可运行(在 Java 层是可运行状态),可能正在运行或在就绪队列等待 CPU。

  • BLOCKED --- 线程被阻塞,等待进入某个监视器(即等待 synchronized 锁)。

  • WAITING --- 无期限等待(例如 Object.wait() / Thread.join()(无超时)/ LockSupport.park())。

  • TIMED_WAITING --- 有期限等待(例如 Thread.sleep(ms) / Object.wait(timeout) / Thread.join(timeout) / LockSupport.parkNanos)。

  • TERMINATED --- run() 方法结束或抛出未捕获异常,线程终止。

常见状态转换(简化)
  • NEW --(start)--> RUNNABLE

  • RUNNABLE --(获得锁/运行)--> RUNNABLE(执行中)

  • RUNNABLE --(进入 synchronized 且锁被占用)--> BLOCKED

  • RUNNABLE --(调用 sleep/wait/park with timeout)--> TIMED_WAITING

  • RUNNABLE --(调用 wait/park/join 无超时)--> WAITING

  • RUNNABLE --(返回/抛异常)--> TERMINATED

示例:synchronized 导致 BLOCKEDsleep 导致 TIMED_WAITING;另一个线程 interrupt()sleep/wait 会抛 InterruptedException

五、线程常用方法都有哪些?

  • 创建/控制

    • start():启动新线程。

    • run():线程执行体;直接调用不会产生新线程。

    • isAlive():线程是否仍存活(未进入 TERMINATED)。

    • getName() / setName(String):线程名。

    • getId():线程 id(JVM 分配)。

    • setDaemon(boolean) / isDaemon():设置守护线程(JVM 在只剩守护线程时会退出)。

    • getPriority() / setPriority(int):线程优先级(平台相关,通常不推荐依赖)。

  • 等待 / 睡眠 / 同步相关

    • sleep(long millis):静态方法,使当前线程休眠(抛 InterruptedException)。Thread.sleep(1000);

    • yield():提示线程调度器当前线程愿意放弃 CPU,但不保证立即发生。

    • join() / join(long):等待目标线程终止(阻塞当前线程),常用于线程间等待结果。

    • wait() / wait(long) / notify() / notifyAll()Object 的方法,用于线程间协作(必须在同步块内调用)。

  • 中断

    • interrupt():向目标线程发送中断标志(不会强制停止线程),用于协作式取消。

    • isInterrupted():查询线程的中断状态(不清除)。

    • Thread.interrupted():静态方法,检查并清除当前线程的中断状态。

    • 在阻塞方法(sleep/wait/join)中,若线程被中断,会抛 InterruptedException,通常需要处理并决定是否传播中断(最好:catch 后重新设置中断 Thread.currentThread().interrupt();)。

  • 状态与异常

    • getState():返回 Thread.State

    • setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler):处理线程未捕获异常。

  • 已弃用 / 不安全的方法

    • suspend() / resume() / stop():已弃用,不要使用,会导致死锁或不一致状态。
  • 并发工具(不是 Thread 的方法,但常用)

    • ExecutorServiceCallableFutureCompletableFutureLockReentrantLock)、AtomicInteger 等更可控、更高效。

六、sleep方法与wait方法有什么区别?

  • Thread.sleep(long)线程类的静态方法 ,作用于当前执行的线程
  • Object.wait()所有对象的成员方法 ,必须在同步代码块 / 同步方法 中调用(持有对象锁),作用于持有该对象锁的线程
常见误区纠正
  1. 误区 1 :"wait () 后线程立即执行" 错误:wait () 被唤醒后,线程会进入BLOCKED状态(等待重新获取锁),只有获取到锁后,才会回到 RUNNABLE 状态执行; 正确:notify () 只是 "唤醒通知",不是 "直接执行"。

  2. 误区 2 :"sleep (0) 没用" 错误:Thread.sleep(0) 会触发 CPU 重新调度,让当前线程让出 CPU 执行权,给其他同优先级线程执行机会; 场景:用于多线程公平调度(如轮询任务)。

  3. 误区 3 :"wait () 可以不在 synchronized 中调用" 错误:JVM 强制要求 wait ()/notify ()/notifyAll () 必须在持有对象锁时调用,否则直接抛 IllegalMonitorStateException; 原理:防止 "虚假唤醒",保证等待 / 唤醒的原子性。

对比维度 Thread.sleep(long millis) Object.wait() / Object.wait(long timeout)
所属类 Thread(静态方法) Object(成员方法,所有对象都有)
锁的处理 不释放持有的锁(如果当前线程持有锁,休眠时锁仍保留) 必须释放持有的对象锁(等待期间锁归还给其他线程)
调用前提 无强制要求,任何地方都可调用 必须在 synchronized 代码块 / 方法中调用(否则抛 IllegalMonitorStateException
唤醒方式 1. 超时自动唤醒;2. 被中断(interrupt())唤醒 1. 其他线程调用 object.notify()/notifyAll();2. 超时自动唤醒(带参版本);3. 被中断唤醒
线程状态 进入 TIMED_WAITING(超时等待)状态 无参版:WAITING(等待);带参版:TIMED_WAITING
作用范围 作用于当前线程(静态方法,无法指定其他线程) 作用于持有当前对象锁的线程
使用场景 单纯的 "延时执行",不涉及线程间通信 线程间通信(等待 / 唤醒机制),如生产者 - 消费者模型
相关推荐
科技林总2 小时前
【系统分析师】6.3 企业信息化规划
学习
我爱娃哈哈2 小时前
SpringBoot + Flowable + 自定义节点:可视化工作流引擎,支持请假、报销、审批全场景
java·spring boot·后端
XiaoFan0122 小时前
将有向工作流图转为结构树的实现
java·数据结构·决策树
百炼成神 LV@菜哥2 小时前
Kylin Linux V10 aarch64 安装启动 TigerVNC-Server
linux·服务器·kylin
m0_737302583 小时前
百度智能云边缘云服务器,端云协同赋能全域智能场景
服务器
小突突突3 小时前
浅谈Java中的反射
java·开发语言
Anastasiozzzz3 小时前
LeetCode Hot100 295. 数据流的中位数 MedianFinder
java·服务器·前端
我真的是大笨蛋3 小时前
Redo Log详解
java·数据库·sql·mysql·性能优化
丝斯20113 小时前
AI学习笔记整理(67)——大模型的Benchmark(基准测试)
人工智能·笔记·学习