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

news2024/11/24 17:39:21

一、概述

记录时间 [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/

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1992836.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

MySQL 体系架构

文章目录 一. MySQL 分支与变种1. Drizzle2. MariaDB3. Percona Server 二. MySQL的替代1. Postgre SQL2. SQLite 三. MySQL 体系架构1.连接层2 Server层&#xff08;SQL处理层&#xff09;3. 存储引擎层1&#xff09;MySQL官方存储引擎概要2&#xff09;第三方引擎3&#xff0…

【java】一维数组

目录 一维数组内存分析Java虚拟机的内存划分一维数组内存解析 一维数组知识点一维数组课后练习 一维数组内存分析 Java虚拟机的内存划分 为了提高运行效率&#xff0c;就对空间进行了不同区域的划分&#xff0c;因为每一片区域都有特定的处理数据方式和内存管理方式。 java中…

cs224w colab0笔记

1.colab0 1.1 数据集 from torch_geometric.datasets import KarateClubdataset KarateClub() print(fDataset:{dataset}:) print() print(fNumber of graphs:{len(dataset)}) print(fNumber of features:{dataset.num_features}) print(fNumber of classes:{dataset.num_cl…

机器学习面试-核心概念-问题理解

1.机器学习的思想 计算机程序随着经验的积累&#xff0c;能够实现性能的提高。对于某一类任务T及其性能度量P&#xff0c;若一个计算机程序在T上以P衡量的性能随着经验E而自我完善&#xff0c;那么就称这个计算机程序在从经验E学习。 机器学习是人工智能的一个分支&#xff0c…

VMware Workstation 与 Device/Credential Guard 的不兼容问题

&#xff08;1&#xff09;出现问题 &#xff08;2&#xff09;出现问题原因&#xff1a; 我电脑原先弄过Hyper-V&#xff0c;这玩意是微软公司开发的一款虚拟化产品&#xff0c;它是微软第一个采用类似 VMware 和 Citrix Xen 等产品中的基于 hypervisor&#xff08;虚拟机监控…

基于区块链的供应链应用开发

区块链的供应链溯源应用开发 一 、环境准备 (1)更新镜像源 apt update(2)安装(openssl、jdk、git) apt -y install openssl default-jdk git(3)配置JAVA_HOME环境变量 echo “export JAVA_HOME=/usr/lib/jvm/java-11-openjdk-amd64/” >> /etc/profilesource /etc…

一键编译并启动一个 ARM Linux qemu 虚拟机

需要事先自己编译 qemu-system-arm 可执行文件&#xff1b; 1&#xff0c;编译创建ARM 虚拟机 1.1 一起从头开始版 cd 进一个空文件夹&#xff0c;然后 $ vim buildup.sh $ bash buildup.sh 访问github网络没什么问题&#xff1a; 硬编码了一个路径/home/hipper/ex_vexpre…

Java对象内存布局和Synchronized锁升级(二)

目录 对象内存布局对象头实例数据对齐填充锁在内存布局中的标志位 锁升级无锁偏向锁偏向锁升级 轻量级锁重量级锁 锁消除和锁粗化锁消除锁粗化 锁升级总结 对象内存布局 在HotSpot虚拟机里&#xff0c;对象在堆内存中的存储布局可以划分为三个部分:对象头(Header)、实例数据(I…

同态加密和SEAL库的介绍(八)性能

本篇会对比三种加密方案&#xff0c;同时每种方案配置三种参数。即九种情况下的各个操作的性能差异&#xff0c;为大家选择合适的方案和合适的参数提供参考。表格中所有时长的单位均为微妙&#xff0c;即 。 当然数据量比较大&#xff0c;为了方便大家查找&#xff0c…

应用商店故障(UOS系统)

应用商店故障&#xff08;UOS系统&#xff09; 1. 安装应用商店内的应用无法下载&#xff0c;更新系统时提示依赖关系被破坏&#xff0c;怎么办&#xff1f; 问题描述 安装应用商店内的应用无法下载&#xff0c;更新系统时均提示依赖关系被破坏 解决方案 1、可先建议用户尝试修…

day 22线程间通信

一、互斥锁 1、资源&#xff1a; 资源是有限的&#xff0c;在程序运行过程中&#xff0c;一段代码、一段空间、一个变量、CPU、内存都可以看做资源 2、互斥锁&#xff1a; 是一种资源,当一个线程任务加锁,其余线程任务无法再次加锁,直到解锁后才能加锁&#xff0c;互斥…

数据机房防静电措施有哪些?安装防静电地板时记住这几点

生活中静电无处不在&#xff0c;一般情况静电不会对我们有什么影响&#xff0c;但在一些特殊场合&#xff0c;比如数据机房、配电室、消控室、电子厂房等&#xff0c;静电的危害必须要引起重视&#xff0c;因为这些场合通常有比较多的电子设备&#xff0c;电子设备中有比较多的…

priority_queue模拟实现【C++】

文章目录 全部的实现代码放在了文章末尾什么是适配器模式&#xff1f;准备工作包含头文件定义命名空间类的成员变量什么是仿函数&#xff1f;比较仿函数在priority_queue中的作用通过传入不同的仿函数可以做到大堆和小堆之间的切换通过传入不同的仿函数可以做到改变priority_qu…

[Leetcode][Medium]-面试题 17.14.最小k个数-TOP K问题-快排/大根堆

一、题目描述 原题地址 二、整体思路 (1)、快排 数组中最小的k个数就是说把数组升序排列&#xff0c;求[0,k-1]区间上的数。 快排可以得到一个元素在升序排序的数组中的正确位置。在这个位置的左边区间[l,l2-1]上的元素都比它小&#xff0c;在这个位置的右边区间[r2,r]上的元素…

zabbix 监控软件

zabbix 监控软件 自带图形化界面&#xff0c;通过网页就可以监控所有的服务器的状态。 事件告警&#xff0c;邮箱通知&#xff08;噩梦&#xff09;。 zabbix是什么&#xff1f; web界面提供的分布式监控以及网络监控功能的开源的企业级的软件解决方案。 服务端 监控端 客…

App安装来源追踪的四大方案解析

App的开发者和运营商&#xff0c;都会研究分析渠道的效果&#xff0c;而对渠道来源的追根溯源是一切分析的基础。假如没有明确的安装来源数据&#xff0c;至少会造成以下几种后果&#xff1a; 没有安装来源数据&#xff0c;我们无法判断各个投放渠道流量的价值&#xff0c;也就…

全网首发!鸿蒙OS登上PC,冒风险流出内测,系统界面截图,过会儿就删

开玩笑的啦&#xff0c;其实这是Deepin操作系统的截图&#xff0c;很漂亮吧&#xff0c;这是deepin v23 rc2 的主题之一 鸿蒙还没有发&#xff0c;不知道24年末还能不能上&#xff0c;emmm

Python实战:基础语法

一、求解列表中的最大元素 import random#定义函数 def get_max(lst):x lst[0] #x存储的是元素的最大值#遍历操作for i in range(1,len(lst)):if lst[i] > x:x lst[i] #对最大值进行重新赋值return x#调用函数 lst [random.randint(1,100) for item in range(10)] print…

基于SiliconCloud快速体验GraphRag.Net

SiliconCloud介绍 SiliconCloud 基于优秀的开源基础模型&#xff0c;提供高性价比的 GenAI 服务。 不同于多数大模型云服务平台只提供自家大模型 API&#xff0c;SiliconCloud上架了包括 Qwen、DeepSeek、GLM、Yi、Mistral、LLaMA 3、SDXL、InstantID 在内的多种开源大语言模…

sgetrf M N is 103040 时报错,这是个bug么 lapack and Openblas the same,修复备忘

1,现象 MN103040时&#xff0c;调用 sgetrf_ 时&#xff0c;无论是 LAPACK 还是 OpenBLAS&#xff0c;都出错&#xff1a; openblas&#xff1a; lapack&#xff1a; 2, 复现代码 出现问题的应该是由于M和N相对数字太大&#xff0c;乘积超出32bit整数的表达范围&#xff0c;…