在Java中实现多线程确实能显著提升程序性能,特别是在多核处理器上处理并发任务时。

🧵 Java多线程开发全面解析

✨ 线程的创建方式

Java提供了多种灵活的方式来创建和管理线程,每种方法都有其适用的场景。

1. 继承Thread类

这是最基础的创建线程方式,通过继承Thread类并重写run()方法。

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行!");
    }
}

public class ThreadExample {
    public static void main(String[] args) {
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        thread1.start(); // 启动第一个线程
        thread2.start(); // 启动第二个线程
    }
}

优点:简单易用,适合快速实现多线程。
缺点:由于Java的单继承限制,如果类已经继承了其他类,则无法使用此方法。

2. 实现Runnable接口

这是更推荐的创建线程方式,它解决了单继承的限制,使代码更加灵活。

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程 " + Thread.currentThread().getName() + " 正在执行!");
    }
}

public class RunnableExample {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunnable());
        Thread thread2 = new Thread(new MyRunnable());
        thread1.start();
        thread2.start();
    }
}

优点:避免了单继承的局限性,任务与线程分离,代码结构更清晰。
缺点:需要手动管理线程的生命周期和资源。

3. 实现Callable接口 + FutureTask

当需要线程执行后返回结果时,Callable是更好的选择。

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

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int a = 6;
        int b = 9;
        System.out.println("我是通过实现Callable接口创建的多线程");
        return a + b;
    }
}

class TestMyCallable {
    public static void main(String[] args) throws Exception {
        MyCallable myCallable = new MyCallable();
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        Thread thread = new Thread(futureTask);
        thread.start();
        System.out.println("返回值为:" + futureTask.get()); // 获取返回值
    }
}

优点:可以获得线程的返回结果,并且可以处理线程执行中的异常。
缺点:比Runnable复杂一些,Future的get方法是阻塞的。

4. 使用Executor框架(推荐)

Executor框架提供了更高级的线程管理机制,是工业级应用的首选。

import java.util.concurrent.*;

public class ExecutorExample {
    public static void main(String[] args) {
        // 创建固定大小的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        
        // 提交Runnable任务
        executorService.submit(() -> {
            System.out.println("Task 1 is running");
        });
        
        // 提交Callable任务并获取Future
        Future<Integer> future = executorService.submit(new MyCallable());
        
        // 关闭线程池
        executorService.shutdown();
    }
}

Java提供了多种类型的线程池以适应不同场景:

  • FixedThreadPool:固定线程数量,适用于任务数量固定的场景
  • CachedThreadPool:按需创建线程,适合短生命周期任务
  • ScheduledThreadPool:适用于需要定时或周期性执行任务的场景
  • SingleThreadExecutor:单线程执行任务,保证顺序执行

优点:提供了更高层次的线程管理机制,简化了线程的使用和调度,减少了线程创建和销毁的开销。
缺点:学习曲线较陡,需要掌握更多的API和概念。

🔄 线程的生命周期与状态

Java线程在其生命周期中会经历以下几种状态,可以通过getState()方法查询当前状态:

线程状态详细说明

  1. 新建(NEW)

    • 线程对象被创建但尚未启动的状态
    • 示例:Thread thread = new Thread();
  2. 可运行(RUNNABLE)

    • 调用start()方法后,线程进入就绪状态,等待CPU时间片
    • 获得CPU时间片后,线程开始执行run()方法
  3. 阻塞(BLOCKED)

    • 线程在等待监视器锁(synchronized锁)时进入此状态
    • 发生在尝试进入同步方法或代码块时
  4. 等待(WAITING)

    • 线程因调用以下方法之一而进入无限期等待:
      • Object.wait()
      • Thread.join()
      • LockSupport.park()
  5. 超时等待(TIMED_WAITING)

    • 与WAITING类似,但有时间限制:
      • Thread.sleep(long millis)
      • Object.wait(long timeout)
      • Thread.join(long millis)
  6. 终止(TERMINATED)

    • 线程执行完毕(run()方法完成)或因异常退出后的状态

下面是Java线程状态流转的核心机制

调用 start

等待synchronized锁

获得锁

调用 wait/join/park

被notify/unpark或中断

调用 sleep/wait/join
并指定超时时间

超时/被notify/unpark
或中断

run方法执行完毕
或出现未捕获异常

NEW
新建

RUNNABLE
可运行

BLOCKED
阻塞

WAITING
等待

TIMED_WAITING
超时等待

TERMINATED
终止

🔄 状态流转详解

下面这个表格汇总了触发各种线程状态转换的具体方法和条件。

状态 触发方法/条件 说明与注意事项
NEW → RUNNABLE thread.start() 线程对象创建后的初始状态。调用start()后,线程进入就绪队列,等待CPU调度。注意:直接调用run()方法只是在当前线程执行普通方法,不会启动新线程。
RUNNABLE → BLOCKED 尝试进入synchronized方法/块,但锁已被其他线程占用。或者进入阻塞IO 此状态仅与synchronized锁竞争相关。在等待JUC(java.util.concurrent)包中的锁(如ReentrantLock)时,线程状态不是BLOCKED,通常是WAITING或TIMED_WAITING。
RUNNABLE → WAITING object.wait() 调用wait()释放当前持有的锁,使线程进入无限期等待,需要其他线程通过notify()/notifyAll()唤醒。
thread.join() 当前线程等待目标线程执行完毕。底层是通过wait()实现。
LockSupport.park() 挂起当前线程,等待unpark()或中断。不会释放已经持有的锁
RUNNABLE → TIMED_WAITING Thread.sleep(long millis) 使线程休眠指定时间。重要sleep期间不会释放任何锁。
object.wait(long timeout) 带超时的等待。若超时前未被唤醒,线程将自动恢复。会释放锁。
thread.join(long millis) 带超时的连接。
LockSupport.parkNanos() / parkUntil() 带超时的挂起。
WAITING/TIMED_WAITING → RUNNABLE object.notify() / notifyAll() 唤醒在对应对象上等待的单个所有线程。
LockSupport.unpark(thread) 解除指定线程的挂起状态。
thread.interrupt() 中断目标线程。如果目标线程正处在WAITING或TIMED_WAITING状态,会抛出InterruptedException并清除中断状态,线程被唤醒。
超时时间到 仅适用于TIMED_WAITING状态。
BLOCKED → RUNNABLE 持有锁的线程释放了锁。 当锁可用时,系统会从所有阻塞的线程中选择一个来获取锁并恢复执行。
RUNNABLE → TERMINATED run()方法正常执行完毕。 线程完成其任务,正常结束。
run()方法执行过程中抛出未捕获的异常。 线程因异常而意外终止。

⚠️ 关键点与最佳实践

  1. RUNNABLE状态的复合性:Java的RUNNABLE状态涵盖了操作系统的“就绪”和“运行”两种状态。通过Thread.getState()无法区分线程是在等待CPU时间片还是在执行。
  2. 谨慎使用yield()Thread.yield()是给调度器的一个提示,表示当前线程愿意让出CPU。但这是一个不保证有效的建议,调度器可以完全忽略它。不要用它来控制执行顺序。
  3. 正确处理中断interrupt()机制是Java提供的协作式线程取消机制。在编写可能阻塞的代码时,要合理响应中断(捕获InterruptedException并执行清理操作或恢复中断状态),使线程能够被优雅地停止。
  4. 避免废弃方法stop(), suspend(), resume()等方法因其固有的不安全性(容易导致死锁和数据不一致)而被废弃,绝对不要使用。

🔧 如何监控线程状态

  • 编程方式:使用Thread.getState()获取特定线程的状态,或通过ThreadMXBean获取JVM内所有线程的详细信息。
  • 可视化工具:利用JConsole、VisualVM等JDK自带工具,可以实时查看线程状态堆栈,非常适合用于调试和性能分析。
public class WaitNotifyDemo {
    // 用于同步的共享对象
    private final Object lock = new Object();
    // 共享消息
    private String message = null;

    // 生产者线程
    class Producer extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                try {
                    System.out.println("生产者: 正在准备消息...");
                    Thread.sleep(2000); // 模拟准备消息的耗时
                    message = "你好,这是给你的消息!"; // 设置消息
                    System.out.println("生产者: 消息已准备好,通知消费者。");
                    lock.notify(); // 通知等待的消费者线程
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    // 消费者线程
    class Consumer extends Thread {
        @Override
        public void run() {
            synchronized (lock) {
                try {
                    System.out.println("消费者: 等待生产者提供消息...");
                    while (message == null) { // 使用循环防止“虚假唤醒”
                        lock.wait(); // 释放锁并进入等待
                    }
                    System.out.println("消费者: 收到消息 -> " + message);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        WaitNotifyDemo demo = new WaitNotifyDemo();
        Consumer consumer = demo.new Consumer();
        Producer producer = demo.new Producer();

        consumer.start(); // 先启动消费者,使其进入等待
        Thread.sleep(500); // 稍作延迟,确保消费者先开始等待
        producer.start(); // 再启动生产者

        // 等待两个线程执行完毕
        consumer.join();
        producer.join();
        System.out.println("主程序: 演示结束。");
    }
}

同步与锁机制

多线程环境下,线程安全是至关重要的考虑因素。

synchronized关键字

class Counter {
    private int count = 0;
    
    public synchronized void increment() {
        count++;
    }
}

Lock接口(更灵活的选择):

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

public class LockExample {
    private int count = 0;
    private Lock lock = new ReentrantLock();
    
    public void increment() {
        lock.lock();
        try {
            count++;
        } finally {
            lock.unlock();
        }
    }
}

读写锁(读多写少场景):

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockExample {
    private int value;
    private ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    
    public void write(int newValue) {
        rwLock.writeLock().lock();
        try {
            value = newValue;
        } finally {
            rwLock.writeLock().unlock();
        }
    }
    
    public int read() {
        rwLock.readLock().lock();
        try {
            return value;
        } finally {
            rwLock.readLock().unlock();
        }
    }
}

更多的锁相关内容查看另一篇博客:https://blog.csdn.net/weixin_44044929/article/details/156861709

线程间通信

线程间通信确实很重要,它让多个线程能够协同工作、安全地传递数据。下面这个表格汇总了主要的通信方式。

通信机制 核心原理 典型应用场景 关键特点
共享变量 多个线程直接读写同一内存区域 简单的状态标志、计数器 需配合同步机制(如synchronizedvolatile)保证安全
等待/通知 (Wait/Notify) 线程在共享对象上等待(wait)或发出通知(notify/notifyAll) 生产者-消费者模式、线程间条件协调 必须获得对象锁(在synchronized块内)使用
线程同步工具 基于消息传递,线程通过线程安全的数据结构(如阻塞队列)交换数据 生产者-消费者模式、任务调度 解耦生产与消费,内置线程安全与协调机制
Lock与Condition 提供比synchronized更灵活的锁操作,Condition可实现精确的等待/通知 复杂的多条件同步场景(如读写锁) 可创建多个Condition,实现精准唤醒
高级同步工具 使用CountDownLatchCyclicBarrierSemaphore 控制并发线程数(Semaphore),同步多个线程的阶段(CyclicBarrier),等待多个任务完成(CountDownLatch) 功能专一,简化特定同步逻辑的实现

⚙️ 关键细节与最佳实践

了解基本方式后,一些关键细节和最佳实践能帮助你避免常见的“坑”。

  • 正确使用Wait-Nofitywait()方法会释放当前持有的锁。调用notify()notifyAll()后,被唤醒的线程并不会立即执行,它们需要重新获取锁。因此,条件检查应放在while循环中,而不是if语句中,这是为了防止虚假唤醒(即线程在没有收到明确通知的情况下被唤醒)。同时,volatile关键字能保证变量的可见性,但不保证复合操作(如count++)的原子性。对于计数等场景,AtomicInteger等原子类通常是更好的选择。

  • 优先使用并发工具:在多数情况下,java.util.concurrent包提供的工具(如BlockingQueue, CountDownLatch等)优于手动使用wait()/notify(),因为它们更成熟,能减少出错。

💎 如何选择通信方式

选择哪种方式取决于具体需求:

  • 简单的状态标志或原子更新:考虑 volatile 或原子类(如 AtomicInteger)。
  • 典型的生成者-消费者模式BlockingQueue 是最直接和推荐的选择。
  • 需要在线程间协调复杂的条件(例如“多条件等待”):Lock 配合 Condition 提供了更精细的控制。
  • 需要让多个线程在某个点同步,或控制同时访问特定资源的线程数量CountDownLatchCyclicBarrierSemaphore 等高级同步工具非常合适。
  • 需要协调线程执行顺序(如让一个线程等待另一个线程完成):Thread.join() 方法简单有效。

⚠️ 务必避开这些坑

  • 死锁:当两个或多个线程互相等待对方释放锁时发生。解决方法是固定锁的获取顺序,或者使用支持超时尝试获取锁的机制。
  • 性能损耗:不必要或过粗粒度的同步(如在非常大的代码块上使用synchronized)会限制并发性能。尽量减小同步代码块的范围
  • 线程活性故障:除了死锁,还要注意活锁(线程不断重试但始终无法进展)和饥饿(某些线程始终得不到执行机会)。

安全的停止线程

在Java中,安全地停止线程是一项重要的编程技能,核心在于采用协作式机制,让线程有机会清理资源并完成必要操作后自行结束,而不是被强制终止。

下面的表格汇总了主要的方法和最佳实践。

方法 核心机制 适用场景 关键要点
协作式中断标志 通过volatile boolean变量作为信号。 简单循环任务,需要自定义停止逻辑。 实现简单;注意volatile保证可见性。
interrupt() 方法 调用线程的interrupt()方法设置中断状态,线程通过isInterrupted()检查或通过InterruptedException感知。 需要利用Java内置中断机制的场景,尤其是可能阻塞的操作(如sleep(), wait())。 JVM内置支持;可中断阻塞操作;需正确恢复中断状态。
线程池管理 通过ExecutorServiceshutdown()shutdownNow()方法停止线程池中所有线程。 使用线程池执行任务时。 管理多个线程;shutdown()优雅关闭,shutdownNow()尝试立即中断。
Future 的 cancel() 通过Future对象的cancel(true)方法尝试取消任务的执行。 需要取消由线程池执行的单个特定任务。 可中断特定任务。
守护线程 (Daemon) 将线程设置为守护线程(setDaemon(true)),当JVM中所有非守护线程结束时,守护线程会自动结束。 执行辅助或后台任务,这些任务可以随主程序结束而终止,无需单独管理其生命周期。 生命周期随JVM结束而终止,不保证finally块执行或资源清理。

⚙️ 详解核心方法

1. 协作式中断标志

设置一个volatile布尔变量作为线程执行的信号。

public class CustomFlagThread extends Thread {
    private volatile boolean running = true; // volatile确保可见性

    public void run() {
        while (running) {
            // ... 执行任务
        }
        // ... 清理工作
    }

    public void stopGracefully() {
        running = false;
    }
}

优点:简单直观,完全控制中断逻辑。
注意点volatile关键字确保一个线程对running的修改对其他线程立即可见。

2. 内置中断机制 (interrupt())

这是Java官方推荐的协作式中断方式。

  • 检查中断状态:适用于未阻塞的线程。

    public class InterruptCheckThread extends Thread {
        public void run() {
            // 检查中断状态前可以执行一些工作
            while (!Thread.currentThread().isInterrupted()) {
                // ... 执行任务
            }
            System.out.println("线程已收到中断信号,优雅退出。");
            // ... 清理资源
        }
    }
    // 外部调用 thread.interrupt() 来请求中断
    
  • 处理InterruptedException:当线程在可中断的阻塞方法(如 Thread.sleep(), Object.wait(), Thread.join())中时,调用interrupt()会触发InterruptedException

    public class InterruptSleepThread extends Thread {
        public void run() {
            try {
                while (!Thread.currentThread().isInterrupted()) {
                    // ... 执行部分任务
                    Thread.sleep(1000); // 休眠时可能被中断
                }
            } catch (InterruptedException e) {
                // 捕获到中断异常,恢复中断状态是良好实践
                Thread.currentThread().interrupt(); 
                System.out.println("线程在休眠中被中断。");
            }
            // ... 清理资源
        }
    }
    

    关键:在捕获InterruptedException后,通常需要再次调用Thread.currentThread().interrupt()来重新设置中断状态,因为异常会清除中断状态。

3. 线程池的停止

使用ExecutorService时,停止线程应通过它提供的方法。

  • shutdown():平缓关闭,不再接受新任务,已提交任务执行完毕。
  • shutdownNow():尝试立即停止所有正在执行的任务(通过调用Thread.interrupt()),并返回等待执行的任务列表。
    ExecutorService executor = Executors.newFixedThreadPool(2);
    executor.submit(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            // ... 执行任务
        }
    });
    // ...
    List<Runnable> notExecutedTasks = executor.shutdownNow(); // 尝试中断所有任务
    
4. 使用Future取消任务

提交任务到线程池会返回Future对象,可用其取消特定任务。

ExecutorService executor = Executors.newSingleThreadExecutor();
Future<?> future = executor.submit(() -> {
    // ... 长时间运行的任务
});
// ...
boolean mayInterruptIfRunning = true;
future.cancel(mayInterruptIfRunning); // 尝试取消任务执行

⚠️ 强烈避免的方法

绝对不要使用Thread.stop()方法。该方法已被废弃,因为它会立即强制停止线程,可能导致:

  • 数据不一致:线程可能正在更新共享数据,强制停止会使数据处于损坏状态。
  • 资源泄漏:线程可能持有锁或其他资源,未来得及释放,进而引发死锁或资源耗尽。

💡 最佳实践与要点

  1. 优先使用interrupt()机制:这是Java语言设计的标准方式,能与很多库函数良好协作。
  2. 明智处理中断:不要忽略InterruptedException。根据任务性质选择:传递异常(声明抛出)、恢复中断状态(再次设置中断标志)或优雅退出
  3. 确保资源清理:无论以何种方式结束线程,一定要在finally块中释放资源(如关闭文件、数据库连接)。
  4. 平衡响应性与性能:中断检查频率需权衡。过于频繁可能影响性能,过于稀疏可能导致中断响应延迟。

📊 如何选择停止方法?

  • 大多数需要协作停止的场景:首选 interrupt()机制
  • 需要高度自定义停止逻辑或简单循环任务:可使用协作式标志位
  • 使用线程池管理多线程:务必使用**ExecutorService的关闭方法Future.cancel()**。
  • 后台辅助任务,允许随主线程结束:可考虑设为守护线程

🛡️ 多线程开发中的注意事项

1. 线程安全

多个线程可能会同时访问共享资源,从而导致数据不一致的问题。可以使用同步机制,如synchronized关键字、ReentrantLock类等来解决。

2. 死锁预防

死锁是指两个或多个线程相互等待对方释放资源,从而导致线程永远无法继续执行的现象。为了避免死锁,需要注意资源的获取顺序,并尽量减少锁的使用。

3. 性能优化

  • 使用线程池减少线程创建和销毁的开销
  • 根据任务类型(CPU密集型/IO密集型)合理配置线程数
  • 使用并发集合(如ConcurrentHashMap、CopyOnWriteArrayList)避免手动同步

4. 资源管理

  • 确保在使用多线程时合理管理资源
  • 在使用Executor框架时,记得调用shutdown()方法关闭线程池

💻 实际应用场景

多线程编程在现实场景中有广泛的应用:

  • 网络服务器:通过多线程处理多个客户端请求,提高服务器的并发处理能力
  • 图像处理:将图像拆分成多个部分并行处理,加速处理过程
  • 爬虫程序:使用多线程并发抓取多个网页,提高爬取效率
  • 游戏开发:多线程可以用于处理游戏中的不同操作,如渲染、输入等
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐