0 序
0.1 别慌,我也是菜鸡!
多线程可以说是一道技术门槛,跨过了可以说是对与JAVA技术掌握,就到了另一个门槛。
但是很难跨过啊!
网上挺多讲的蛮好的书的,也是一点点引导,各种知识点讲的透透的。
这样的送到嘴边的精华挺好的,但是有一点不好的点就是,我现在半天就要入手多线程做一个简单的业务功能,我哪来的及去翻书啊!家人们!谁懂啊!
所以我这里总结一篇快速入门JAVA多线程的教程。
PS:如果发现有错漏,请私信我,给我一点face,我玻璃心9999+!
0.2 咱们学习有路线!
第一个知识点,我们会讲解 什么是多线程 , 多线程的作用是什么 还有 多线程的主要应用场景 ,都是小故事,所以完全没有难度,所以不要慌。
第二,我们就直接以迅雷不及掩耳之势,做几个小示例,了解最最最简单的多线程是怎么在JAVA里面跑的。注意啊,这里的示例只能用于学习,完全没法应用于程序开发,所以看一下就OK了。
第三步,也就是将我们的线程池,如果你要真正让多线程应用到业务中,那你就一定要掌握到 线程池 这个知识点。
OK,别着急,我知道你看到 线程池 这个概念很懵,没关系,我只是告诉你我们的文章终点在哪,你暂时不用理解。
上面就是我们的文章架构,OK,话不多说,咱们冲!
2 多线程是个什么东西?
我们举一个例子来说明一下,
假设我们现在开了一家快递店,就我们自己一个人送快递,我们现在接到了张三、李四和王五的订单。
是不是我们得一家一家的送,送完张三、再送李四,最后送王五。
最后可能因为超时还可能被投诉。
为了解决超时的问题,我们现在又合伙了两个小伙伴,这样是不是我们就可以同一时间完成三分订单。
上面的例子中,快递店就是相当于 进程 ,类似于QQ、微信这类的程序,而我们几个小伙伴就是 线程 ,用来同时处理QQ、微信的多条信息和操作,使得数据处理更快。
OK,以上就是多线程通俗的理解,这里不会长篇大论的讲一大堆知识点,咱们快马加鞭,继续冲!
3 爷,今天就要看看都能整出什么花里胡哨!
在上面的章节我们超级通俗粗略的讲述了什么是多线程及其作用。
接下来我们上手我们第二节的内容,东西比较多,坚持住,掌握了就可以进入后面的最最最重要环节了。
我们会学习 Thread 、 Runnable 、 Callable 、线程池还有四种方法创建多线程。
3.1 最最最简单的线程创建方法 —— Thread
第一步,既然我们要操作多线程,那我们可能需要一个工具和方法,而这个工具方法就封装在了一个超级重要的类里面 —— Thread类。
所以要实现对应的多线程,则要继承Thread类型,重写对应的方法。
OK,我们这里直接上代码,一步步讲解:
// 主函数
public class Main {
public static void main(String[] args) {
// 声明一个HelloThread多线程类
HelloThread helloThread = new HelloThread();
// 启动多线程
helloThread.start();
}
}
// 创建一个HelloThread类继承Thread 实现多线程功能
public class HelloThread extends Thread {
// 在这里实现我们的业务逻辑
public void run() {
System.out.println("HelloThread!");
}
}
输出:
HelloThread!
上面是一个线程的示例,
我们首先创建一个HelloThread类继承Thread从而使具备操作线程的能力,并且重写run函数,在run的函数里面实现我们要做的事情。
// 创建一个HelloThread类继承Thread 实现多线程功能
public class HelloThread extends Thread {
public void run() {
System.out.println("HelloThread!");
}
}
之后,我们开始声明一个helloThread类,并通过继承Thread得到的start(),通过start()方法执行该线程。
// 声明一个HelloThread多线程类
HelloThread helloThread = new HelloThread();
// 启动多线程
helloThread.start();
最终输出结果:
HelloThread!
以上就是最简单的创建线程和运行线程的操作,
记住一个非常重要的话,Thread是Java线程里面的核心类,后面的所有创建线程的方法本质上都只是调用Thread的不同方式而已。
OK,我的好哥们,你能了解上面的内容,你开始摸到多线程的门了,已经实现从0到1的飞跃了,给自己一个掌声!
但是,一个线程就只是单线程,接下来我们同时创建两个线程,了解多线程的运行情况,go! go! go!
3.2 最最最简单的多线程2 —— Thread多线程示例
我们现在通过Thread创建两个线程,代码如下,
// 主函数
public class Main {
public static void main(String[] args) {
// 声明一个多线程类
HelloThread helloThread1 = new HelloThread("线程1");
HelloThread helloThread2 = new HelloThread("线程2");
// 启动多线程
helloThread1.start();
helloThread2.start();
}
}
public class HelloThread extends Thread {
// 多线程名称
private String name;
HelloThread(String name) {
this.name = name;
}
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(name + "Hello,World!-" + i);
// 每个线程暂停2毫秒,模拟正在运行任务
try {
sleep(2);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}
输出结果:
线程1Hello,World!-0
线程2Hello,World!-0
线程2Hello,World!-1
线程1Hello,World!-1
线程2Hello,World!-2
线程1Hello,World!-2
运行图如下,
上面的示例看起来有点复杂,我们慢慢来讲解,
我们通过下面的语句声明了两个线程,分别是线程1和线程2
// 声明一个多线程类
HelloThread helloThread1 = new HelloThread("线程1");
HelloThread helloThread2 = new HelloThread("线程2");
创建成功后,我们启动多线程。
// 启动多线程
helloThread1.start();
helloThread2.start();
最后的输出结果就是如下:
线程1Hello,World!-0
线程2Hello,World!-0
线程2Hello,World!-1
线程1Hello,World!-1
线程2Hello,World!-2
线程1Hello,World!-2
我们可以根据上面运行图看到,
首先执行的是线程1的第一个输出,“线程1Hello,World!-0”,然后暂停2ms的过程中,线程2也运行到了第一个输出"线程2Hello,World!-0",然后又暂停了2ms,继续往后执行。
我们可以看到,这两个线程并不是先跑完线程1再执行线程2,而是这两个线程是同时在跑,各自在执行自己的内容。
这就体现了最简单的多线程,多个线程同时在执行任务,各自运行自己的逻辑。
作为最简单的多线程创建方法,必然有着它的局限性,虽然我们可以精准控制线程的开启,但是我们的必须直接继承Thread类,同时业务逻辑是跟线程本身耦合的,所以这个方法并不适合用于生产开发,所以我们接下来继续往下看另一个创建方法。
3.3 Runnable接口创建多线程
代码示例如下,
// 继承Runnable类
class PrintTask implements Runnable {
private String message;
PrintTask(String message) {
this.message = message;
}
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println(i + message);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class Main {
public static void main(String[] args) {
PrintTask task1 = new PrintTask("Hello");
PrintTask task2 = new PrintTask("World");
Thread thread1 = new Thread(task1);
Thread thread2 = new Thread(task2);
thread1.start();
thread2.start();
}
}
输出结果:
0Hello
0World
1World
1Hello
2World
2Hello
OK,首先我们创建PrintTask类继承Runnable,在PrintTask实现run方法,里面写我们要运行的代码。
运行流程如下,
其实我们可以看到,Runnable创建线程的本质,其实也是调用Thread对象创建实例。
那通过继承Runnable接口去创建线程比直接继承Thread对象创建线程最大的好处就是
“业务逻辑和线程直接分离,实现代码解耦,同时业务逻辑可以继续继承别的类。”
如果你第一次时间没看懂上面这句话没关系,后面我会加文章讲述这几种创建线程方法的利弊。这里就不做过多的赘述了,总之记住,以后创建多线程多用第二和第三种,不要直接继承Thread去创建。
OK,了解了第二种线程创建方式。
那我们现在思考一个问题,如果我们想要获取多线程的返回值,那怎么办?
我们看上面的两种方法,其实都没有返回值。
那如果我们要在线程结束后获取线程的返回值怎么办?
那我们就用如下的方法创建线程。
3.4 老子要的就是结果 —— Callable
如果我们要获取线程完成后的返回值,那我们可以通过继承***Callable***抽象类去创建多线程。
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
public class Trader implements Callable<Integer> {
public Integer call() {
// 执行交易任务
return 42;
}
}
public class Main {
public static void main(String[] args) throws Exception {
Trader trader = new Trader();
FutureTask<Integer> futureTask = new FutureTask<>(trader);
Thread thread = new Thread(futureTask);
thread.start();
// 获取交易结果
int result = futureTask.get();
System.out.println("交易结果:" + result);
}
}
输出结果:
交易结果:42
最后,我们通过调用future.get()方法来获取宝藏寻找的结果。这个方法会阻塞主线程,直到Callable执行完成并返回结果。
需要注意的是,Callable需要与ExecutorService一起使用,并且在使用完后需要关闭ExecutorService以释放资源。
以上就是我们创建线程的三种方法。
OK,学会了上面这三种,那我们就相当于拿到了弹药,但是还缺发生大炮的炮筒。
为什么这么说?
我们可以看到上面的三种方法,是不是每种方法,都要我们手动的去创建线程,启动线程,完全没法根据系统的实际作用去启动。
十分的原始。
那有什么办法可以让线程的创建和启动完全自动化,可以让线程在CUP忙的时候先歇一下,在CUP空闲的就哐哐运行呢?
有,那就是我们的线程池!
通过线程池我们可以先对线程更加智能化的管理!让线程的创建和启动由系统按照实际情况控制,而这才真正让多线程这个功能能在生产环境使用。
4 最终的大BOSS —— 线程池
4.1 啥是线程池啊?
我们学习了上面三种创建线程的方法,分别是通过 Thread 、 Runnable 、 Callable 方法去创建流程。
但是每次都是我们自己手动开启线程,结束线程,如果不小心创建了线程但是没有及时结束线程,那岂不是会导致可以用的资源越来越少,最后导致没有资源可用,导致程序崩溃,这不直接要被领导吊起打!
那有什么方法,可以让我们只要提供要做的业务逻辑,然后让系统自己去控制线程的使用、销毁等等一大堆乱七八糟的琐事呢?
当然就是我们的线程池啦!
线程池作用最大作用就是对线程资源进行控制。
(1)可以重复利用线程。比如我们如果要执行10个相同的任务,但是我们系统中只能同时创建三个县城,那如果用普通方法去创建10个线程就会导致系统资源不足导致程序出问题。但是通过线程池,我们可以限制最大线程数,比如设置为2,那么系统就会只用两个线程同时去完成这10个任务。
(2)自动控制线程的创建和销毁,极大的减少了我们代码量。本来我们只要写100行代码去控制代码的启动、分配和销毁,但是现在只要调用线程池就行,代码量缩短为10行以内。
OK,让我们开始学习吧!
既然我们要调用线程池,那我们就需要创建线程池的工具,而这个工具就是我们的JAVA类 —— Executor。
JAVA引进了这个***Executor***抽象类,但是作为一个抽象类,那就说明对应的线程池操作方法并不是在***Executor***实现的,
而JAVA又引进了***ExectorService***这个抽象类,继承***Executor***这个抽象类,但是两个抽象类继承来继承去,那还是只是一堆方法规范而已,并没有创建线程池的实际代码实现。
那真正能能操作线程池的类是——ThreadPoolExecutor。
***ThreadPoolExecutor***继承了***ExectorService***并实现了里面的方法,从而具备了操作线程池的作用。
可以通过***ThreadPoolExecutor***实现对线程池的创建、启动和管理。
但是由于 ThreadPoolExecutor 创建线程池的方法还有一个弊端,就是它实例化的时候需要传入很多参数,简单来说, ThreadPoolExecutor 就类似于一种游戏,玩这个游戏还需要我们从头到尾去捏角色,得捏半天才能正式开始,使用起来有点反人类,所以JAVA有非常银杏化的封装了 ThreadPoolExecutor 类,加入了 Executors 这个类,从而让线程池对象的创建更加方便快捷好用,我们就可以直接选已经有的角色直接开始我们的game了!
所以,说到最后,我们其实要用的,是 Executors 这个类。
是不是有点弯弯绕绕的,没关系,看不懂?也没关系,只要记住我们要用Executors这个类去创建我们想要的线程池就行了!
现在正式开始我们的学习吧!
4.2 我们的贴心大哥 —— Exectors
4.2.1 创建最简单的线程池方法 —— newSingleThreadExecutor
我们可以通过Exectors的方法去创建一个单线程线程池示例对象,也就是线程池里面最多只能创建一个线程,然后通过这个线程池对象去管理线程。
代码示例如下
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class TaskManager {
public static void main(String[] args) {
// 通过Executors的newSingleThreadExecutor创建一个单线程池,用来执行我们的线程方法。
ExecutorService executor = Executors.newSingleThreadExecutor();
// 通过Runnable创建我们要运行的线程
Runnable task = new Runnable() {
public void run() {
System.out.println("任务正在执行...");
}
};
// 通过execute方法将我们的线程任务导入线程池并开始执行
executor.execute(task);
// 调用shutdown方法关闭线程池,线程池会在所有线程执行完任务后,自动关闭。
executor.shutdown();
}
}
在这个例子中,我们使用Executors工厂类的newSingleThreadExecutor()方法创建了一个单线程的线程池。
然后,我们创建了一个Runnable对象作为我们的任务。在任务的run()方法中,我们简单地打印出一条消息表示任务正在执行。
通过调用executor.execute()方法,我们将任务提交给线程池来执行。
最后,我们调用executor.shutdown()方法来关闭线程池,线程池就会在执行完所有线程任务后,关闭所有线程。
这就是最简单的线程池示例,也是可以通过上面的方法在开发中使用线程池。
但是我们不可能就用单线程的线程池啊,是吧,就一个线程在池子里调用任务,那不还是单线程嘛!
所以我们还要学习Executors的其它创建线程池的方法。
4.2.2 自定义线程数量的线程池创建方法 —— newFixedThreadPool
上面的方法只能创建单线程,那我们现在学习一个同时允许运转多个线程的线程池创建方法 —— newFixedThreadPool。
我们直接看开发示例,
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FixedThreadPoolExample {
public static void main(String[] args) {
// 创建固定大小为3的线程池
ExecutorService executor = Executors.newFixedThreadPool(3);
// 提交任务到线程池
for (int i = 0; i < 5; i++) {
Runnable task = new Task(i);
executor.execute(task);
}
// 关闭线程池
executor.shutdown();
}
}
class Task implements Runnable {
private int taskId;
public Task(int taskId) {
this.taskId = taskId;
}
public void run() {
System.out.println("任务 " + taskId + " 正在执行,线程名:" + Thread.currentThread().getName());
try {
// 模拟任务执行时间
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("任务 " + taskId + " 完成");
}
}
输出结果:
任务 1 正在执行,线程名:pool-1-thread-2
任务 2 正在执行,线程名:pool-1-thread-3
任务 0 正在执行,线程名:pool-1-thread-1
任务 2 完成
任务 3 正在执行,线程名:pool-1-thread-3
任务 0 完成
任务 4 正在执行,线程名:pool-1-thread-1
任务 1 完成
任务 3 完成
任务 4 完成
在这个示例中,我们使用Executors.newFixedThreadPool(3)创建了一个固定大小为3的线程池。
然后,我们使用一个循环提交了5个任务到线程池中。每个任务都是一个实现了Runnable接口的Task对象。
在Task对象的run()方法中,我们简单地打印出任务的编号和执行线程的名称,并模拟任务执行了2秒钟。
通过executor.execute(task)方法,我们将任务提交给线程池来执行。
当线程一到三的时候运行三个任务的时候,剩余两个任务会等待,等前面线程完成后才会继续完成后面的任务
最后,我们调用executor.shutdown()方法来关闭线程池,线程池就会在所有任务执行完后自动关闭和销毁所有线程。
使用newFixedThreadPool()方法创建固定大小的线程池,可以限制并发执行的任务数量,适用于控制资源消耗或限制并行度的场景。
到这里,通过newnewFixedThreadPool创建线程池,就说明我们可以在开发环境简单的使用多线程了,也正是开始跨入多线程的学习大门了。
但是多线程还有好多要注意的点,
比如如果多个线程共用同一个资源,该怎么处理?
如果我想主线程先暂停,等分支线程执行完后再执行,该怎么处理?
如果其中分支线程出现问题后,我们如何捕捉和处理异常?
还有其它情况,这就需要后续各位小伙伴们自己摸索了。