一篇文章超快读懂JAVA多线程(入门到开发)

0 序

0.1 别慌,我也是菜鸡!

多线程可以说是一道技术门槛,跨过了可以说是对与JAVA技术掌握,就到了另一个门槛。

但是很难跨过啊!

网上挺多讲的蛮好的书的,也是一点点引导,各种知识点讲的透透的。

这样的送到嘴边的精华挺好的,但是有一点不好的点就是,我现在半天就要入手多线程做一个简单的业务功能,我哪来的及去翻书啊!家人们!谁懂啊!

所以我这里总结一篇快速入门JAVA多线程的教程。

PS:如果发现有错漏,请私信我,给我一点face,我玻璃心9999+!

0.2 咱们学习有路线!

第一个知识点,我们会讲解 什么是多线程多线程的作用是什么 还有 多线程的主要应用场景 ,都是小故事,所以完全没有难度,所以不要慌。

第二,我们就直接以迅雷不及掩耳之势,做几个小示例,了解最最最简单的多线程是怎么在JAVA里面跑的。注意啊,这里的示例只能用于学习,完全没法应用于程序开发,所以看一下就OK了。

第三步,也就是将我们的线程池,如果你要真正让多线程应用到业务中,那你就一定要掌握到 线程池 这个知识点。

OK,别着急,我知道你看到 线程池 这个概念很懵,没关系,我只是告诉你我们的文章终点在哪,你暂时不用理解。

在这里插入图片描述

上面就是我们的文章架构,OK,话不多说,咱们冲!

在这里插入图片描述

2 多线程是个什么东西?

我们举一个例子来说明一下,

假设我们现在开了一家快递店,就我们自己一个人送快递,我们现在接到了张三、李四和王五的订单。

是不是我们得一家一家的送,送完张三、再送李四,最后送王五。

在这里插入图片描述

最后可能因为超时还可能被投诉。

为了解决超时的问题,我们现在又合伙了两个小伙伴,这样是不是我们就可以同一时间完成三分订单。

在这里插入图片描述

上面的例子中,快递店就是相当于 进程 ,类似于QQ、微信这类的程序,而我们几个小伙伴就是 线程 ,用来同时处理QQ、微信的多条信息和操作,使得数据处理更快。

OK,以上就是多线程通俗的理解,这里不会长篇大论的讲一大堆知识点,咱们快马加鞭,继续冲!
在这里插入图片描述

3 爷,今天就要看看都能整出什么花里胡哨!

在上面的章节我们超级通俗粗略的讲述了什么是多线程及其作用。

接下来我们上手我们第二节的内容,东西比较多,坚持住,掌握了就可以进入后面的最最最重要环节了。

我们会学习 ThreadRunnableCallable 、线程池还有四种方法创建多线程。

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 啥是线程池啊?

我们学习了上面三种创建线程的方法,分别是通过 ThreadRunnableCallable 方法去创建流程。

但是每次都是我们自己手动开启线程,结束线程,如果不小心创建了线程但是没有及时结束线程,那岂不是会导致可以用的资源越来越少,最后导致没有资源可用,导致程序崩溃,这不直接要被领导吊起打!
在这里插入图片描述

那有什么方法,可以让我们只要提供要做的业务逻辑,然后让系统自己去控制线程的使用、销毁等等一大堆乱七八糟的琐事呢?

当然就是我们的线程池啦!

线程池作用最大作用就是对线程资源进行控制。

(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创建线程池,就说明我们可以在开发环境简单的使用多线程了,也正是开始跨入多线程的学习大门了。

但是多线程还有好多要注意的点,

比如如果多个线程共用同一个资源,该怎么处理?

如果我想主线程先暂停,等分支线程执行完后再执行,该怎么处理?

如果其中分支线程出现问题后,我们如何捕捉和处理异常?

还有其它情况,这就需要后续各位小伙伴们自己摸索了。

在这里插入图片描述

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值