一、概述
记录时间 [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. 参考示例
编写代码
- 定义一个继承自 Thread 类的子类
TestThread1
; - 重写
run()
方法; - 在主线程中创建线程对象
testThread1
; - 设置线程名称
setName()
; - 调用
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. 参考示例
编写代码
- 定义一个实现了
Runnable
接口的类TestThread3
; - 重写
run()
方法; - 创建实现了
Runnable
接口的类的实例testThread3
; - 创建
Thread
对象并传入Runnable
实例; - 调用
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
接口时,同一个对象如何被多个线程使用。
多线程抢票是一个典型的并发问题示例,它涉及到多个线程同时尝试购买有限数量的票。在这个场景中,如果不正确地处理并发问题,可能会导致诸如超卖、数据不一致等问题。
编写代码
- 自定义类
TestThread4
实现Runnable
接口; - 设置票数
ticketNums
为 10; - 重写
run()
方法,描述抢票的过程; - 创建实现了
Runnable
接口的类的实例ticket
; - 创建
Thread
对象并传入实例ticket
,给线程命名new Thread(ticket, "小明")
; - 设置 3 个线程,模拟 3 类角色抢票;
- 调用
start()
方法开启线程; 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()
方法来启动线程。
- 调用
- 获取结果
- 通过调用
FutureTask
的get()
方法来获取线程的结果。
- 通过调用
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();
; - 通过调用
futureTask
的get()
方法获取线程的结果。
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()
方法。 - 通过
FutureTask
的get()
方法获取线程的结果,该方法会阻塞直到线程完成。 call()
方法可以抛出异常,这些异常需要在调用get()
方法时处理。
五、三种方法的异同
下面通过多线程同步下载图片的案例,来观察上述三种方式的区别。
1. 导入工具包
Apache Commons IO 是 Apache Commons 项目的一部分,该项目为 Java 开发者提供了一系列实用的库。
Commons IO 库特别专注于提供用于处理输入/输出操作(I/O)的工具类,使得这些操作比仅使用标准 Java 库更加方便和高效。
FileUtils - 提供文件操作的方法,如复制、移动、删除文件以及列出目录中的文件。
获取工具包
从 MVN 仓库中获取 commons-io-2.6.jar
包
添加到类路径
IDEA 中的操作步骤为:
- 在我们的 Java 项目的
src
目录下新建package
,命名为lib
; - 将
commons-io-2.6.jar
包放入lib
中; - 将
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/