杰瑞科技汇

Java多线程中Runnable是什么?

目录

  1. 为什么需要 Runnable - 解决 Java 单继承的局限性
  2. Runnable 是什么? - 接口定义与核心方法
  3. 如何使用 Runnable 创建并启动线程? - 标准三步法
  4. Runnable vs. Thread - 核心区别与优势
  5. 实践案例:一个简单的多线程示例
  6. Runnable 的实际应用:线程池
  7. RunnableCallable 的区别

为什么需要 Runnable

在 Java 中,创建线程有两种主要方式:

Java多线程中Runnable是什么?-图1
(图片来源网络,侵删)
  1. 继承 Thread
  2. 实现 Runnable 接口

如果只使用继承 Thread 类,会带来一个问题:Java 不支持多继承,如果一个类已经继承了一个父类(class MyData extends SomeOtherClass),它就不能再继承 Thread 类了,而实现 Runnable 接口则没有这个限制,因为一个类可以实现多个接口。

Runnable 提供了一种更灵活、更符合面向对象设计原则的方式来创建多线程任务。


Runnable 是什么?

Runnable 是 Java 中一个非常简单的函数式接口(Functional Interface),它位于 java.lang 包中。

@FunctionalInterface
public interface Runnable {
    /**
     * When an object implementing interface <code>Runnable</code> is used
     * to create a thread, starting the thread causes the object's
     * <code>run</code> method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method <code>run</code> is that it may
     * take any action whatsoever.
     *
     * @see     java.lang.Thread#run()
     */
    public abstract void run();
}

核心要点:

  • 只有一个抽象方法public void run()
  • 作用run() 方法包含了线程要执行的代码逻辑,当线程启动后,JVM 会自动调用该线程关联的 Runnable 对象的 run() 方法。
  • 注意run() 方法不会自动启动新线程,它只是一个普通的方法,直接调用 myRunnable.run() 会在当前线程中顺序执行,而不会创建新线程,必须通过 Thread 对象来启动它。

如何使用 Runnable 创建并启动线程?(标准三步法)

这是使用 Runnable 的标准流程:

第一步:创建一个 Runnable 接口的实现类

创建一个类,让它实现 Runnable 接口,并重写 run() 方法。

// 1. 定义一个任务类,实现 Runnable 接口
class MyTask implements Runnable {
    private String taskName;
    public MyTask(String name) {
        this.taskName = name;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(taskName + " is running, count: " + i);
            try {
                // 模拟任务执行,休眠 500 毫秒
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(taskName + " has finished.");
    }
}

第二步:创建 Thread 对象,并将 Runnable 实例作为参数传入

Thread 类的构造函数可以接收一个 Runnable 对象,当你将一个 Runnable 传递给 Thread 时,这个 Runnable 就成为了该线程的“任务”。

// 2. 创建 Runnable 实例
MyTask task1 = new MyTask("Task-A");
MyTask task2 = new MyTask("Task-B");
// 3. 创建 Thread 对象,并将 Runnable 实例传入
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);

第三步:调用 start() 方法启动线程

调用 thread.start() 是最关键的一步,这会做两件事:

  1. 创建一个新的线程
  2. 在新线程中调用 Runnable 对象的 run() 方法

千万不要调用 run() 方法,那只是在主线程中顺序执行任务,失去了多线程的意义。

// 4. 启动线程
thread1.start();
thread2.start();

完整示例代码:

class MyTask implements Runnable {
    private String taskName;
    public MyTask(String name) {
        this.taskName = name;
    }
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(taskName + " is running, count: " + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println(taskName + " has finished.");
    }
}
public class RunnableDemo {
    public static void main(String[] args) {
        System.out.println("Main thread starts.");
        // 1. 创建 Runnable 实例
        MyTask task1 = new MyTask("Task-A");
        MyTask task2 = new MyTask("Task-B");
        // 2. 创建 Thread 对象
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);
        // 3. 启动线程
        thread1.start();
        thread2.start();
        System.out.println("Main thread ends.");
    }
}

可能的输出(因为线程执行顺序不确定,所以每次运行结果可能不同):

Main thread starts.
Main thread ends.
Task-A is running, count: 1
Task-B is running, count: 1
Task-A is running, count: 2
Task-B is running, count: 2
...
Task-A has finished.
Task-B has finished.

注意,Main thread ends. 可能会在两个任务执行完之前就打印出来,这恰恰体现了多线程的并发特性。


Runnable vs. Thread

特性 实现 Runnable 接口 继承 Thread
继承性 灵活,一个类可以实现多个接口,同时还可以继承一个类。 受限,Java 不支持多继承,所以不能再继承其他类。
资源共享 简单、自然,多个 Thread 实例可以共享同一个 Runnable 对象,从而共享其数据。 复杂,如果多个线程需要共享数据,需要将数据定义为 static 或通过构造函数传递,容易出错。
设计模式 推荐,符合“任务”与“执行者”分离的原则。Runnable 是任务,Thread 是执行者。 不推荐,将任务逻辑和线程管理逻辑耦合在同一个类中,不符合单一职责原则。
启动方式 必须创建 Thread 实例,并传入 Runnable,然后调用 start() 直接创建 Thread 子类的实例,然后调用 start()

在绝大多数情况下,优先选择实现 Runnable 接口的方式,它更灵活、更安全,也更符合现代 Java 编程思想。


实践案例:一个简单的多线程示例

假设我们要模拟一个售票系统,有多个窗口同时售票。

// 票务中心,票是共享资源
class TicketOffice {
    private int totalTickets = 10;
    public void sellTickets(String windowName) {
        while (totalTickets > 0) {
            // 使用同步块来防止线程安全问题
            synchronized (this) {
                if (totalTickets > 0) {
                    System.out.println(windowName + " is selling ticket No." + totalTickets);
                    totalTickets--;
                    try {
                        Thread.sleep(100); // 模拟售票耗时
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        System.out.println(windowName + " has no more tickets to sell.");
    }
}
// 售票窗口任务
class TicketSeller implements Runnable {
    private TicketOffice office;
    private String windowName;
    public TicketSeller(TicketOffice office, String name) {
        this.office = office;
        this.windowName = name;
    }
    @Override
    public void run() {
        office.sellTickets(windowName);
    }
}
public class TicketSellingDemo {
    public static void main(String[] args) {
        TicketOffice office = new TicketOffice();
        // 创建三个售票窗口
        Thread window1 = new Thread(new TicketSeller(office, "Window-1"));
        Thread window2 = new Thread(new TicketSeller(office, "Window-2"));
        Thread window3 = new Thread(new TicketSeller(office, "Window-3"));
        window1.start();
        window2.start();
        window3.start();
    }
}

这个例子中,TicketSeller 实现了 Runnable,它持有一个共享的 TicketOffice 引用,多个 Thread 对象共享同一个 TicketSeller 实例(或者多个实例但共享同一个 TicketOffice),从而实现了对共享资源的访问。


Runnable 的实际应用:线程池

在现代 Java 应用中,我们通常不会直接创建和销毁线程,因为这是一个昂贵的操作,而是使用线程池

Runnable 是线程池中最核心的任务单元,当你向线程池提交一个任务时,你提交的就是一个 Runnable 对象。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolExample {
    public static void main(String[] args) {
        // 创建一个固定大小为 3 的线程池
        ExecutorService executor = Executors.newFixedThreadPool(3);
        // 提交 10 个任务到线程池
        for (int i = 1; i <= 10; i++) {
            final int taskId = i;
            Runnable task = new Runnable() {
                @Override
                public void run() {
                    System.out.println("Task-" + taskId + " is being executed by " + Thread.currentThread().getName());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("Task-" + taskId + " finished.");
                }
            };
            executor.execute(task); // 提交任务
        }
        // 关闭线程池
        executor.shutdown();
    }
}

在这个例子中,我们创建了一个 Runnable 匿名类来定义任务,然后通过 executor.execute(task) 将其提交给线程池执行,线程池会从其内部的线程队列中取出一个空闲线程来执行这个 Runnablerun() 方法。


RunnableCallable 的区别

Runnable 有一个重要的局限性:它的 run() 方法不能返回结果,也不能抛出受检异常,为了解决这个问题,Java 引入了 Callable 接口。

特性 Runnable Callable
核心方法 void run() V call() throws Exception
返回值 (泛型 V)
异常 不能抛出受检异常 可以抛出受检异常
如何获取结果 无法直接获取 需要通过 Future<V> 对象来获取
使用场景 适用于不需要返回结果的并发任务 适用于需要返回结果或可能抛出异常的并发任务

CallableRunnable 的增强版。


  • Runnable 是 Java 多线程编程的基础,它定义了一个可以被线程执行的任务。
  • 使用 Runnable 的标准流程:创建实现类 -> 创建 Thread 对象 -> 调用 start()
  • 核心优势:解决了 Java 单继承的局限,实现了任务与线程的分离,使得资源共享更简单,设计更灵活。
  • Thread 类相比Runnable 是更推荐的方式。
  • 在现代 Java 中Runnable 是线程池(ExecutorService)的标准任务单元,是实现高并发应用的关键部分。

掌握 Runnable 是迈向高级 Java 并发编程的第一步,也是最重要的一步。

分享:
扫描分享到社交APP
上一篇
下一篇