多线程 02:线程实现,创建线程的三种方式,通过多线程下载图片案例分析异同(Thread,Runnable,Callable)

一、概述

记录时间 [2024-08-08]

前置知识Java 基础篇Java 面向对象
多线程 01:Java 多线程学习导航,线程简介,线程相关概念的整理

Java 多线程学习主要模块包括:线程简介;线程实现;线程状态;线程同步;线程通信问题;拓展高级主题。

本文详细介绍了 3 种创建线程的方式,并通过多线程下载图片的案例,分析了这 3 种方法的异同。

  • 继承 Thread 类;
  • 实现 Runnable 接口;
  • 实现 Callable 接口。

二、继承 Thread 类

1. 查阅资料

在 Java 中,通过继承 Thread 类来创建线程是一种常见的方法。

如图,通过 JDK8 帮助文档可知,这种方法允许在子类中重写 run() 方法,定义线程执行的具体任务。

在这里插入图片描述


2. 操作步骤

  • 创建子类
    • 创建一个新的类继承自 Thread 类。
  • 重写 run() 方法
    • 在子类中重写 run() 方法,定义线程执行的具体逻辑。
  • 创建线程对象
    • 创建继承自 Thread 类的子类的实例。
  • 启动线程
    • 调用线程对象的 start() 方法来启动线程。

3. 参考示例

编写代码

  1. 定义一个继承自 Thread 类的子类 TestThread1
  2. 重写 run() 方法;
  3. 在主线程中创建线程对象 testThread1
  4. 设置线程名称 setName()
  5. 调用 start() 方法开启线程。
// 创建线程方式一:继承 Thread 类,重写 run() 方法,调用 start 开启线程
// 总结:(注意)线程开启不一定立即执行,由 CPU 调度执行

public class TestThread1 extends Thread {

    // 线程入口点
    @Override
    public void run() {
        // run 方法线程体
        for (int i = 0; i < 200; i++) {
            System.out.println("多线程测试-------" + i);
        }
    }

    public static void main(String[] args) {
        // main 线程,主线程

        // 创建一个线程对象
        TestThread1 testThread1 = new TestThread1();
        
        // 设置线程名称
        // testThread1.setName("MyThread Thread");

        // 调用 start() 方法,开启线程
        testThread1.start();

        // 主线程中的代码
        for (int i = 0; i < 1000; i++) {
            System.out.println("这是主线程-------" + i);
        }
    }

}

得到结果

执行上述代码,得到以下结果(结果不唯一)。

不难发现,主程序和线程是同时执行的。

在这里插入图片描述


注意事项

  • 通过继承 Thread 类创建线程是一种简单直接的方法,适用于不需要从线程返回结果的情况
  • 通过 setName() 方法可以设置线程的名称,这有助于调试和识别线程。
  • 线程的实际执行逻辑放在 run() 方法中。
  • 必须调用 start() 方法来启动线程,而不是直接调用 run() 方法。直接调用 run() 方法会导致线程在当前线程中执行,而不是作为一个新的线程执行。
  • 线程启动后不会立即执行,而是由 CPU 安排调度。
  • 不能继承其他类。因为 Java 不支持多重继承,所以如果继承了 Thread 类,就不能再继承其他类。如果需要继承其他类,可以考虑实现 Runnable 接口。
  • Thread 类本身实现了 Runnable 接口(通过查看源码)。

三、实现 Runnable 接口

1. 查阅资料

在 Java 中,通过实现 Runnable 接口来创建线程是一种更为常见的方法。

如图,通过 JDK8 帮助文档可知,这种方法允许定义一个类实现 Runnable 接口,并重写 run() 方法来指定线程执行的具体任务。

在这里插入图片描述

2. 操作步骤

  • 创建实现 Runnable 接口的类
    • 创建一个新的类,并让它实现 Runnable 接口。
  • 重写 run() 方法
    • 在实现的类中重写 run() 方法,定义线程执行的具体逻辑。
  • 创建 Runnable 实例
    • 创建实现了 Runnable 接口的类的实例。
  • 创建 Thread 对象并传入 Runnable 实例
    • Thread 类充当代理类;
    • 创建 Thread 类的实例,并将实现了 Runnable 接口的类的实例作为构造函数的参数传入。
  • 启动线程
    • 调用 Thread 对象的 start() 方法来启动线程。

3. 参考示例

编写代码

  1. 定义一个实现了 Runnable 接口的类 TestThread3
  2. 重写 run() 方法;
  3. 创建实现了 Runnable 接口的类的实例 testThread3
  4. 创建 Thread 对象并传入 Runnable 实例;
  5. 调用 start() 方法开启线程。
// 创建线程方式二:实现 runnable 接口,重写 run 方法,执行线程需要丢入 runnable 接口实现类,调用 start 方法

public class TestThread3 implements Runnable {

    @Override
    public void run() {
        // run 方法线程体
        for (int i = 0; i < 200; i++) {
            System.out.println("多线程测试-------" + i);
        }
    }

    public static void main(String[] args) {
        // main 线程,主线程

        // 创建 runnable 接口的实现类对象
        TestThread3 testThread3 = new TestThread3();

        // 创建线程对象,通过线程对象来开启线程
        // 一种代理
//        Thread thread = new Thread(testThread3);
//        thread.start();

        // 简写方式
        new Thread(testThread3, "name").start();

        // 主线程中的代码
        for (int i = 0; i < 1000; i++) {
            System.out.println("这是主线程-------" + i);
        }
    }
}

注意事项

  • 通过 Thread 构造函数中的第二个参数可以设置线程的名称,这有助于调试和识别线程。
  • 线程的实际执行逻辑放在实现了 Runnable 接口的类的 run() 方法中。
  • 必须调用 start() 方法来启动线程,而不是直接调用 run() 方法。
  • 实现 Runnable 接口的类可以同时继承其他类,这提供了更大的灵活性,方便同一个对象被多个线程使用。

4. 参考示例:初识并发


实现 Runnable 接口,避免了单继承局限性,灵活方便,方便同一个对象被多个线程使用。

通过购票案例,我们来了解实现 Runnable 接口时,同一个对象如何被多个线程使用。

多线程抢票是一个典型的并发问题示例,它涉及到多个线程同时尝试购买有限数量的票。在这个场景中,如果不正确地处理并发问题,可能会导致诸如超卖、数据不一致等问题

编写代码

  1. 自定义类 TestThread4 实现 Runnable 接口;
  2. 设置票数 ticketNums 为 10;
  3. 重写 run() 方法,描述抢票的过程;
  4. 创建实现了 Runnable 接口的类的实例 ticket
  5. 创建 Thread 对象并传入实例 ticket,给线程命名 new Thread(ticket, "小明")
  6. 设置 3 个线程,模拟 3 类角色抢票;
  7. 调用 start() 方法开启线程;
  8. Thread.currentThread().getName():获取当前线程的名字。
// 多个线程同时操作同一个对象
// 买火车票的例子

// 发现问题:多个线程操作同一个资源的情况下,线程不安全,数据紊乱

// 1. 自定义类 TestThread4 实现 Runnable 接口
public class TestThread4 implements Runnable {

    // 票数
    // 2. 设置票数 ticketNums 为 10
    private int ticketNums = 10;

    // 3. 重写 run 方法,描述抢票的过程
    @Override
    public void run() {

        // 如果票数为 0,表示票卖完了
        while (true) {
            if (ticketNums <= 0) {
                break;
            }

            // 模拟延时
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            // 8. 获取当前线程的名字
            System.out.println(Thread.currentThread().getName() + "-->拿到了第" + (ticketNums--) + "票");
        }

    }

    public static void main(String[] args) {

        // 一份资源
        // 4. 创建实现了 Runnable 接口的类的实例 ticket
        TestThread4 ticket = new TestThread4();

        // 多个代理
        /* 
        	5. 创建 Thread 对象并传入实例 ticket,给线程命名;
        	6. 设置 3 个线程,模拟 3 类角色抢票;
        	7. 调用 start() 方法开启线程;
        */
        new Thread(ticket, "小明").start();
        new Thread(ticket, "老师").start();
        new Thread(ticket, "黄牛党").start();
    }
}

得到结果

执行上述代码,得到以下结果(结果不唯一)。

发现问题:老师和小明都拿到了第 6 票,出现了并发。多个线程操作同一个资源的情况下,线程不安全,数据紊乱。

在这里插入图片描述


5. 参考示例:龟兔赛跑

案例分析

实现一个“龟兔赛跑”的多线程程序是一个有趣的示例,可以帮助理解线程的创建和控制。在这个示例中,我们将创建两个线程,一个代表乌龟,另一个代表兔子,它们将沿着赛道前进,直到到达终点。

  • 设定赛道的总距离,随着比赛的进行,参赛者们逐渐向终点靠近。
  • 判断比赛是否结束。
  • 当有一方到达终点时,打印出胜利者的名字。
  • 比赛开始:经典的龟兔赛跑故事。
  • 兔子在比赛中途需要休息,通过延时模拟兔子中途打盹的情景。
  • 乌龟最终赢得了比赛。

编写代码

胜利者只有一个

// 模拟龟兔赛跑
public class Race implements Runnable {

    // 胜利者
    private static String winner;
    
    // ....
}

判断是否完成比赛

// 判断是否完成比赛
private boolean gameOver(int steps) {
    // 判断是否有胜利者
    if (winner != null) {
        // 已经存在胜利者了
        return true;
    } {
        // 步数等于 100,说明完成了比赛
        if (steps >= 100) {
            winner = Thread.currentThread().getName();
            System.out.println("winner is " + winner);
            return true;
        }
    }

    return false;
}

重写 run() 方法

  • 判断比赛是否结束
  • 如果比赛结束了,就停止程序
  • 模拟兔子休息
@Override
public void run() {

    for (int i = 0; i <= 100; i++) {

        // 模拟兔子休息,跑 20 步休息一下
        if (Thread.currentThread().getName().equals("兔子") && i%20==0) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 判断比赛是否结束
        boolean flag = gameOver(i);

        // 如果比赛结束了,就停止程序
        if (flag) {
            break;
        }

        System.out.println(Thread.currentThread().getName() + "-->跑了" + i + "步");
    }

}

创建并启动两个线程

创建并启动两个线程,分别代表兔子和乌龟。

public static void main(String[] args) {

    Race race = new Race();

    // 创建两个线程
    new Thread(race, "兔子").start();
    new Thread(race, "乌龟").start();

}

四、实现 Callable 接口

Callable 接口与 Runnable 接口类似,但是它允许线程返回一个值,并且可以抛出异常。

1. 操作步骤

  • 创建实现 Callable 接口的类
    • 创建一个新的类,并让它实现 Callable 接口。
  • 重写 call() 方法
    • 在实现的类中重写 call() 方法,定义线程执行的具体逻辑。
    • call() 方法可以返回一个结果,并且可以抛出异常。
  • 创建 Callable 实例
    • 创建实现了 Callable 接口的类的实例。
  • 创建 FutureTask 对象
    • 创建 FutureTask 对象,并将实现了 Callable 接口的类的实例作为构造函数的参数传入。
    • FutureTask 对象允许你从线程获取结果。
  • 创建 Thread 对象并传入 FutureTask 实例
    • 创建 Thread 类的实例,并将 FutureTask 对象作为构造函数的参数传入。
  • 启动线程
    • 调用 Thread 对象的 start() 方法来启动线程。
  • 获取结果
    • 通过调用 FutureTaskget() 方法来获取线程的结果。

2. 参考示例

编写代码

  • 定义一个实现了 Callable 接口的类;
  • 重写 call() 方法,定义线程执行的具体逻辑。
// 定义一个实现了 Callable 接口的类
public class MyCallable implements Callable<Integer> {
    
    // 重写 call() 方法,定义线程执行的具体逻辑
    @Override
    public Integer call() throws Exception {
        System.out.println("Calculating the sum...");
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
            try {
                Thread.sleep(100); // 模拟耗时操作
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return sum;
    }
}

  • 创建实现了 Callable 接口的类的实例 callable
  • 创建 FutureTask 对象 futureTask
  • 创建 Thread 对象 thread 并传入 futureTask
  • 启动线程 thread.start();
  • 通过调用 futureTaskget() 方法获取线程的结果。
class Main {
    public static void main(String[] args) {
        // 创建实现了 Callable 接口的类的实例
        Callable<Integer> callable = new MyCallable();

        // 创建 FutureTask 对象
        FutureTask<Integer> futureTask = new FutureTask<>(callable);

        // 创建 Thread 对象并传入 FutureTask 实例
        Thread thread = new Thread(futureTask, "MyCallable Thread");

        // 启动线程
        thread.start();

        try {
            // 获取线程的结果
            Integer result = futureTask.get();
            System.out.println("The sum is: " + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

得到结果

这个案例最终得到了 1~100 数字的累加和。

Calculating the sum...
The sum is: 5050

注意事项

  • 线程的实际执行逻辑放在实现了 Callable 接口的类的 call() 方法中。
  • 必须调用 start() 方法来启动线程,不要直接调用 call() 方法。
  • 通过 FutureTaskget() 方法获取线程的结果,该方法会阻塞直到线程完成。
  • call() 方法可以抛出异常,这些异常需要在调用 get() 方法时处理。

五、三种方法的异同

下面通过多线程同步下载图片的案例,来观察上述三种方式的区别。

1. 导入工具包

Apache Commons IO 是 Apache Commons 项目的一部分,该项目为 Java 开发者提供了一系列实用的库。

Commons IO 库特别专注于提供用于处理输入/输出操作(I/O)的工具类,使得这些操作比仅使用标准 Java 库更加方便和高效。

FileUtils - 提供文件操作的方法,如复制、移动、删除文件以及列出目录中的文件。

获取工具包

MVN 仓库中获取 commons-io-2.6.jar

在这里插入图片描述


添加到类路径

IDEA 中的操作步骤为:

  1. 在我们的 Java 项目的 src 目录下新建 package,命名为 lib
  2. commons-io-2.6.jar 包放入 lib 中;
  3. lib 添加到项目的类路径中。
    • 右键 --> Add as Library...

在这里插入图片描述


检查是否添加成功File-->Project Structure-->Libraries

在这里插入图片描述


2. 编写代码(公共部分)

编写图片下载器

使用工具类编写图片下载器,用于下载图片。

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

// 下载器:文件下载工具类
class WebDownloader {
    // 下载方法
    public void downloader(String url, String name) {
        try {
            // 工具类:通过网页地址下载文件
            // Commons IO 是一个工具类库,针对开发 IO 流功能
            // FileUtils 是文件工具,复制 url 到文件
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO 异常,downloader 方法出现问题");
        }
    }
}

编写构造器

编写构造器,用于构造对象传递图片参数。

private String url;     // 网络图片地址
private String name;     // 保存的文件名,可以自己取

// 构造器
public TestThread2(String url, String name) {
    this.url = url;
    this.name = name;
}

3. 编写代码(各自部分)

继承 Thread 类


通过继承 Thread 类并重写 run 方法来定义线程的行为。

步骤一:继承 Thread

public class TestThread2 extends Thread {
    //...
}

步骤二:重写 run() 方法

// 重写 run 方法
// 下载图片线程的执行体
@Override
public void run() {
    WebDownloader webDownloader = new WebDownloader();
    webDownloader.downloader(url, name);
    System.out.println("下载了文件名为:" + name);
}

步骤三:编写主方法

// 主方法
public static void main(String[] args) {

    // 通过构造器创建线程对象
    // 远程路径 + 存储名字
    TestThread2 t1 = new TestThread2("url1", "图片1.png");
    TestThread2 t2 = new TestThread2("url2", "图片2.png");
    TestThread2 t3 = new TestThread2("url3", "图片3.png");

    // 启动线程,表面上是按顺序启动的
    // 实际上是同时执行的
    t1.start();
    t2.start();
    t3.start();

}

// 测试图片路径
// https://i-blog.csdnimg.cn/blog_migrate/326b74ea2db3909a616d2fb7b7de5260.png
// https://i-blog.csdnimg.cn/blog_migrate/4597506ad46a5b1ed0fcefa38b6e301e.png
// https://i-blog.csdnimg.cn/blog_migrate/f536d984323427e75a40041526d5aab7.png

步骤四:执行结果

当使用多线程下载图片时,由于线程的调度是由操作系统和 JVM 控制的,并且线程的执行顺序是不确定的,因此图片的实际下载顺序通常不会与提交任务的顺序相同。

不难发现,下载图片的顺序与我们启动线程的顺序不相同。

在这里插入图片描述


实现 Runnable 接口


通过实现 Runnable 接口并重写 run 方法来定义线程的行为。之后,可以将 Runnable 实例传递给 Thread 的构造函数来创建线程。

步骤一:实现 Runnable 接口

public class TestThread5 implements Runnable {
    //...
}

步骤二:重写 run() 方法

同继承 Thread 类,二者都需要重写 run() 方法。


步骤三:编写主方法

// 主方法
public static void main(String[] args) {

    // 通过构造器创建线程对象
    // 远程路径 + 存储名字
    TestThread5 t1 = new TestThread5("url", "图片1.png");
    TestThread5 t2 = new TestThread5("url", "图片2.png");
    TestThread5 t3 = new TestThread5("url", "图片3.png");

    // 启动线程 runnable
    new Thread(t1).start();
    new Thread(t2).start();
    new Thread(t3).start();
}

不同点在于,实现 Runnable 接口,启动线程时需要借助代理对象

启动线程的方式:

  • 继承 Thread 类:子类对象.start()
  • 实现 Runnable 接口:传入目标对象 + Thread 对象.start()
// Thread
myThread.start();

// Runnable
new Thread(myThread).start();

实现 Callable 接口


Callable 接口与 Runnable 接口类似,但是它允许线程返回一个值,并且可以抛出异常。

步骤一:实现 Callable 接口

public class TestCallable2 implements Callable<Boolean> {}

步骤二:重写 call() 方法

// 重写 call() 方法
// 下载图片线程的执行体
@Override
public Boolean call() {
    WebDownloader webDownloader = new WebDownloader();
    webDownloader.downloader(url, name);
    System.out.println("下载了文件名为:" + name);

    return true;
}

步骤三:编写主方法

  • 通过构造器创建线程对象;
  • 创建 FutureTask 对象;
  • 创建 Thread 对象并传入 FutureTask 实例,并启动线程;
  • 获取线程结果,需要抛出异常。
// 主方法
public static void main(String[] args) throws ExecutionException, InterruptedException {

    // 通过构造器创建线程对象
    TestCallable2 t1 = new TestCallable2("url", "图片1.png");
    TestCallable2 t2 = new TestCallable2("url", "图片2.png");
    TestCallable2 t3 = new TestCallable2("url", "图片3.png");

    // 创建 FutureTask 对象
    FutureTask<Boolean> f1 = new FutureTask<>(t1);
    FutureTask<Boolean> f2 = new FutureTask<>(t2);
    FutureTask<Boolean> f3 = new FutureTask<>(t3);

    // 创建 Thread 对象并传入 FutureTask 实例
    // 启动线程
    new Thread(f1).start();
    new Thread(f2).start();
    new Thread(f3).start();

    // 获取线程结果
    Boolean rs1 = f1.get();
    Boolean rs2 = f2.get();
    Boolean rs3 = f3.get();

    System.out.println(rs1);
    System.out.println(rs2);
    System.out.println(rs3);


}

4. 多线程下载图片完整代码

图片下载——继承 Thread 类

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

// 练习 Thread,实现多线程同步下载图片
public class TestThread2 extends Thread {

    private String url;     // 网络图片地址
    private String name;     // 保存的文件名,可以自己取

    // 构造器
    public TestThread2(String url, String name) {
        this.url = url;
        this.name = name;
    }

    // 重写 run 方法
    // 下载图片线程的执行体
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下载了文件名为:" + name);
    }

    // 主方法
    public static void main(String[] args) {

        // 通过构造器创建线程对象
        // 远程路径 + 存储名字
        TestThread2 t1 = new TestThread2("https://i-blog.csdnimg.cn/blog_migrate/326b74ea2db3909a616d2fb7b7de5260.png", "图片1.png");
        TestThread2 t2 = new TestThread2("https://i-blog.csdnimg.cn/blog_migrate/4597506ad46a5b1ed0fcefa38b6e301e.png", "图片2.png");
        TestThread2 t3 = new TestThread2("https://i-blog.csdnimg.cn/blog_migrate/f536d984323427e75a40041526d5aab7.png", "图片3.png");

        // 启动线程,表面上是按顺序启动的
        // 实际上是同时执行的
        t1.start();
        t2.start();
        t3.start();
    }
}

// 下载器:文件下载工具类
class WebDownloader {
    // 下载方法
    public void downloader(String url, String name) {
        try {
            // 工具类:通过网页地址下载文件
            // Commons IO 是一个工具类库,针对开发 IO 流功能
            // FileUtils 是文件工具,复制 url 到文件
            FileUtils.copyURLToFile(new URL(url), new File(name));
        } catch (IOException e) {
            e.printStackTrace();
            System.out.println("IO 异常,downloader 方法出现问题");
        }
    }
}

图片下载——实现 Runnable 接口

import org.apache.commons.io.FileUtils;

import java.io.File;
import java.io.IOException;
import java.net.URL;

public class TestThread5 implements Runnable {

    private String url;     // 网络图片地址
    private String name;     // 保存的文件名,可以自己取

    // 构造器
    public TestThread5(String url, String name) {
        this.url = url;
        this.name = name;
    }

    // 重写 run 方法
    // 下载图片线程的执行体
    @Override
    public void run() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下载了文件名为:" + name);
    }

    // 主方法
    public static void main(String[] args) {

        // 通过构造器创建线程对象
        // 远程路径 + 存储名字
        TestThread5 t1 = new TestThread5("https://i-blog.csdnimg.cn/blog_migrate/326b74ea2db3909a616d2fb7b7de5260.png", "图片1.png");
        TestThread5 t2 = new TestThread5("https://i-blog.csdnimg.cn/blog_migrate/4597506ad46a5b1ed0fcefa38b6e301e.png", "图片2.png");
        TestThread5 t3 = new TestThread5("https://i-blog.csdnimg.cn/blog_migrate/f536d984323427e75a40041526d5aab7.png", "图片3.png");

        // 启动线程 runnable
        new Thread(t1).start();
        new Thread(t2).start();
        new Thread(t3).start();
    }
}

// 下载器:文件下载工具类
//class WebDownloader {
//    // 下载方法
//    public void downloader(String url, String name) {
//        try {
//            // 工具类:通过网页地址下载文件
//            // Commons IO 是一个工具类库,针对开发 IO 流功能
//            // FileUtils 是文件工具,复制 url 到文件
//            FileUtils.copyURLToFile(new URL(url), new File(name));
//        } catch (IOException e) {
//            e.printStackTrace();
//            System.out.println("IO 异常,downloader 方法出现问题");
//        }
//    }
//}

图片下载——实现 Callable 接口

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

// 创建线程方式三:实现 callable 接口
/*
    callable 的好处:
    1. 可以定义返回值
    2. 可以抛出异常
 */

public class TestCallable2 implements Callable<Boolean> {

    private String url;     // 网络图片地址
    private String name;     // 保存的文件名,可以自己取

    // 构造器
    public TestCallable2(String url, String name) {
        this.url = url;
        this.name = name;
    }

    // 重写 call() 方法
    // 下载图片线程的执行体
    @Override
    public Boolean call() {
        WebDownloader webDownloader = new WebDownloader();
        webDownloader.downloader(url, name);
        System.out.println("下载了文件名为:" + name);

        return true;
    }

    // 主方法
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        // 通过构造器创建线程对象
        TestCallable2 t1 = new TestCallable2("https://i-blog.csdnimg.cn/blog_migrate/326b74ea2db3909a616d2fb7b7de5260.png", "图片1.png");
        TestCallable2 t2 = new TestCallable2("https://i-blog.csdnimg.cn/blog_migrate/4597506ad46a5b1ed0fcefa38b6e301e.png", "图片2.png");
        TestCallable2 t3 = new TestCallable2("https://i-blog.csdnimg.cn/blog_migrate/f536d984323427e75a40041526d5aab7.png", "图片3.png");

        // 创建 FutureTask 对象
        FutureTask<Boolean> f1 = new FutureTask<>(t1);
        FutureTask<Boolean> f2 = new FutureTask<>(t2);
        FutureTask<Boolean> f3 = new FutureTask<>(t3);

        // 创建 Thread 对象并传入 FutureTask 实例
        // 启动线程
        new Thread(f1).start();
        new Thread(f2).start();
        new Thread(f3).start();

        // 获取线程结果
        Boolean rs1 = f1.get();
        Boolean rs2 = f2.get();
        Boolean rs3 = f3.get();

        System.out.println(rs1);
        System.out.println(rs2);
        System.out.println(rs3);


    }

}


// 下载器
//class WebDownloader {
//    // 下载方法
//    public void downloader(String url, String name) {
//        try {
//            // 工具类:通过网页地址下载文件
//            FileUtils.copyURLToFile(new URL(url), new File(name));
//        } catch (IOException e) {
//            e.printStackTrace();
//            System.out.println("IO 异常,downloader 方法出现问题");
//        }
//    }
//}

六、参考资料

狂神说 Java 多线程:https://www.bilibili.com/video/BV1V4411p7EF
TIOBE 编程语言走势: https://www.tiobe.com/tiobe-index/
Typora 官网:https://www.typoraio.cn/
Oracle 官网:https://www.oracle.com/
Notepad++ 下载地址:https://notepad-plus.en.softonic.com/
IDEA 官网:https://www.jetbrains.com.cn/idea/
Java 开发手册:https://developer.aliyun.com/ebook/394
Java 8 帮助文档:https://docs.oracle.com/javase/8/docs/api/
MVN 仓库:https://mvnrepository.com/

  • 29
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值