// Runnable 方式实现多线程
class MyRunnable01 implements Runnable {
    /**
     * 重写run方法,实现线程的执行逻辑
     */
    @Override
    public void run() {
        // 循环输出当前线程的名称
        while (true) {
            System.out.println(Thread.currentThread().getName() +
                    ":run()方法在运行");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
public class Example02 {
    public static void main(String[] args) throws InterruptedException {
        // 创建1个自定义线程任务对象
        MyRunnable01 mr = new MyRunnable01();
        //创建2个线程对象
        Thread t1 = new Thread(mr, "线程1");
        Thread t2 = new Thread(mr, "线程2");
        // 启动线程
        t1.start();
        t2.start();
        // 在主方法中执行while循环
        while (true) {
            System.out.println("main()方法在运行");
            //让当前线程暂停1000毫秒
            Thread.sleep(1000);
        }
    }
}

1. 任务定义 (MyRunnable01)

程序定义了一个实现 Runnable 接口的类。这相当于定义了一个名为“任务”的蓝图,规定了线程启动后要做的具体事情:不断输出线程名,并暂停 1 秒。

2. 多线程的启动 (main 方法)

在 main 方法中,程序创建了三个独立的“执行流”:

  • t1:一个执行 mr 任务的线程,被命名为“线程1”。
  • t2:一个执行 mr 任务的线程,被命名为“线程2”。
  • main 线程:这是程序的“总指挥”,执行 main 方法中的循环。

3. 并发执行机制

当执行到 t1.start() 和 t2.start() 时,JVM 会通知操作系统创建两个新的线程。此时,程序处于并发运行状态:

  • 调度器轮换:这三个线程(t1, t2, main)在 CPU 上并不是绝对同步的。操作系统会由调度器在它们之间快速切换,因此你会看到控制台交替打印出“线程1”、“线程2”和“main()方法”的信息。
  • 独立的死循环:每个线程内部都有 while(true),这意味着它们一旦启动就不会自动停止。Thread.sleep(1000) 的作用是让该线程暂时让出 CPU 使用权,进入阻塞状态一秒钟,这样其他线程就有机会获得执行权。

4. 总结:三个线程的关系

你可以把这个程序想象成三个人在同一间办公室工作

  • 每个人(线程)手里都拿着同一份工作说明书(MyRunnable01 的逻辑)。
  • 每个人每做完一步(输出信息)后,都会休息 1 秒钟(sleep)。
  • 虽然大家按相同的规则工作,但在任何一个瞬间,谁在说话(输出)是由“办公室主管”(系统 CPU)来分配的,所以输出顺序是不固定的,看起来是交织在一起的。

// 使用 Callable 和 FutureTask 实现多线程
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable01 implements Callable<String> {
    @Override
    public String call() throws InterruptedException {
        // 循环输出当前线程的名称
        while (true) {
            String str = Thread.currentThread().getName() +
                    ":call()方法在运行";
            System.out.println(str);
            Thread.sleep(1000);
            return str;
        }
    }
}
public class Example03 {
    public static void main(String[] args) throws Exception {
        // 创建Callable线程任务对象
        Callable<String> call01 = new MyCallable01();
        Callable<String> call02 = new MyCallable01();
        // 通过FutureTask对象封装Callable对象
        FutureTask<String> f1 = new FutureTask<>(call01);
        FutureTask<String> f2 = new FutureTask<>(call02);
        // 创建线程对象
        Thread t1 = new Thread(f1, "线程1");
        Thread t2 = new Thread(f2, "线程2");
        // 启动线程
        t1.start();
        t2.start();
        while (true) {
            if (f1.isDone()) {
                System.out.println(f1.get());
            }
            if (f2.isDone()) {
                System.out.println(f2.get());
            }
            System.out.println("main()");
            Thread.sleep(1000);
        }
    }
}

相比于之前看到的 Runnable,这个版本最核心的区别在于:它能从线程中获取返回值

1. Callable 接口:有“回执”的任务

Runnable 的 run() 方法没有返回值,而 Callable 的 call() 方法是有返回值的(这里返回 String)。这就像是你交给快递员任务,Callable 不仅会帮你运东西,还能把任务的结果(比如签收回执)带回来。

2. FutureTask:任务的“代理人”

Thread 构造函数只接受 Runnable 接口,不直接支持 Callable。因此,我们需要 FutureTask 来作为中间人:

  • 它封装了 Callable 任务。
  • 它实现了 RunnableFuture 接口(既能被执行,又能存结果)。
  • 当线程执行完 call() 方法后,结果会自动保存在 FutureTask 中,主程序可以通过 f1.get() 随时领取这个结果。

3. 主线程的“状态检查”机制 (isDone)

在 main 方法的循环中,你看到了一段很有趣的逻辑:

if (f1.isDone()) {
    System.out.println(f1.get());
}
  • isDone():这是一个询问机制,类似于主线程在问:“任务完成了没?”。
  • get():只有当 isDone() 返回 true 时,调用 get() 才会立即拿到结果。如果任务还没完成,get() 方法默认会阻塞(暂停等待),直到结果出来。

4. 程序执行过程的变化

这个程序和上一个 Runnable 示例表现上有显著不同:

  • 只输出一次:注意你的 MyCallable01 中虽然有 while(true),但在循环内部执行了一次打印和休息后,紧接着就是 return str;。这会导致线程在执行一次任务后直接结束(终止)。
  • 任务结束后不再执行:因为 MyCallable01 执行完一次就返回了,所以 t1 和 t2 线程会很快走向死亡(线程状态变为 TERMINATED)。
  • main 线程持续工作main 线程会不断轮询 f1 和 f2 的状态。你会观察到:
    1. 前 1 秒:线程 1 和线程 2 输出结果,main 线程输出 main()
    2. 1 秒后:任务结束,之后的循环里 f1.isDone() 和 f2.isDone() 持续为 true,主线程会一直打印之前拿到的那个结果值。

总结比较

特性 Runnable 方式 Callable 方式
返回值 有 (通过 FutureTask 获取)
异常处理 只能在 run 内部捕获 可以抛出,由调用者接收
适用场景 简单的后台任务 需要获取计算结果的场景

核心提示:这个程序展示了异步获取结果的模式。在大型应用中,我们通常会让线程去计算复杂的数据,计算完成后,主线程再通过 FutureTask 统一收集结果。

更多推荐