多线程创建方式对比与线程池相关原理概述汇总(超详细)

🎈个人公众号:🎈 :✨✨✨ 可为编程✨ 🍟(正在建设当中,感兴趣的伙伴加v: sunsuncoder 一起交流)🍟
🔑个人信条:🔑 为与不为皆为可为🌵
🍉本篇简介:🍉 很久没有写文章了,大概有一年多了,期间总结了很多的笔记,解决了很多的问题,其中的成长不言而喻,回看之前的笔记,很多地方有出入,自己就萌生了将之前的文章再次优化的想法,争取每周一篇高质量文章,知识整合也好,学习记录也罢,在文章记录中不断夯实自己的知识储备,相互学习。

关注公众号【可为编程】回复【面试】领取2023年最新面试题!!!

概述

很久没有写文章了,一时也不知道从何写起,那就先从多线程来吧,这次想把多线程与高并发的相关知识进行一下梳理,从多线程的几种创建方式开始,逐步到线程池原理分析,再到java中常用锁的使用场景与原理分析,再到高并发的处理方案,以及后面分布式锁等知识点,分成批次来进行梳理,这次先分析一下多线程的创建方式的异同点与线程池的执行原理。
···································
关注公众号【可为编程】回复【面试】领取2023年最新面试题!!!
···································

1、线程的创建

线程的创建无非就是以下几种方式:

  1. 采用直接集成Thread类,并重写run方法
  2. 实现Runnable接口,并重写run方法
  3. 实现有返回值的Callable接口,重写call方法
  4. 采用线程池 (不建议使用Executors来创)

前三种方式不建议在程序中直接使用,这就是为什么我们要采用线程池的问题?那为啥呢?

穿插问题一、为什么要采用线程池?
  1. 减: 首先就是减少开销,频繁的创建线程会占用内存,增加内存的开销,每次请求都要创建和销毁一个线程的话会占用系统很多的资源,同时也会增大系统处理时间,使得响应时间变慢。
  2. 控: 合理的控制线程数,通过线程池的核心线程数、最大线程数、队列线程数等来控制多线程的数量,可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,使服务器崩溃。
  3. 管: 方便管理线程,线程池就就是线程的托管中心,里面包含了定义的线程数量,当任务到达时,任务可以不需要的等到线程创建就能立即执行。

上面提到,为什么不建议使用Executors来创建线程呢? 这个问题我们留在后面谈到线程池的时候再进行详细的阐述。

1.1 直接继承Thread类

/**
 * @author :zjc
 * @ProjectName: execises
 * @Package: com.execises.zjc.controller.thread
 * @ClassName: ThreadOpByThread
 * @date :Created in 2021/7/16 15:03
 * @description:创建多线程之继承Thread类
 * @modified By:
 * @version: v1.0.0$
 */
public class ThreadOpByThread extends Thread {
    @Override
    public void run() {
        super.run();
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }

    static class testThread {
        public static void main(String[] args) {
            ThreadOpByThread threadOpByThread = new ThreadOpByThread();
            threadOpByThread.start();
        }
    }

}

1.2 实现Runnable接口

/**
 * @author :zjc
 * @ProjectName: execises
 * @Package: com.execises.zjc.controller.thread
 * @ClassName: ThreadOpByThread
 * @date :Created in 2021/7/16 15:03
 * @description:创建多线程之继承Thread类
 * @modified By:
 * @version: v1.0.0$
 */
public class ThreadOpByRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + i);
        }
    }

    static class testThread {
        public static void main(String[] args) {
            ThreadOpByRunnable threadOpByThread = new ThreadOpByRunnable();
            Thread thread = new Thread(threadOpByThread);
            thread.start();
        }
    }

}

1.2 实现Callable接口

在这里我们采用两种方式,一个是采用FutureTask来接收返回值,第二个就是采用线程池来接收返回值,首先我们采用第一种方式。

/**
 * @author :zjc
 * @ProjectName: execises
 * @Package: com.execises.zjc.controller.thread
 * @ClassName: ThreadOpByThread
 * @date :Created in 2021/7/16 15:03
 * @description:创建多线程之继承Thread类
 * @modified By:
 * @version: v1.0.0$
 */
public class ThreadOpByCallable implements Callable<Integer> {
    @Override
    public Integer call() {
        int sum = 0;
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + i);
            sum += i;
        }
        return sum;
    }

    static class TestThread {
        public static void main(String[] args) throws ExecutionException, InterruptedException {
            ThreadOpByCallable threadOpByThread = new ThreadOpByCallable();
            FutureTask<Integer> ft= new   FutureTask<Integer>(threadOpByThread);
            Thread thread = new Thread(ft);
            thread.start();
            int sum = ft.get();
            System.out.println("最终值:"+sum);
        }

    }

第二种就是采用线程池的方法,这里先采用newCachedThreadPool线程池,后面我会讲解这几个线程池有啥区别,自定义线程池为啥就那么好,这里先卖个关子。

public static void main(String[] args) throws ExecutionException, InterruptedException {
            ExecutorService poolExecutor = Executors.newCachedThreadPool();
//            ExecutorService poolExecutor = Executors.newFixedThreadPool(10);
//            ExecutorService poolExecutor = Executors.newSingleThreadExecutor();
//            ExecutorService poolExecutor = Executors.newScheduledThreadPool(10);
//            ExecutorService poolExecutor = Executors.newWorkStealingPool();

            //创建一个有返回值的实例
            Callable c = new ThreadOpByCallable();
            //提交线程 获取Future对象
            Future future = poolExecutor.submit(c);
            System.out.println("运行结果" + future.get().toString());
            //关闭线程池
            poolExecutor.shutdown();


        }

这里我们就用到了Executors这个类,那么阿里规约里面,是不建议使用这个类的,使用这个类时就会弹出以下提示:在这里插入图片描述
那么我们就要想,为什么会这样提示呢?
关注公众号【可为编程】回复【面试】领取2023年最新面试题!!!

穿插问题二、为什么不建议使用Executors来创建线程池?

经过查看阿里开发手册,上面是这样写的:线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
ps:如果有想要阿里开发手册(华山版)可以在评论区留下邮箱,私发给你。
Executor提供的四个常见的静态方法创建线程池,但是阿里规约却并不建议使用它,这四个线程池也就是我上面注释掉的

  1. newCachedThreadPool
  2. newFixedThreadPool
  3. newSingleThreadExecutor
  4. newScheduledThreadPool
    我这边又多加了一个newWorkStealingPool,接下来我就用一个例子来阐述他们有什么样的关系和区别。
    首先在进入线程池章节之前,我们先思考一下这个问题,为什么我已经有了Thread还要用Runnable来创建线程呢,这俩有啥不同呢?
穿插问题三、有了Thread还要用Runnable来创建线程

1、首先我们要明白这两者的区别,Thread是类,只能通过单一继承的方式来实现,Runnable是接口,在这里可以去详细的学习一下接口和抽象类的区别,后面我也会总结一下,放到这里。
2、在使用Runnable定义的子类中没有start()方法,只有run()方法,只有Thread类中才有start()方法。

一、对于类继承,java只允许单继承模式,如果你需要设计一个类既继承业务类拥有某个业务类功能,又继承线程对象Thread类具有多线程的功能,那么Thread类就无法满足你的要求。这时候就要选择Runnable。
二、在使用Runnable定义的子类中没有start()方法,只有Thread类中才有。此时观察Thread类,有一个构造方法:public Thread(Runnable targer)此构造方法接受Runnable的子类实例,也就是说可以通过Thread类来启动Runnable实现的多线程。(start()可以协调系统的资源)

在程序开发中只要是多线程肯定永远以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:
避免点继承的局限,一个类可以继承多个接口。 适合于资源的共享

那么start()方法和run()方法有什么区别呢?

穿插问题四、start()方法和run()方法有什么区别?

这个问题看我之前写过的一篇文章:
start()方法和run()方法区别与多线程抢占式运行原理.

继续前面的问题,此时我们观察Thread类,有一个构造方法:

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

public Thread(Runnable target)此构造方法接受Runnable的子类实例,也就是说可以通过Thread类来启动Runnable实现的多线程。(start()可以协调系统的资源)

在程序开发中只要是多线程肯定永远以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:

1、避免点继承的局限,一个类可以继承多个接口。

2、适合于资源的共享

详细了解一下关于资源共享方式和方法,网上有很多,这边就举个例子,可以去看下面的链接。

https://www.iteye.com/blog/yechw-630317
关注公众号【可为编程】回复【面试】领取2023年最新面试题!!!

2.线程池

文章一开始就阐述了采用线程池的优点,如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题。如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。线程池通过复用可以大大减少线程频繁创建与销毁带来的性能上的损耗。

在前面我们讲过线程池中不建议采用Executors去创建,那我们就来对比一下,Executors中的四种方法和自定义线程池有啥不同,我在这里还是采用代码的形式,根据响应时间来进行一个对比:

/**
 * @author :zjc
 * @ProjectName: execises
 * @Package: com.execises.zjc.controller.thread.threadPool
 * @ClassName: ThreadExecutorsNewCachedThreadPool
 * @date :Created in 2021/7/18 9:40
 * @description:线程池
 * @modified By:
 * @version: v1.0.0$
 */
public class ThreadExecutors {
    static class MyThread implements Runnable {
        private String name;

        public MyThread(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "----------" + name);
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        ExecutorService poolExecutor = Executors.newCachedThreadPool(); //时间间隔为1s 最快
        //ExecutorService poolExecutor = Executors.newFixedThreadPool(10);//时间间隔为1s 慢
        //ExecutorService poolExecutor = Executors.newSingleThreadExecutor();// 时间间隔为1s 超级慢 一个一个执行
        //ExecutorService poolExecutor = Executors.newScheduledThreadPool(10); //时间间隔为1s 一般
        //ExecutorService poolExecutor = Executors.newWorkStealingPool(); //时间间隔为1s 一般
		//关注公众号【可为编程】回复【面试】领取2023年最新面试题!!!
        try {
            for (int i = 0; i < 100; i++) {
                poolExecutor.execute(new MyThread(i + ""));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            //关闭线程池
            poolExecutor.shutdown();

        }
    }
}

这里就逐一介绍一下Executors去创建线程池的四种方式:

2.1、Executors的常用线程池

Executors提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口。
在讲解这几个方法式,我们先了解一下线程池中的几个参数,以及主要的线程池对象ThreadPoolExecutor。

	//关注公众号【可为编程】回复【面试】领取2023年最新面试题!!!
    public ThreadPoolExecutor(int corePoolSize,//核心线程数
                              int maximumPoolSize,//最大线程池大小,也就是线程池总的大小
                              long keepAliveTime,//线程最大空闲时间
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue, //线程等待队列
                              ThreadFactory threadFactory, //线程创建工厂
                              RejectedExecutionHandler handler //拒绝策略
                             ) 

上面代码就是ThreadPoolExecutor中主要的参数,在这里我用一个生动形象的比喻来整体上阐述他们的关系。

2.2、外包模式分析线程池执行逻辑

目前很多互联网大厂都是采用外包的形式,这里也采用这种形式,能够通俗易懂的了解相关的逻辑原理。

corePoolSize核心线程数就是大厂公司的正式员工

maximumPoolSize减去corePoolSize就是外包过来的员工也就是非核心员工,该部分也就是线程池总的大小减去核心线程数。

workQueue是队列,用来存放每一个线程所要执行的任务,也就是公司的任务列表,类似于需求评审报告的性质。
关注公众号【可为编程】回复【面试】领取2023年最新面试题!!!
在这里插入图片描述

2.2.1、newCachedThreadPool()

创建一个可缓存的线程池,调用execute将重用以前构造的线程(如果线程可用)。如果现有线程没有可用的,则创建一个新线程并添加到池中。终止并从缓存中移除那些已有 60 秒钟未被使用的线程。有多少任务我就创建多少员工。
其实他的底层也就是直接新建了一个ThreadPoolExecutor对象,我们查看其源码如下:

  public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
corePoolSize = 0

maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制;
@Native public static final int   MAX_VALUE = 0x7fffffff;

keepAliveTime = 60s,线程空闲60s后自动结束。

workQueue 为 SynchronousQueue 同步队列,这个队列类似于一个接力棒,入队出队必须同时传递,因为CachedThreadPool线程创建无限制,不会有队列等待,所以使用SynchronousQueue;
在这里插入图片描述

首先会先到核心线程数也就是核心员工去找,看有没有核心员工,没有的话就放入到队列当中,因为是同步队列,只存在一个节点,只要有任务过来就立马创建消费流程,所以就会在非核心员工处去执行任务。因为这个部分有很多的非核心线程数,所以每一个任务都能被执行,所以速度会很快。
关注公众号【可为编程】回复【面试】领取2023年最新面试题!!!

2.2.2、newFixedThreadPool(int nThreads)
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }

线程大小为10个10个一组,创建固定数目线程的线程池。newFixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程。

LinkedBlockingQueue为无界任务队列(等待队列的大小是无界,理论上大小取决于内存大小)查看其底层源码,该队列的大小为Integer的最大值:2的31次方-1。

public LinkedBlockingQueue() {
        this(Integer.MAX_VALUE);
    }

该队列的大小为2的31次方-1

我们在程序中固定的线程数为10,所以核心员工和最大员工数量都是10,10-10为非核心员工数,所以只有核心员工10人,体现在图中就是:在这里插入图片描述
所以整个线程运行中只存在10个员工,首先执行0-9序号的10个任务,其余90个任务在队列里等待,执行完毕后,再去执行10-19序号的10个任务,其余80个在队列里等待,重复整个操作,直到所有任务都被执行完毕。
在这里插入图片描述
所以在这里是他的线程数量决定了它的运行快慢,如果将其改为100,他执行效率和newCachedThreadPool效率大别不是很大,几乎相同。

关注公众号【可为编程】回复【面试】领取2023年最新面试题!!!

2.2.3、SingleThreadExecutor()

单例线程,任意时间池中只能有一个线程用的是和cache池和fixed池相同的底层池。

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

他和newFixedThreadPool底层是相同的,只不过每次最大线程数和核心线程数都是1,所以每次执行都是单一进程在执行,线程名称均为thread-1,执行完一个任务再去执行下一个任务,循环往复。
在这里插入图片描述
从整体上看,以上几种创建方式都可以结合业务去调整,都能够满足我们日常开发中的需要,但是我们为什么不建议使用以上几种线程池的创建方式呢?因为Executors 中的线程池对象会产生OOM异常。

穿插问题:Executors 中的线程池对象为什么会产生OOM?

Executors 返回线程池对象的弊端如下:   
FixedThreadPool 和 SingleThreadExecutor :允许请求的队列长度为   Integer.MAX_VALUE,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool :允许创建的线程数量为   Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM

首先我们先明白,为啥会发生OOM,一般在大型企业互联网公司,大促或者秒杀的时候,都会产生大量的请求过来,并发会超级高,任务会越来越多。
····················································································
假如阿里采用newCachedThreadPool()有多少个任务就会创建多少个线程,比如有1千万个请求任务,就会创建1千万个线程,这些线程都会在电脑当中创建,所以CPU就会100%,程序就会卡死不动了。
因为创建的是线程,需要CPU分配最小时间单位去执行线程,所以newCachedThreadPool()并不一定会发生OOM操作,会直接卡死,占用大量CPU。
····················································································

FixedThreadPool()我们上面已经分析过,他是一个可以固定线程数量的线程池,我们设定为10,所以只能执行10个任务,并不会一直新创建线程,剩余的都会放在队列里,又因为其队列大小为Integer的最大值,无限增长队列,因为队列存在于内存当中,通过内存装载数据,直到装满到最大值,产生内存溢出。SingleThreadExecutor与其原理相同。
····················································································
关注公众号【可为编程】回复【面试】领取2023年最新面试题!!!

2.2.4、newScheduledThreadPool(int corePoolSize)

调度型线程池;这个池子里的线程可以按schedule依次delay执行,或周期执行;
ScheduledThreadPool底层采用了DelayedWorkQueue队列,对于他的原理我在这里找到一篇很不错文章,大家可以相互学习一下:
链接: Java 线程池之 DelayedWorkQueue 使用及源码分析.

····················································································
正因为以上这些原因,加上我们前面已经分析过Executors创建线程的弊端,所以我们可以采用自定义的形式来创建线程:

ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10,
                20,
                0L,
                TimeUnit.MICROSECONDS,
                new ArrayBlockingQueue<>(10));

在上面定义的队列采用数组队列的形式,指定容量为10,最大线程数量为20,队列容量为10,但是执行这段代码的时候呢,会报错RejectedExecutionException,在第30个线程处抛出异常,那这是为啥呢?
在这里插入图片描述
从上面图可以看到,当我们的第30个任务来时,容量都爆满,都不能存放,所以会拒绝。
所以要结合业务,进行预估,在预估的基础上再进行二到三倍的扩充,同时还要结合CPU和IO等设备来综合进行判定,这样才能够最大程度上保障资源不浪费和线程池不抛出异常。

 ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10,
                20,
                0L,
                TimeUnit.MICROSECONDS,
                new LinkedBlockingDeque<>(3),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.DiscardOldestPolicy());

同时我们也可以在这里采用LinkedBlockingDeque(10)作为队列,初始值为10,这是一个由链表结构组成的双向阻塞队列,即可以从队列的两端插入和移除元素。双向队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。有兴趣的同学可以去了解一下:
链接:Java并发编程之LinkedBlockingDeque阻塞队列详解
我们还可以为其加上线程工厂Executors.defaultThreadFactory()和设置拒绝策略DiscardOldestPolicy:
ThreadPoolExecutor里面4种拒绝策略(详细)
····················································································
但是我们又有新的发现,那就是在执行的时候,我们看到控制台输出如下:
在这里插入图片描述
为什么在抛出异常后还能执行10个线程呢?不应该最后输出29就好了吗?
那么这里就涉及到两个方面:
一个是提交优先级
一个是执行优先级

思考:什么是执行优先级和提交优先级?
结尾

后面我会单独写一篇文章对以上两个优先级进行总结,今天先到这里,码字太不容易了,希望能够得到大家的支持。

欢迎感兴趣的小伙伴一起探讨学习知识,以上是个人的一些总结分享,如有错误的地方望各位留言指出,十分感谢。

觉得有用的话别忘点赞、收藏、关注,手留余香! 😗 😗 😗

这里是一个真诚的***青年技术交流QQ群:761374713***,不管你是大学生、社畜、想学习变成的其他人员,欢迎大家加入我们,一起成长,一起进步,真诚的欢迎你,不管是技术,还是人生,还是学习方法。有道无术,术亦可求,有术无道,止于术。在这里插入图片描述

欢迎大家关注【可为编程】,成长,进步,编程,技术、掌握更多知识!
在这里插入图片描述

评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

NotNull1

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值