浅谈多线程与高并发(一),线程定义与实现方式

前言

多线程是每位开发者必须要掌握的东西,同时它也是基础中的重中之重,有了它我们能充分利用起cpu的性能,提高系统的效率,以及防止一些并发问题。

进程与线程

谈多线程之前首先需要了解进程与线程的关系。

进程是啥?

它是现系统当中资源分配的最小单位,电脑中的任务管理器里面的就是一个个的进程。
在这里插入图片描述

线程是啥?

并发并不一定依赖多线程,但Java里谈论并发大多数都与线程脱不开关系。
线程是比进程更轻量级的调度执行单位,线程的引入可以把一个进程的资源分配和执行调度分开,各个线程既可以共享进程资源(内存地址,文件IO等),又可以独立调度(线程是CPU调度的基本单位)。
Thread类的所有关键方法都声明了native的,意味着这个方法没有使用或无法使用平台无关的手段来实现,也有可能是为了执行效率。

实现线程主要有三种方式:使用内核线程实现,使用用户线程实现和使用用户线程加轻量级进程混合实现。

1,使用内核线程实现(1:1)
内核线程(KLT,Kernel-Level Thread),直接由操作系统内核(Kernel,即内核)支持的线程。由内核来完成线程切换,内核通过操纵调度器(Scheduler)对线程进行调度,并负责将线程的任务映射到各个处理器上。每个内核线程可以视为内核的一个分身,这样操作系统就有能力同时处理多件事情,支持多线程的内核叫做多线程内核。
程序一般不会去直接使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(LWP),即通常意义上的线程。由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。*轻量级进程与内核线程之间1:1关系称为一对一的线程模型。
在这里插入图片描述

内核线程保证了每个轻量级进程都成为一个独立的调度单元,即时有一个轻量级进程在系统调用中阻塞了,也不会影响整个进程的继续工作。
局限:基于内核线程实现,因此各线程操作等需要系统调用,系统调用代价高,需要在用户态和内核态来回切换,其次,每个轻量级进程都需要一个内核线程的支持,因此轻量级进程要消耗一定的内核资源,如内核线程的栈空间,因此一个系统支持轻量级进程的数量是有限的。

2,使用用户线程实现(N:1)
广义上,内核线程以外,就是用户线程。轻量级也算用户线程,但轻量级进程的实现始终是建立在内核上的,许多操作都要进行系统调度,效率会受到限制。
狭义上,用户线程指完全建立在用户空间的线程库上。这种线程不需要切换内核态,效率非常高且低消耗,也可以支持规模更大的线程数量,部分高性能数据库中的多线程就是由用户线程实现的。这种进程与用户线程之间1:N的关系称为一对多的线程模型。
在这里插入图片描述

用户线程优势在于不需要系统内核支援,劣势也在于没有系统内核的支援,所有的线程操作都是需要用户程序自己处理。阻塞处理等问题的解决十分困难,甚至不可能完成。所以使用用户线程会非常复杂。

3,用户线程加轻量级进程混合实现(N:M)
内核线程与用户线程混合使用。可以使用内核提供的线程调度功能及处理器映射,并且用户线程的系统调用要通过轻量级线程来完成,大大降低整个进程被完全阻塞的风险。用户线程与轻量级进程比例是N:M。
在这里插入图片描述

那什么是多线程?

说到多线程就不得不提两个概念,串行和并行。

串行指的是时间上不存在重叠,有严格先后顺序的,比如现有abc三个任务,必须a执行完了然后到b,b完了再到c。
在这里插入图片描述

并行指的是时间上有重叠的,可以同时执行的,打个比方,我们的电脑管家,我们要对电脑进行体检,杀毒,清理文件,理论上这三个东西是可以同时执行的,没有先后顺序的。

多线程的应用场景

1,程序中出现需要等待的操作,比如网络操作、文件IO等,可以利用多线程充分使用处理器资源,而不会阻塞程序中其他任务的执行
2,程序中出现可分解的大任务,比如耗时较长的计算任务,可以利用多线程来共同完成任务,缩短运算时间
3,程序中出现需要后台运行的任务,比如一些监测任务、定时任务,可以利用多线程来完成

多线程的传统实现方式(三种)

JDK1.2之前,绿色线程——用户线程。JDK1.2——基于操作系统原生线程模型来实现。Sun JDK,它的Windows版本和Linux版本都使用一对一的线程模型实现,一条Java线程就映射到一条轻量级进程之中。Solaris同时支持一对一和多对多

一,继承Thread类

package com.thread;
//通过继承Thread类实现自定义线程类
public class MyThread extends Thread {
	//线程体
    @Override
    public void run() {
        System.out.println("Hello, I am the defined thread created by extends Thread");
    }
    public static void main(String[] args){
        //实例化自定义线程类实例
        Thread thread = new MyThread();
        //调用start()实例方法启动线程
        thread.start();
    }
}

优点:实现简单,只需实例化继承类的实例,即可使用线程
缺点:扩展性不足,Java是单继承的语言,如果一个类已经继承了其他类,就无法通过这种方式实现自定义线程

二,实现Runnable接口

package com.thread;
public class MyRunnable implements Runnable {
    //线程体
    @Override
    public void run() {
        System.out.println("Hello, I am the defined thread created by implements Runnable");
    }
    public static void main(String[] args){
    	//线程的执行目标对象
        MyRunnable myRunnable = new MyRunnable();
        //实际的线程对象
        Thread thread = new Thread(myRunnable);
        //启动线程
        thread.start();
    }
}

优点:
1,扩展性好,可以在此基础上继承其他类,实现其他必需的功能
2,对于多线程共享资源的场景,具有天然的支持,适用于多线程处理一份资源的场景
缺点:
1,构造线程实例的过程相对繁琐一点
2,无法获取线程结束后的返回值

这里还要说的一点是Thread也是实现了Runnable接口
在这里插入图片描述

三,实现Callable接口

从最传统的开发来讲如果要进行多线程的实现肯定依靠的就是Runnable,但是Runnable接口有一个缺点:当线程执行完毕后,我们无法获取一个返回值,所以从JDK1.5之后就提出了一个新的线程实现接口:java.util.concurrent.Callable接口。首先观察这个接口的定义:
在这里插入图片描述
可以发现Callbale定义的时候可以设置一个泛型,此泛型的类型就是返回数据的类型,这样的的好处是可以避免向下转行所带来的安全隐患。

实现方式:
package com.thread;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "Hello, I am the defined thread created by implements Callable";
    }
    public static void main(String[] args){
        //线程执行目标
        MyCallable myCallable = new MyCallable();
        //包装线程执行目标,因为Thread的构造函数只能接受Runnable接口的实现类,而FutureTask类实现了Runnable接口
        FutureTask<String> futureTask = new FutureTask<>(myCallable);
        //传入线程执行目标,实例化线程对象
        Thread thread = new Thread(futureTask);
        //启动线程
        thread.start();
        String result = null;
        try {
            //获取线程执行结果
            result = futureTask.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        System.out.println(result);
    }
}

仔细看里面注释会发现这里还使用了适配器模式,因为callable并不能当构造参数直接传给Thread,所以还需要FutureTask做一个适配
优点:
1,扩展性好
2,支持多线程处理同一份资源
3,具备返回值以及可以抛出受检查异常
缺点:
1,相较于实现Runnable接口的方式,较为繁琐

小结

大家发现了没有,其实三种方式都是直接或间接的实现了Runable接口,并重写了它的run方法。三种实现方式都存在着一个使用范式,即首先实现线程执行目标对象(包含线程所要执行的任务),然后将目标对象作为构造参数以实例化Thread实例,来获得线程!本质上都是实现一个线程体,由Thread来执行线程体,达到开启线程执行任务的效果!但是,三种实现方式各有优缺点,使用时,应该结合具体需求来选用合适的实现方式进行开发!

再补充一下Springboot的多线程实现方式

Springboot是基于任务执行器(TaskExecutor)来实现多线程和并发编程的,使用ThreadPoolTaskExecutor来创建一个基于线程池的TaskExecutor。这其中大部分的线程都是异步非阻塞的。

实现方式

1,首先在Application上使用@EnableAsync来声明开启多线程管理
在这里插入图片描述

2,注册bean并实现AsyncConfigurator接口,重写getAsyncExecutor()方法配置好线程池的信息,该方法会返回一个Executor
在这里插入图片描述

3,在具体要使用多线程的方式上使用@Async注解,此时该方法便已经实现了多线程异步处理。(也可以说多线程并行处理,串行得用锁,后续文章会谈及)
在这里插入图片描述

最后,如果觉得该篇文章对你有用,麻烦请扫描关注一下我的公众号。谢谢!
在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值