Java学习篇(四)—— Java 多线程

如何创建一个线程?

Java创建线程有两种方法,这里对三种方法做一个梳理,方便理解。

实现Runnable接口和run()方法

Java的接口就是一种协议,约定了想要被统一管理的类要遵循的协议。在Java中,线程是由Thread类创建和管理的,但线程需要执行具体的任务——也就是我们写的代码逻辑。而Runnable就是一个“任务的标准形式”:

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

public class Main {
    public static void main(String[] args) {
        Runnable task = new MyTask(); // 创建任务
        Thread thread = new Thread(task); // 把任务交给线程
        thread.start(); // 启动线程(调用 run())
    }
}

Runnable的意义是:让任何类都可以被线程调用,Java线程调用的代码入口必须是run()方法。

所以用这个方法创建的对象,都需要交给Thread对象去管理,thread则会调用run()方法。

更常见的做法是使用内部匿名类:

Thread thread = new Thread(new Runnable() {
    public void run() {
        System.out.println("匿名线程运行中!");
    }
});
thread.start();

继承Thread来创建线程

public class MyThread extends Thread {
    public void run() {
        System.out.println("我是继承 Thread 的线程");
    }
}

另一个方法是继承Thread类,重写了它的run()方法。这种方法与实现Runnable接口的区别就是:实现Runnable接口是在实现一个任务,将任务交给线程管理者执行和控制,而继承Thread方法则是直接定义了这个线程管理者执行的任务。由于Java是单继承的,所以继承的方法缺点也很明显,灵活性较低。

继承Thread类的方式,如果想要执行一个任务,必须新建一个线程,执行完成后还要销毁,开销非常大;而实现Runnable接口只需要新建任务,可以做到同一个线程执行多个任务,大大减小了线程创建、销毁的资源浪费。

Thread类详解

为什么创建线程可以有两种方式,原因在于Thread本身就是实现了Runnable接口的一个类,因此它也必须实现run()方法,那么继承Thread类的子类也可以重写run()方法。

Java线程状态

Java中线程可以有如下6中状态:NEW 新创建,RUNNABLE 可运行,BLOCKED 阻塞,WAITING 等待,TIMED WAITING 计时等待,TERMINATED 终止

在现代JVM中,Java的线程本质上就是操作系统线程(Native Thread),并且是一一对应的关系,操作系统的线程状态有五种,分别是:初始状态(NEW),可运行状态(READY),运行状态(RUNNING),等待状态(WAITING),终止状态 (TERMINATED)

  • 初始状态(NEW):这个状态其实是存疑的,操作系统程在调用pthread_create()后,已经在内核中被创建完成,拥有了自己的资源和调度实体,所以更类似于Java的new + start(),Java的NEW状态是指,线程在JVM层已存在,但是操作系统线程尚未分配、未创建,因此也没有占用OS资源(比如线程栈空间、线程ID)。

  • 可运行状态(READY):线程资源已经创建,但是没有分配时间片,没有运行,对应Java的RUNNABLE可运行状态。

  • 运行状态(RUNNING):分配时间片,正在运行,对应的也是Java的RUNNABLE可运行状态

  • 等待状态(WAITING):对操作系统而言,等待有两个原因:被其它优先级更高的任务打断、没有获取资源。而Java则将等待分为了三种状态:BLOCKED 阻塞,WAITING 等待,TIMED WAITING 计时等待,当访问的资源被上锁时,进入阻塞状态;被I/O等中断时,进入等待状态;也可以计时等待,超时自动唤醒,进入阻塞状态或者可运行状态

  • 终止状态 (TERMINATED):进程运行结束/线程运行结束进入终止态,对应Java的终止态

Thread类

源码详见参考资料一,源码中可以看到,当使用Runnable实现类实例化Thread对象时,会使this.target = traget,在之后的run()里调用targetrun()方法。

    public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

    public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

    @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

线程池

推荐使用Runnable创建线程任务的关键原因是,线程资源很宝贵,为一个任务创建一个线程非常浪费线程资源,一个线程可以完成不只一个任务,所以为任务创建一个任务队列,再创建一个线程池,管理一组线程池来执行任务队列里的任务,能充分的利用线程资源,同时提高响应速度。

Excutor框架

Excutor接口

void execute(Runnable command);

Executor executor = new Executor() {
    public void execute(Runnable command) {
        new Thread(command).start();  // 每次都开新线程执行任务
    }
};

Excutor接口里只有一个核心方法,用来执行任务,它不在乎任务到底是怎么执行的,由实现这个接口的类自己定义,最简单的就是来一个任务申请一个线程,但是这个显然不合理,我们使用线程池就是为了管理线程。

ExecutorService接口

ExecutorService接口继承了Executor接口,但是Executor只能执行任务,无法控制任务的结果、取消、超时,不具备线程池的管理能力。ExecutorServiceExecutor基础上新增了很多重要能力:

  • 提交任务:submit()方法能提交RunnableCallable

  • 生命周期管理:shutdown()isShutdown()awaitTermination()等方法,关闭线程池。

  • 获取结果:submit()返回Future,可获取返回值或取消任务。

  • 批量提交:invokeAll()invokeAny(),提交多个任务。

public interface ExecutorService extends Executor {
    void shutdown();                      // 平滑关闭线程池
    List<Runnable> shutdownNow();        // 立即停止线程池
    boolean isShutdown();
    boolean isTerminated();
    boolean awaitTermination(long timeout, TimeUnit unit);

    <T> Future<T> submit(Callable<T> task);        // 提交有返回值的任务
    <T> Future<T> submit(Runnable task, T result); // 提交无返回值的任务
    Future<?> submit(Runnable task);               // 提交无返回值的任务

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks);
    <T> T invokeAny(Collection<? extends Callable<T>> tasks);
}

ExecutorService只是一个接口,规定了任务可以提交,可以关闭线程池,对于有返回值的任务可以获取返回值Future,具体的实现需要类来定义。

ThreadPoolExecutor实现类

ThreadPoolExecutor是Java并发编程中最核心的线程池实现类,实现了ExecutorService接口,所有通过Executors工具类创建的线程池底层其实都是它。主要负责:1. 管理线程的复用(减少线程创建/销毁开销)。2.控制并发线程的数量。3.管理任务队列。4.定义线程池饱和时的策略。

ExecutorService接口只定义了线程的提交,提交的线程具体怎么管理和执行,是由ThreadPoolExcutor定义的。

下面是ThreadPoolExecutor的构造函数:

public ThreadPoolExecutor(
    int corePoolSize,                      // 核心线程数,常驻线程数(长期保留,即使空闲)
    int maximumPoolSize,                   // 最大线程数,任务太多时会临时扩充
    long keepAliveTime,                    // 当线程数大于核心线程数时,多余的空闲线程存活的最长时间
    TimeUnit unit,                         // 时间单位(秒、毫秒等)
    BlockingQueue<Runnable> workQueue,     // 等待执行的任务队列(如 LinkedBlockingQueue)
    ThreadFactory threadFactory,           // 生成线程的方式,默认即可
    RejectedExecutionHandler handler       // 拒绝策略
)

假设你提交一个任务:

executor.execute(() -> { /* 一段任务代码 */ });

它的流程如下:

  • 当前线程数 < corePoolSize:立刻调用addWorker()创建一个新线程执行任务。

  • 线程数 >= corePoolSize 且队列没满:任务加入队列等待。

  • 队列满了 且 当前线程数 < maximumPoolSize:创建新线程处理任务。

  • 队列满了 且 当前线程数 >= maximumPoolSize:执行拒绝策略(默认抛异常)。

拒绝策略和任务队列详见参考资料四

线程池不会主动把任务交给空闲线程,而是让线程从队列中拉任务,任务必须先入队,空闲线程才会处理。

常见对比

Runnable 和 Callable

Runnable自 Java 1.0 以来一直存在,但Callable仅在 Java 1.5 中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是 Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。
工具类 Executors 可以实现将 Runnable 对象转换成 Callable 对象。(Executors.callable(Runnable task) 或 Executors.callable(Runnable task, Object result))。

@FunctionalInterface
public interface Runnable {
   /**
    * 被线程执行,没有返回值也无法抛出异常
    */
    public abstract void run();
}

@FunctionalInterface
public interface Callable<V> {
    /**
     * 计算结果,或在无法这样做时抛出异常。
     * @return 计算得出的结果
     * @throws 如果无法计算结果,则抛出异常
     */
    V call() throws Exception;
}

Callable用到了泛型机制,可以定义返回类型。

参考资料

posted @ 2025-06-22 17:59  ZCry  阅读(28)  评论(0)    收藏  举报