【多线程实践】一、为何使用多线程&三种线程创建方式利弊分析

2022-10-08 09:00:39 浏览数 (1)

hello,你好呀,我是灰小猿,一个超会写bug的程序猿!

在平常的业务场景中,多线程无疑是比较常用的,而且熟练的使用多线程是开发高并发系统的基础,今天呢,我们就来根据在实际开发中是如何使用多线程的来探讨一下多线程的相关技术,少讲理论多谈实践,以实际开发的角度去总结一下。

一、认识多线程

先来认识几个基本概念吧,回顾一下基础。

何为进程?

进程是程序的一次执行过程,是系统运行的基本单位,它是动态执行的,有自己的生命周期,当我们启动一个程序的时候,就是一个进程创建,程序开始运行就是进程的运行,程序运行结束后这个进程自然也就消亡了,

当然在这个程序执行的过程中你可以去调用很多其他的方法或者业务,但是这都不影响这仍然只是一个进程,而不是多个进程的结合体。

何为线程?

线程要比进程小一个执行单位,一个进程在执行的过程中可以产生多个线程,就像你在程序执行的过程中既可以进行算术运算,又可以进行文件读取是一样的,算术运算可以调用一个cpu内核运行,文件处理可以调用另一个。

线程又可以被看作是轻量级的进程,进程中有堆和方法栈等资源,进程中的多个线程可以共享这些资源,但是线程又有自己独立的程序计数器、虚拟机栈和本地方法栈。多个线程之间互不干扰。

何为并发?

举个例子先:下班了你有三十分钟的时间去吃饭,吃饭的时候来了一个bug,你放下手上的筷子去修复了这个bug,经过测试bug修复完成,之后你又继续去吃饭,那么在这三十分钟里面,你既吃了饭又修了bug,那你就是支持并发的。

何为并行?

还是上面那个例子,你正在吃饭,这个时候经理说有bug,但是你很饿,所以你边吃饭边去修复这个bug,最后你吃完了饭也修复了bug,你同时进行了吃饭和修bug这两件事,那么你就是支持并行的。

总结一下

并发:

  • 同一时间段内多个任务都在执行
  • 但是单位时间内不一定同时执行 并行:
  • 单位时间内多个任务同时进行

当然,如果你在下班的这个时间只能吃饭而不能修复bug,或者只能修复bug而不能吃饭,那你就是既不支持并发也不支持并行的。

二、为什么使用多线程?

使用一个东西之前总要知道为什么用它对吧,总有使用它的道理的。

先看一个串行任务:

你下班去餐厅吃饭,但是餐厅只有一个窗口在打饭,而且大妈还手抖,这个时候你在排着长队,可能轮到你的时候,黄花菜都没有了。

如果这是代码中的一个场景的话,那么执行时间=单个任务执行的时间*任务数量

那换成并行任务会是怎样呢?

食堂多开了两个窗口,原本只有一队的人现在排成了三队,排队时间直接除以3,而且还有一部分人去点了外卖。这个时候大家都在同时打饭,也都能吃上饭。

如果在代码中,执行任务的时间=串行的时间/cpu核心数

再从深层分析一下使用多线程的好处:

从计算机底层考虑

线程是轻量级的进程,是程序执行的最小单位,线程之间切换和调度的成本是远小于进程间切换的, 而且我们知道现在的电脑基本上都是4核或者8核cpu的,这也就意味着多个线程可以同时运行。这也就减少了线程上下文切换的开销。

从当前互联网的发展趋势考虑

现在很多软件或网站,动不动就是几百上千万的并发量场景,如果这些任务一个一个执行可想而知用户请求时间多久,而多线程就是开发高并发业务的基础,利用好多线程机制可以大大的提高系统整体的执行性能。

从单核时代到多核时代考虑

在以前单核时代,多线程设计的目的主要是为了提高线程利用CPU和IO的效率,在当时的进程中只有一个线程,如果这个线程阻塞了那么整个程序都会阻塞,而且CPU和IO设备也只能有一个运行,这样下来执行效率直接给折了50%。

现在是多核时代,这个时候的多线程主要就是为了提高进程利用CPU的能力,换句话说就是最大程度的压榨CPU资源。 假设现在有一个复杂任务只有一个线程,那么不管你的电脑再厉害,有再多的CPU,那还是那一个CPU在执行,但是如果是多线程的,那么可能就是多个线程映射在多个CPU在同时执行这个任务,哪个执行得快很显然了。这个时候多核任务的执行时间=单核下任务的执行时间/CPU核心数

三、线程的五种状态

线程从它创建到其消亡,是有一个完整的生命周期的。具体如下:

四、创建线程的三种方式

上面我们了解了为什么使用多线程,那么接下来呢我们就来看一下在Java中如何创建多线程,他们都有什么各自的优缺点。

(1)继承Thread类(不推荐使用)

第一种方法是继承Thread类的方式,这种方式的实现需要重写其中的run方法,并且在其中实现需要异步实现的方法体,具体如下:

代码语言:javascript复制
/**
 *创建线程方式一:继承Thread类
 */
public class TestThread01 extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 10; i  ) {
            System.out.println("线程方法体-线程名:"   Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        //创建一个线程对象
        TestThread01 testThread01 = new TestThread01();
        //调用start方法开启线程
        testThread01.start();

        for (int i = 0; i < 10; i  ) {
            System.out.println("主线程方法-线程名:"   Thread.currentThread().getName());
        }
    }
}

方法总结: 继承Thread类来创建线程的方式一般是推荐使用的,原因是我们知道Java是具有单继承的局限性的,一般如果继承了Thread类,那么这个类就不能再继承其他类了。

(2)实现Runnable接口(推荐使用)

创建线程的第二种方法是实现Runnable接口,和上面继承Thread的方法类似,使用这种方法也需要重写接口中实现的run方法。具体如下:

代码语言:javascript复制
/**
 * 创建线程方式2:实现runnable接口
 */
public class TestThread03 implements Runnable {
    @Override
    public void run() {
        //run方法线程体
        for (int i = 0; i < 10; i  ) {
            System.out.println("线程方法体-线程名:"   Thread.currentThread().getName());
        }
    }

    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        TestThread03 testThread03 = new TestThread03();
        //创建线程对象,通过线程对象来开启线程代理
        new Thread(testThread03).start();
        for (int i = 0; i < 10; i  ) {
            System.out.println("主线程方法-线程名"   Thread.currentThread().getName());
        }
    }
}

方法总结: 实现runnable接口的方式是大部分情况下我们创建多线程都会使用的,并且在很多源码的底层实现中也是使用的这种方法,原因是它采用接口的方式,完美的解决了Java单继承的问题,而且使用起来灵活方便,方便同一个对象被多个线程使用,扩展性也更强。

(3)实现Callable接口(不推荐使用)

第三种创建多线程的方式是实现Callable接口,这种方式与前两种方式都有所不同,使用这种方式创建线程之后,我们可以使用其中的get()方法获取到异步执行的方法体的返回值,这是其他两种方法所不能的,具体的实现如下:

代码语言:javascript复制
import java.util.concurrent.*;
/**
 * 创建线程方式3:实现Callable接口
 */
public class TestThread05 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建三个线程
        MyCallable myCallable1 = new MyCallable(100);
        MyCallable myCallable2 = new MyCallable(200);
        MyCallable myCallable3 = new MyCallable(300);

        //提交线程并执行
        FutureTask<String> task1 = new FutureTask<>(myCallable1);
        FutureTask<String> task2 = new FutureTask<>(myCallable2);
        FutureTask<String> task3 = new FutureTask<>(myCallable3);

        new Thread(task1).start();
        new Thread(task2).start();
        new Thread(task3).start();

        //通过get方法获取执行结果
        System.out.println(task1.get());
        System.out.println(task2.get());
        System.out.println(task3.get());
    }

}

/**
 * 实现Callable接口
 */
class MyCallable implements Callable<String> {
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }

    /**
     * 重写call方法
     *
     * @return 返回值
     * @throws Exception
     */
    @Override
    public String call() throws Exception {
        int num = n * n;
        return Thread.currentThread().getName()   "获取结果:"   num;
    }
}

我们可以看一下执行结果:

方法总结: 实现Callable接口的方式虽然可以获取到方法执行的返回结果,但是如果不是特殊情况,我们一般是不会通过这种方式来创建线程的,因为这种方法相对于继承Thread类来说虽然是解决了Java单继承的局限性,但是相对于实现Runnable接口而言,没有实现Runnable方法操作简单,所以这种方法我们一般也是不推荐使用的。

以上呢就是对多线程基本认识的一些简单分享,后续我将继续和大家分享多线程及线程池的最佳实践以及底层实现原理。

我是灰小猿,我们下期见!

0 人点赞