每一年都奔走在自己热爱里 没有人是一座孤岛,总有谁爱着你

岁
万
月
12

线程详解(理论)

一、进程和线程

在Java中,进程(Process)和线程(Thread)是多任务编程的核心概念,它们既有联系也有本质区别。

1.1 定义

  • 进程是操作系统中资源分配的基本单位。它是一个正在运行的程序的实例,包括程序代码、数据、堆栈、寄存器状态以及进程控制块等信息。
  • 线程是进程中的一个执行单元,是进程内部的最小调度单位。一个进程可以包含一个或多个线程,这些线程共享进程的资源,但各自有自己的执行路径和栈。

1.2 关系

包含关系:

  • 一个进程可以包含多个线程,所有线程共享进程的资源(如内存、文件句柄等)。

执行单元:

  • 进程是操作系统分配资源(CPU、内存)的基本单位。
  • 线程是CPU调度的最小单位,是进程内的具体执行任务的分支。

协作方式:

  • 进程间需要IPC(Inter-Process Communication)通信(如管道、Socket)。
  • 线程间可直接通过共享内存通信(但需同步机制避免数据竞争)。

1.3 核心区别

image

比喻理解:

  • 进程像一个独立的工厂:
    每个工厂有自己的车间(内存)、工人(线程)、原料(资源),工厂之间通过物流(IPC)协作。

  • 线程像工厂内的工人:
    工人共享工厂的资源(车间、仓库),协同完成生产任务,但需要避免争夺工具(同步问题)。

1.4 引入线程的原因

  • 并发执行:
    线程允许一个进程中同时执行多个任务。通过在同一个进程内创建多个线程,可以实现并发处理,从而提高程序的效率和响应速度。例如,一个程序可以在一个线程中处理用户界面响应,在另一个线程中进行后台数据处理。
  • 资源共享:
    线程共享进程的资源(如内存空间和文件描述符),这使得线程间的通信和数据共享比进程间更加高效和简单。线程之间可以轻松共享数据和资源,而不需要像进程间那样复杂的通信机制(如管道或消息队列)
  • 减少开销:
    创建和管理线程比创建和管理进程的开销要小得多。线程的创建和销毁成本较低,因为线程共享同一进程的地址空间和资源,不需要像进程那样进行内存映射和管理。
  • 提高响应性:
    在多线程程序中,某个线程的任务(如I/O操作)不会阻塞其他线程的执行。这意味着即使一个线程正在等待某些操作完成,其他线程仍然可以继续运行,从而保持程序的响应性。
  • 更好的资源利用:
    在多核处理器上,多线程程序可以将多个线程分配到不同的处理器核心上,从而提高程序的并行处理能力。这种并行执行能够更有效地利用计算资源,提升性能。

1.5 应用场景

适合用进程的场景

  • 需要高隔离性(如浏览器每个标签页作为独立进程)。

  • 调用外部程序(如Java中执行Shell脚本或启动其他应用)。

  • 分布式系统中跨机器的任务协作(通过Socket通信)。

适合用线程的场景

  • 高并发任务(如Web服务器处理大量请求)。

  • 需要共享数据的任务(如多线程计算并汇总结果)。

  • 异步任务(如后台下载文件时不阻塞UI主线程)。

1.6 启动代码示例

  • 启动进程(执行外部命令)
/**
     * 通过Runtime启动进程
     * @throws IOException
     * @throws InterruptedException
     */
    @RequestMapping("/processByRuntime")
    public void processByRuntime() throws IOException, InterruptedException {
        Runtime run = Runtime.getRuntime();
        //打开记事本,相当于在命令提示符中输入notepad打开记事本
        run.exec("notepad");
    }

    /**
     * 通过ProcessBuilder启动进程
     *
     * @throws IOException
     * @throws InterruptedException
     */
    @RequestMapping("/processByProcessBuilder")
    public void processByProcessBuilder() throws IOException, InterruptedException {
        //打开记事本
        ProcessBuilder pBuilder = new ProcessBuilder("notepad");
        pBuilder.start();
    }

  • 启动线程
@RequestMapping("/threadTest")
    public void threadTest(){
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 提交任务
        executor.submit(() -> {
            System.out.println("Thread 1: " + Thread.currentThread().getName());
        });

        executor.submit(() -> {
            System.out.println("Thread 2: " + Thread.currentThread().getName());
        });

        // 关闭线程池
        executor.shutdown();
    }

1.7 常见误区

  • 误区1:线程一定比进程快?
    → 不一定!线程切换也有开销,高并发时可能因锁竞争反而降低性能。

  • 误区2:多线程不需要考虑资源隔离?
    → 需要!线程共享内存,若未同步会导致数据不一致(如经典的i++非原子操作问题)。

  • 误区3:进程间无法共享数据?
    → 可以通过共享内存(如MemoryMappedFile)或中间件(如Redis)实现。


二、线程的生命周期

Java 线程的生命周期主要包括以下几个状态:

  1. 新建(New):当创建了 Thread 类的实例,但还未调用 start() 方法时,线程处于新建状态。
  2. 就绪(Runnable):调用 start() 方法后,线程进入就绪状态,等待系统调度。
  3. 运行(Running):当线程被系统选中并开始执行 run() 或 call() 方法时,线程处于运行状态。
  4. 阻塞(Blocked):线程可能因为等待锁、等待 I/O 操作完成、调用 wait() 方法等而进入阻塞状态,暂时停止执行。
  5. 等待(Waiting):线程调用 wait()、join() 或 LockSupport.park() 等方法会进入等待状态,直到其他线程通知或中断。
  6. 超时等待(Timed Waiting):线程调用 sleep()、wait(long)、join(long) 或 LockSupport.parkNanos() 等方法,在等待一段时间后自动唤醒。
  7. 终止(Terminated):线程执行完 run() 或 call() 方法,或者出现异常而终止。

三、线程的创建和启动

3.1 继承Thread类

  • 定义一个类 MyThread 继承 Thread 类,并重写 run() 方法,将线程要执行的代码放在 run() 方法中。
  • 创建 MyThread 实例,并调用 start() 方法启动线程,调用 start() 会使线程进入就绪状态,等待系统调度执行,而不是直接调用 run() 方法,直接调用 run() 方法只是普通的方法调用,不会启动新线程。
public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行的代码
        System.out.println("---------------------Thread is running---------------------");
    }
}

@RequestMapping("/threadByThread")
    public void threadByThread(){
        MyThread  myThread = new MyThread();
        myThread.start();//启动线程
    }

3.2 实现 Runnable 接口

  • 定义一个类 MyRunnable 实现 Runnable 接口,并重写 run() 方法。
  • 创建 MyRunnable 的实例,将其作为参数传递给 Thread 类的构造函数,然后调用 start() 方法启动线程。
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行的代码
        System.out.println("----------------ThreadByRunnable is running----------------");
    }
}


@RequestMapping("/ThreadByRunnable")
    public void ThreadByRunnable(){
        MyRunnable myRunnable = new MyRunnable();
        Thread thread = new Thread(myRunnable);
        thread.start();//启动线程
    }

3.3 使用 Callable 和 Future(带返回值的线程)

  • 定义一个类 MyCallable 实现 Callable 接口,重写 call() 方法,该方法可以有返回值,并且可以抛出异常。
  • 创建 FutureTask 对象,将 MyCallable 实例作为参数传递给 FutureTask 的构造函数。
  • 创建 Thread 对象,将 FutureTask 作为参数传递给 Thread 的构造函数并启动线程。
  • 使用 futureTask.get() 方法获取线程执行的结果,该方法会阻塞直到线程执行完成并返回结果,如果线程未完成,调用线程会等待。
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // TODO: 线程执行的代码
        return 100;
    }
}


@RequestMapping("/ThreadByCallable")
    public void ThreadByCallable() throws ExecutionException, InterruptedException {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        // 获取线程的返回值
        Integer result = futureTask.get();
        System.out.println("ThreadByCallable result: " + result);
    }

四、线程的同步

在Java多线程编程中,线程同步是确保多个线程安全访问共享资源的核心机制。

4.1 为什么要同步?

当多个线程同时修改共享数据时,可能导致数据不一致或竞态条件(Race Condition)。
示例:两个线程同时执行 i++(非原子操作):

public class UnsafeCounter {
    private int count = 0;
    public void increment() {
        count++; // 实际分为三步:读取 → 修改 → 写回
    }
}

问题:若线程A和B同时读取 count=5,各自加1后写回 6,最终结果应为 7,但实际得到 6。

4.2 同步机制

4.2.1 synchronized 关键字

作用:确保同一时刻只有一个线程执行同步代码。
使用方式:
(1)同步方法(锁是当前对象实例)
synchronized 修饰方法时,同一时间只有一个线程可以执行该方法,保证了方法的同步。

// 同步方法
public synchronized void increment() { 
    count++; 
}




// 静态同步方法(锁是类的Class对象)
public static synchronized void staticMethod() { 
    // ...
}

(2)同步代码块(可指定锁对象)
this:表示当前对象实例,即调用这段代码的对象的引用。这个对象实例将作为同步块的锁对象(monitor lock),线程需要获取 当前对象实例的锁 才能执行同步块内的代码,确保同一时刻只有一个线程能进入该同步代码块。
互斥范围:所有使用 this 作为锁的同步块或同步方法(如 synchronized void method())共享同一把锁。
同一对象的多个线程会竞争该锁,但不同对象的锁是独立的。

public void increment() {
    synchronized (this) { 
        count++;
    }
}

this 与其他锁对象的对比:
image

关键注意事项:
(1)不要锁住可变对象:

// 错误!StringBuffer 内容可变,可能导致锁失效
synchronized (stringBuffer) { 
    stringBuffer.append("test");
}

正确做法:使用一个 final 不可变对象 作为锁:

private final Object lock = new Object();
synchronized (lock) { 
    // ...
}

(2)避免锁泄露:
确保同步块内的代码不会长时间阻塞(如死循环或等待I/O)。
(3)区分实例锁和类锁:
实例锁(this)和类锁(类名.class)是两把不同的锁,互不影响。
(4)为什么 synchronized (this) 和 synchronized 方法 是等价的?
非静态同步方法(如 public synchronized void method())隐式使用 this 作为锁,因此与 synchronized (this) 效果相同。


4.2.2 Lock锁

在Java并发编程中,Lock接口(尤其是其实现类ReentrantLock)提供了比传统synchronized更灵活、更强大的线程同步机制。
(1)使用 ReentrantLock 类,在进入代码块前调用 lock() 方法加锁,在代码块执行完后在 finally 块中调用 unlock() 方法释放锁,确保锁的释放,避免死锁。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

private int count = 0;
private final Lock lock = new ReentrantLock();

public void increment() {
    lock.lock();
    try {
        count++;
    } finally {
        lock.unlock(); // 确保释放锁
    }
}

(2)Lock 接口的核心方法
image

(3)可重入性
同一线程可重复获取同一把锁,避免死锁。

Lock lock = new ReentrantLock();
public void methodA() {
    lock.lock();
    try {
        methodB(); // 可重入,不会阻塞
    } finally {
        lock.unlock();
    }
}
public void methodB() {
    lock.lock();
    try {
        // ...
    } finally {
        lock.unlock();
    }
}

(4)公平性

  • 公平锁:线程按请求顺序获取锁(构造参数true)。
  • 非公平锁:允许插队,默认模式(更高吞吐量)。
// 创建公平锁(按顺序分配锁)
Lock fairLock = new ReentrantLock(true); 
// 创建非公平锁(默认)
Lock nonFairLock = new ReentrantLock(); 

(5)可中断与超时机制
避免无限等待:通过tryLock和lockInterruptibly防止线程永久阻塞。

Lock lock = new ReentrantLock();
if (lock.tryLock(3, TimeUnit.SECONDS)) { // 尝试3秒内获取锁
    try {
        // 成功获取锁后的操作
    } finally {
        lock.unlock();
    }
} else {
    // 超时后的处理逻辑(如记录日志、重试等)
}

(6) Lock vs synchronized
image

4.2.3 volatile 关键字

(1)作用
保证可见性:确保变量的修改对所有线程立即可见。

  • 普通变量可能被线程缓存(在CPU寄存器或本地内存),而volatile变量直接读写主内存。

禁止指令重排序:防止JVM和处理器对指令进行重排序优化。

(2)适用场景

  • 状态标志:如 volatile boolean isRunning,用于控制线程启停。

  • 单次写入的安全发布:确保对象构造完成后对其他线程可见(需配合final)。

  • 双重检查锁定(Double-Checked Locking):优化单例模式(需volatile禁止重排序)。

(3)代码示例

public class VolatileExample {
    private volatile boolean flag = false;

    public void start() {
        new Thread(() -> {
            while (!flag) { // 及时感知 flag 变化
                // 等待
            }
            System.out.println("Thread started");
        }).start();
    }

    public void setFlag(boolean value) {
        flag = value; // 修改后立即对所有线程可见
    }
}

(4)注意事项

  • 不保证原子性:volatile无法解决复合操作(如i++)的线程安全问题。

  • 替代方案:简单计数器场景优先使用原子类(如AtomicInteger)。


五、线程间通信

wait()、notifyAll() 与 notify()
核心机制:

  • wait():释放当前对象的锁,使线程进入等待状态,直到被notify()唤醒。

  • notify():随机唤醒一个在该对象上等待的线程。

  • notifyAll():唤醒所有在该对象上等待的线程。

使用规则:

  • 必须在同步块中调用:wait()和notify()必须持有对象的锁(即位于synchronized块内)。

  • 循环检查条件:使用while而非if检查条件,防止虚假唤醒(Spurious Wakeup)。

  • 唤醒后重新竞争锁:被唤醒的线程需重新获取锁才能继续执行。

代码示例:

public class ProducerConsumer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int MAX_SIZE = 5;
    private final Object lock = new Object();
    private volatile boolean isRunning = true; // 控制线程启停

    public void produce() throws InterruptedException {
        while (isRunning) {
            synchronized (lock) {
                while (queue.size() == MAX_SIZE) {
                    lock.wait(); // 等待队列不满
                }
                queue.add(1);
                System.out.println("Produced, size: " + queue.size());
                lock.notifyAll(); // 唤醒消费者
            }
            Thread.sleep(100); // 模拟生产耗时
        }
    }

    public void consume() throws InterruptedException {
        while (isRunning) {
            synchronized (lock) {
                while (queue.isEmpty()) {
                    lock.wait(); // 等待队列非空
                }
                queue.poll();
                System.out.println("Consumed, size: " + queue.size());
                lock.notifyAll(); // 唤醒生产者
            }
            Thread.sleep(200); // 模拟消费耗时
        }
    }

    public void stop() {
        isRunning = false; // 通过 volatile 变量安全终止线程
    }
}

六、线程池

6.1 为什么需要线程池

  1. 减少线程创建/销毁开销:线程的创建和销毁成本高昂,线程池通过复用线程降低开销。
  2. 控制并发资源:避免无限制创建线程导致内存耗尽或CPU过载。
  3. 统一管理任务:通过队列缓冲任务,拒绝策略处理过载,增强系统的健壮性。
  4. 适用于高并发任务处理、异步编程、定时任务调度等。

使用建议:

  • 优先手动创建 ThreadPoolExecutor,避免 Executors 的潜在问题。
  • 根据任务类型合理设置线程数和队列容量。
  • 始终为线程池定义有意义的名称和拒绝策略,方便问题排查。

6.2 线程池核心类:ThreadPoolExecutor

Java 线程池的核心实现类是 java.util.concurrent.ThreadPoolExecutor,其构造函数包含 7 个关键参数:

public ThreadPoolExecutor(
    int corePoolSize,      // 核心线程数(常驻线程,即使空闲也不会被回收,除非设置)
    int maximumPoolSize,   // 最大线程数(临时线程 + 核心线程)
    long keepAliveTime,    // 临时线程空闲存活时间
    TimeUnit unit,         // 存活时间单位
    BlockingQueue<Runnable> workQueue, // 任务队列
    ThreadFactory threadFactory,      // 线程工厂(自定义线程创建)
    RejectedExecutionHandler handler  // 拒绝策略
)

workQueue队列类型:

  • LinkedBlockingQueue 无界队列(默认容量 Integer.MAX_VALUE) 任务量稳定,避免突发流量导致OOM
  • ArrayBlockingQueue 有界队列(需指定固定容量) 控制任务积压,防止资源耗尽
  • SynchronousQueue 不存储任务,直接移交线程执行 高吞吐场景,要求快速响应
  • PriorityBlockingQueue 优先级队列(任务按优先级排序) 任务有优先级区分

handler拒绝策略:
当线程池和队列均满时,处理新提交任务的策略:

  • AbortPolicy(默认) 抛出 RejectedExecutionException 异常
  • CallerRunsPolicy 由提交任务的线程直接执行该任务
  • DiscardPolicy 静默丢弃新任务,不抛异常
  • DiscardOldestPolicy 丢弃队列中最旧的任务,然后重新提交新任务

6.3 线程池工作流程

当提交一个新任务时,线程池的处理逻辑如下:

  1. 核心线程未满 → 创建新线程执行任务。

  2. 核心线程已满 → 任务放入任务队列等待。

  3. 队列已满且线程数未达最大值 → 创建临时线程执行任务。

  4. 队列已满且线程数已达最大值 → 触发拒绝策略。

6.4 常见线程池类型

6.4.1 FixedThreadPool(固定大小线程池)

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

public class ThreadPoolExample {
    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(5);// 核心线程=最大线程
        for (int i = 0; i < 10; i++) {
            executorService.execute(() -> {
                System.out.println("Thread is running");
            });
        }
        executorService.shutdown();
    }
}

Executors.newFixedThreadPool(5) 创建一个固定大小为 5 的线程池。
executorService.execute() 方法将任务提交给线程池执行。
executorService.shutdown() 方法关闭线程池。

  • 特点:线程数固定,使用无界队列(LinkedBlockingQueue)。

  • 风险:任务堆积可能导致OOM。

  • 适用场景:任务量已知且稳定。

6.4.2 CachedThreadPool(缓存线程池)

ExecutorService executor = Executors.newCachedThreadPool(); 
// 核心线程=0,最大线程=Integer.MAX_VALUE,存活时间60秒,使用SynchronousQueue
  • 特点:线程数弹性伸缩,适合短时异步任务。

  • 风险:无限制创建线程可能导致资源耗尽。

  • 适用场景:高频短任务,如HTTP请求处理。

6.4.3 SingleThreadExecutor(单线程池)

ExecutorService executor = Executors.newSingleThreadExecutor(); 
// 核心线程=最大线程=1,使用无界队列
  • 特点:所有任务按顺序执行,保证无并发问题。

  • 适用场景:需顺序执行任务的场景(如日志写入)。

6.4.4 ScheduledThreadPool(定时任务线程池)

ScheduledExecutorService executor = Executors.newScheduledThreadPool(3);
executor.scheduleAtFixedRate(() -> {
    System.out.println("Task runs every 2 seconds");
}, 0, 2, TimeUnit.SECONDS);
  • 特点:支持定时或周期性任务。

  • 适用场景:心跳检测、定时数据同步。

6.5 监控线程池状态

通过 ThreadPoolExecutor 提供的方法监控运行状态:

int activeThreads = executor.getActiveCount();      // 活跃线程数
long completedTasks = executor.getCompletedTaskCount(); // 已完成任务数
int queueSize = executor.getQueue().size();         // 队列中等待任务数

6.6 自定义线程池

// 手动创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,                                     // corePoolSize
    5,                                     // maximumPoolSize
    60, TimeUnit.SECONDS,                  // keepAliveTime
    new ArrayBlockingQueue<>(100),         // 有界队列(容量100)
    Executors.defaultThreadFactory(),      // 默认线程工厂
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由提交者执行
);

// 提交任务
executor.execute(() -> {
    System.out.println("Task executed by " + Thread.currentThread().getName());
});

// 关闭线程池(平滑关闭)
executor.shutdown(); 

方法 作用

  • shutdown() 平滑关闭:停止接受新任务,等待已提交任务执行完成。
  • shutdownNow() 强制关闭:尝试中断所有线程,返回未执行的任务列表。
  • isShutdown() 返回线程池是否已关闭。
  • isTerminated() 返回所有任务是否已完成。

七、线程的调度和优先级

7.1 线程优先级

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + " is running");
    }
}

public class ThreadPriorityExample {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread("Thread 1");
        MyThread thread2 = new MyThread("Thread 2");
        thread1.setPriority(Thread.MAX_PRIORITY);
        thread2.setPriority(Thread.MIN_PRIORITY);
        thread1.start();
        thread2.start();
    }
}

setPriority() 方法可以设置线程的优先级,范围从 1(最低)到 10(最高),但优先级只是一个建议,实际的调度由操作系统决定。

7.2 线程调度

Java 线程的调度由操作系统负责,操作系统根据线程的状态和优先级等因素来调度线程的执行,开发人员可以通过 yield() 方法让当前线程让出 CPU 资源,让其他线程有机会执行,但不保证一定有效。


八、线程的中断

class MyThread extends Thread {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            // 线程执行的代码
        }
        System.out.println("Thread interrupted");
    }
}

public class ThreadInterruptExample {
    public static void main(String[] args) throws InterruptedException {
        MyThread thread = new MyThread();
        thread.start();
        Thread.sleep(1000);
        thread.interrupt();
    }
}

interrupt() 方法用于中断线程,调用该方法会设置线程的中断标志。
线程可以通过 isInterrupted() 方法检查中断标志,或者在阻塞操作中抛出 InterruptedException 来响应中断。

九、线程典型例子及概念问题

参考:https://blog.csdn.net/ELSA001/article/details/145141870

posted @ 2025-12-29 15:30  果咩那塞  阅读(12)  评论(0)    收藏  举报