Java 线程(一)

62 阅读7分钟

线程和进程

进程

进程是操作系统进行资源分配和调度的基本单位,是程序的一次执行过程。每个进程都有独立的内存空间、数据段和代码段,以及系统资源(如打开的文件、信号量等)

关键特性

  • 独立:进程之间相互隔离,一个进程崩溃通常不会影响其他进程
  • 资源拥有:操作系统会为每个进程分配独立的地址空间和系统资源
  • 开销大:创建、切换和销毁进程都需要较大的系统开销

线程

线程是进程内的一个执行单元,是CPU调度和执行的最小单位。一个进程可以包含多个线程,这些线程共享进程的资源

关键特性

  • 共享:同一进程内的线程共享内存空间和资源
  • 轻量:创建、切换和销毁线程的开销比进程小得多
  • 协作:线程间通信比进程间通信更高效

并发和并行

并发

并发是指在重叠的时间段内处理多个任务的能力,这些任务可能在逻辑上同时进行,但不一定在物理上同时执行

关键特性

  • 关注的是任务的结构和调度方式
  • 通过任务切换实现"同时"执行的假象
  • 单核CPU也能实现并发
  • 主要解决的是程序的组织和响应性问题

并行

并行是指在物理上同时执行多个任务,需要多核/多处理器硬件支持

关键特性

  • 关注的是任务的执行方式
  • 真正的同时执行,没有任务切换的开销
  • 需要多核CPU或分布式系统支持
  • 主要解决的是计算性能问题

线程的生命周期

线程的生命周期是指线程从创建到销毁的整个过程,通常包括多个状态

在 Java 中,线程的生命周期由 Thread.State 枚举定义

public enum State {

    /**
     * 线程被创建,但尚未调用start()方法
     */
    NEW,
    
     /**
     * 线程正在 JVM 中执行或等待 CPU 调度
     */
    RUNNABLE,
    
     /**
     * 线程等待获取监视器锁(如 synchronized 块)
     */
    BLOCKED,

     /**
     * 线程无限期等待,直到被其他线程唤醒
     */
    WAITING,

     /**
     * 线程等待指定时间后自动恢复
     */
    TIMED_WAITING,

     /**
     * 线程执行完毕或异常退出
     */
    TERMINATED;
}

NEW(新建)

线程对象被创建(new Thread()),但未调用 start()

特点

  • 此时JVM为其分配内存,并初始化其成员变量的值,但尚未分配系统资源(如 CPU 时间片)
  • 不能直接运行,必须调用 start() 进入 RUNNABLE 状态

RUNNABLE(就绪态)

调用 start() 方法后,线程进入 RUNNABLE 状态

特点

  • 不一定正在运行,可能正在等待 CPU 调度(取决于 OS 的线程调度策略)。

  • 在 Java 中,RUNNABLE 包括:

    • Ready(就绪) :等待 CPU 分配时间片
    • Running(运行中) :正在 CPU 上执行

BLOCKED(阻塞态)

处于运行状态的线程在某些情况下,会让出 CPU 并暂时停止自己的运行,进入 阻塞状态

在以下情况发发生时,线程会让出时间片,进入阻塞状态:

  • 线程调用 sleep() 方法,表示当前线程主动放弃所占用的处理器资源,暂时进入中断状态(此时不会释放持有的对象锁),时间到后进入就绪状态等待系统分配 CPU 继续执行
  • 线程调用一个阻塞式IO方法,在该方法返回之前,该线程会被阻塞
  • 线程试图获得一个同步监视器(synchronized),但该同步监视器正被其他线程所持有,此时该线程会等待其他线程释放锁然后继续参与下一次锁的竞争中
  • 程序调用了线程的 suspend 方法将线程挂起
  • 线程调用wait,等待notify/notifyAll唤醒时(会释放持有的对象锁)

阻塞状态分类:

  1. 等待阻塞:运行状态中的线程执行 wait() 方法,使本线程进入到等待阻塞状态
  2. 同步阻塞:线程在获取 synchronized 同步锁失败(因为锁被其它线程占用),它会进入到同步阻塞状态
  3. 其他阻塞:通过调用线程的 sleep()或join()或发出I/O请求 时,线程会进入到阻塞状态。当 sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕 时,线程重新转入就绪状态

处于阻塞状态的线程只能进入就绪态,无法直接进入运行态。而就绪和运行状态之间的转换不受程序控制,而是由系统线程调度所决定。当处于就绪状态的线程获得处理器资源时,该线程进入运行状态;当处于运行状态的线程失去处理器资源时,该线程进入就绪状态。但有一个方法例外,调用yield()方法可以让运行状态的线程转入就绪状态

WAITTING(等待态)

线程处于 无限制等待状态,直到等待一个特殊的事件来将其重新唤醒,如:

  • 通过 wait() 方法进入阻塞状态的线程需要等待其他线程的 notify() 或者 notifyAll() 方法
  • 通过 join() 方法进入阻塞状态的线程需要等待目标线程运行结束而自动唤醒

TIMED_WAITTING(限时等待态)

线程进入了一个 时限等待状态,在等待指定时间后无需其他线程唤醒而自动进入就绪态参与时间片的分配

TERMINAL(终止态)

线程执行完毕后,进入终止(TERMINATED)状态,需要注意的是一旦线程通过start()方法启动后就再也不能回到新建(NEW)状态,线程终止后也不能再回到就绪(RUNNABLE)状态

生命周期流程图

stateDiagram-v2
    [*] --> NEW: 创建线程对象\n(new Thread())
    NEW --> RUNNABLE: start()
    
    state RUNNABLE {
        [*] --> Ready: 等待CPU调度
        Ready --> Running: 获取CPU时间片
        Running --> Ready: 时间片用完\n或yield()
    }
    
    RUNNABLE --> BLOCKED: 尝试获取锁失败\n(synchronized)
    BLOCKED --> RUNNABLE: 锁被释放
    
    RUNNABLE --> WAITING: wait()/join()\n(无超时)
    WAITING --> RUNNABLE: notify()/interrupt()
    
    RUNNABLE --> TIMED_WAITING: sleep(ms)\nwait(ms)
    TIMED_WAITING --> RUNNABLE: 超时结束\n或被唤醒
    
    RUNNABLE --> TERMINATED: run()执行完毕\n或异常退出
    TERMINATED --> [*]

线程的创建

最开始接触线程创建可能认为只有简单的三种创建方式,但随着时间的发展,创建线程的方式也变得多了起来

继承 Thread 类

通过继承Java的Thread类并重写其run()方法来创建线程

class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码
        System.out.println("线程运行中: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 启动线程
    }
}

优点

  • 实现简单,直接继承Thread类即可
  • 可以直接使用Thread类的方法,如getName()、setName()等

缺点

  • Java是单继承的,继承Thread后不能再继承其他类
  • 线程与任务耦合在一起,不够灵活

实现Runable接口

通过实现Runnable接口并实现其run()方法,然后将Runnable实例传递给Thread构造函数

class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
        System.out.println("线程运行中: " + Thread.currentThread().getName());
    }
}

public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start(); // 启动线程
    }
}

优点

  • 避免了单继承的限制,可以继承其他类
  • 线程与任务解耦,更加灵活
  • 适合多个线程共享同一个任务的情况

缺点

  • 不能直接获取线程执行结果
  • 不能直接抛出受检异常

实现Callable接口

通过实现Callable接口并实现其call()方法,然后使用FutureTask包装Callable实例,最后传递给Thread构造函数

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        // 线程执行的代码
        System.out.println("线程运行中: " + Thread.currentThread().getName());
        return "任务执行完成";
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        MyCallable myCallable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start(); // 启动线程
        
        // 获取线程执行结果
        String result = futureTask.get();
        System.out.println("线程执行结果: " + result);
    }
}

优点

  • 可以获取线程执行结果(通过FutureTask的get()方法)
  • 可以抛出受检异常
  • 支持泛型返回值

缺点

  • 实现相对复杂
  • get()方法会阻塞主线程直到任务完成

通过线程池

可以通过Executors工具类创建线程池,也可以自定义线程池创建线程池

public class Main {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(5);
        
        // 提交Runnable任务
        executor.execute(() -> {
            System.out.println("Runnable任务执行");
        });
        
        // 提交Callable任务
        executor.submit(() -> {
            System.out.println("Callable任务执行");
            return "结果";
        });
        
        executor.shutdown();
        
        // 又或者自定义线程池

        ThreadPoolExecutor _executor = new ThreadPoolExecutor(
                                            2, 3, 0, TimeUnit.SECONDS,
                                            new LinkedBlockingQueue<Runnable>(3), 
                                            Executors.defaultThreadFactory(), 
                                            new ThreadPoolExecutor.AbortPolicy()
                                       ); 
        _executor.submit(()->{ System.out.println("4B......"); }); 
        _executor.shutdown();        
    }
}

Executor、ExecutorService、Executors详解

使用CompleteFuture类

CompletableFutureJDK1.8引入的一个新类,可以用来执行异步任务,如下:

public class UseCompletableFuture { 
    public static void main(String[] args) throws InterruptedException { 
        CompletableFuture<String> cf = CompletableFuture.supplyAsync(() -> { 
            System.out.println("5......"); 
            return "zhuZi"; 
        }); 
        // 主线程需要阻塞,否则看不到结果 
        Thread.sleep(1000); 
    }
}

CompleteFuture详解