多线程---阻塞队列收尾和线程池

(一).阻塞队列收尾

在上一章的文章中,介绍到了阻塞队列,最后模拟实现了一个阻塞队列

java 复制代码
package Thread;

class MyBlockingQueue{
    private String[] array=null;

    //队头
    private int head;
    //队尾
    private int tail;
    //元素个数  用于判断阻塞队列是否满了
    private int size=0;

    public MyBlockingQueue(int capacity){
        array=new String[capacity];
    }

    //put()方法
    public void put(String str){
        synchronized (this){ //引入线程安全,加锁
            //判断队列满了情况
            if (size>=array.length){
                return;
            }
        }

        array[tail]=str;
        tail++;
        size++;
        //重置,构成循环
        if (tail>=array.length){
            tail=0;
        }
    }

    //take()方法
    public String take(){
        synchronized (this){ //引入线程安全,加锁
            //判断队列为空的情况
            if (size==0){
                return null;
            }
        }
        String str=array[head];
        head++;
        size--;
        if (head==array.length){
            head=0;
        }
        return str;
    }
}

public class demo29 {

    public static void main(String[] args) {

    }
}

但是这个阻塞队列还没有写完,我们并没有实现"阻塞"的效果

对于"阻塞"效果,我们可以想一下。在进行put()的时候,应该什么时候进行阻塞?是不是应该阻塞队列满了的时候进行阻塞,当队列满了的时候,再往队列中插入数据,此时插不进去了,所以要进行阻塞。在进行take()的时候,应该什么时候进行阻塞?是不是应该在阻塞队列为空的时候进行阻塞,当队列为空的时候,再往队列里取数据,此时取不出来了,所以要进行阻塞

所以,我们要在take()和put()方法的if()语句中使用wait()方法,同时wait()要搭配"锁"进行操作

但是只有阻塞还不够,我们要对wait()进行唤醒。当队列满了的时候,此时再往里插入数据,发现插不进去了,所以阻塞等待,wait()在等待的过程中会释放锁,那么这个时候,我进行了一次take()操作,那么此时阻塞队列中又可以进行插入数据了。所以总结来说,我们要在put()和take()方法中使用wait()方法,然后在put()方法中使用notify()方法,来唤醒take()中的wait()方法,在take()方法中使用notify()关键字来唤醒put()中的wait()方法

其实这也不难理解,put()中的notify()是唤醒take()中的wait()方法,原因是只有当队列不为空的时候才能被唤醒,所以只有当put()入队列成功的时候才是队列不为空的情况

take()中的notify()是唤醒put()中的wait()方法,原因是只有当队列不满的时候才能被唤醒,所以当take()出队列成功的时候才是队列不为空的情况

java 复制代码
package Thread;

class MyBlockingqueue{
    String[] array=null;

    private int head=0;
    private int tail=0;
    private int size=0;

    public MyBlockingqueue(int capacity){
        array=new String[capacity];
    }

    public void put(String str)  {
        synchronized (this){
            //判断队列是否满了
            if (size==array.length){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            array[tail]=str;
            tail++;
            size++;
            if (tail>=array.length){
                tail=0;
            }
            this.notify();
        }
    }

    public String take() {
        synchronized (this){
            //判断队列是否为空
            if (size==0){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            String ret=array[head];
            head++;
            size--;
            if (head>=array.length){
                head=0;
            }
            this.notify();
            return ret;
        }
    }

}


public class demo30 {
    public static void main(String[] args) {

    }
}

此时,基本的阻塞队列已经完成了,但是这里还有一个问题

java 复制代码
            if (size==array.length){
                this.wait();
            }

这里我们用的是一个if()判断,当我们点开wait()的原码的时候

我们发现,编译器建议我们用while()循环,这是为什么?

这是因为,wait()方法除了能被notify()方法给唤醒,还可能被interrupt()方法在中断的时候将wait()方法给唤醒,如果被interrupt()唤醒,那么当处理异常的时候继续往下走,然后执行tail++等操作,但是一旦执行,那么程序就出问题了,因为tail就越界了,所以搭配while()循环进行使用,目的是为了"二次验证",判断一下当前条件是否成了,wait()之前判断一次,wait()唤醒之后再判断一次

java 复制代码
package Thread;

class MyBlockingqueue{
    String[] array=null;

    private int head=0;
    private int tail=0;
    private int size=0;

    public MyBlockingqueue(int capacity){
        array=new String[capacity];
    }

    public void put(String str)  {
        synchronized (this){
            //判断队列是否满了
            while (size==array.length){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            array[tail]=str;
            tail++;
            size++;
            if (tail>=array.length){
                tail=0;
            }
            this.notify();
        }
    }

    public String take() {
        synchronized (this){
            //判断队列是否为空
            while (size==0){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            String ret=array[head];
            head++;
            size--;
            if (head>=array.length){
                head=0;
            }
            this.notify();
            return ret;
        }
    }

}


public class demo30 {
    public static void main(String[] args) {

    }
}

注意:如果3个线程都在put()中阻塞了,此时线程4 take()了一下,然后随机唤醒了线程1,那么线程1继续往下执行,当线程1在put()方法中执行notify()的时候,可能会唤醒线程2或者线程3(小概率)

(二).线程池

1.概念

线程池可以理解为把线程提前创建好,放到一个池子(类似于数组)中,用的时候随时去取,用完了之后放回到池子中。

线程池是为了让我们能够高效的创建和销毁线程。随着互联网的发现,我们对性能需求更进一步,我们觉得频繁的创建销毁线程的开销有些不能接收,所以此时就会有两个方案:①.线程池②.协程(轻量化线程) ②目前在java圈子里还没有普遍使用,所以这里只介绍线程池

2.为什么从池子里直接取线程会比直接创建线程的开销更大?

想要了解为什么,我们需要先了解 操作系统中的 "用户态" 和 "内核态"

一个操作系统 = 内核 + 配套的应用程序

内核的作用就是 "管理硬件设备和给软件提供稳定的运行环境",一个操作系统中内核就是一份,一份内核,要给所有的应用程序提供服务支持

从线程池中取现成的线程是纯应用程序代码就可以完成,所以是一个可控的;从操作系统创建线程,就需要挫折系统内核配合完成,所以是一个不可控的。

使用线程池,就可以省下应用程序切换到"内核"中运行的这样的开销

3.线程池构造的介绍

(1).引入

Java 标准库中也提供了直接使用的线程池 ThreadPoolExcutor

其中,核心方法为submit(Runnable runnable)方法,通过runnable描述一段要执行的任务,通过submit()方法将任务放到线程池中,此时线程池里的线程就会执行这样的任务

(2).参数介绍

有四个构造方法,最后一个构造方法的参数最多,所以直接介绍最后一个构造方法

java 复制代码
//        ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(
//        int corePoolSize,
//        int maximumPoolSize,
//        long keepAliveTime,
//        TimeUnit unit,
//        BlockingQueue<Runnable> workQueue,
//        ThreadFactory threadFactory,
//        RejectedExecutionHandler handler);

corePoolSize:"核心线程数",表示线程池中最少有多少个线程,线程池一旦创建,这些线程也就随之创建了,直到这个线程池销毁,这些线程也就随之销毁了

maximumPoolSize:"最大线程数","核心线程"+"非核心线程",对于非核心线程,不繁忙就销毁了,繁忙之后再创建

keepAliveTime:"非核心线程允许空闲的最大时间"

unit:"时间单位",枚举类型 second 表示 "秒"

workQueue:"工作队列",对于我们来说,可以自由的选择数组/链表,指定capacity,是否需要带优先级/比较规则 的工作队列,线程池本质上也是 "生产者消费者模型",调用submit()就是生产任务,线程池里的线程就是在消费任务

当然Java开发文档也提供了一个默认的工作队列**"defaultThreadFactory()"**

threadFactory:"工厂模式",工厂模式主要用于弥补构造方法的缺陷,和"单例模式"是并列的关系,也是一种设计模式。

工厂设计模式主要是通过静态方法,把构造对象new的过程以及各种属性的初始化的过程封装起来了,后续如果使用到了这里的对象,则直接通过静态方法的调用,就可以获取到了

示例:在高中的时候我们学过求一个点的横坐标和纵坐标

方法1:直接写出x和y

方法2:通过极坐标,从坐标原点O到所求的点连一条直线r,然后知道了r和x轴的夹角@,那么x=r*cos(@) ,y=r*sin(@)

针对上图中的情况,构造方法名字是固定的,想要提供不同的版本就需要通过重载,但是有的时候还不一定能过能够构成重载,那么这个时候"工厂模式"这个设计模式就可以解决该问题

hander:"拒绝策略"

对于"阻塞队列"来说,submit()把任务添加到任务队列中,如果队列满了,再添加,那么就会发生阻塞,但是对于"线程池"来说,发现入队列操作时,队列满了,不会真的触发"入队列",不会真阻塞,而是执行 "拒绝策略"相关的任务

"拒绝策略"有四种形式

①.AboutPolicy:线程池直接抛出异常,会导致线程池可能无法继续工作

②.CallerRunsPolicy:让调用submit()的线程自行执行任务

③.DiscardOldestPolicy:丢弃队列中最老的任务

④.DiscardPolicy:丢弃最新的任务,即当前的submit()的这个任务

4.线程池的进一步封装

对于ThreadPoolExcutor,用户觉得需要传的参数太多了,所以Java标准库针对ThreadPoolExcutor进行了进一步封装,简化了线程的使用,同时也是基于工厂设计模式
Executors

①.通过 newFixedThreadPool

②.通过 newCachedThreadPool

注意:在使用线程池的时候,建议使用ThreadPoolExcutor这个版本,不应该使用Executors,因为Executors的线程数目/拒绝策略等信息都是隐式的,可能会不好控制

5.模拟实现线程池

在模拟实现线程池的时候,我们首先要创建出指定数量的线程,所以说在构造方法中,就需要把线程创建出来。同时要注意,随时都会有新的任务被添加进去,所以说线程就需要持续不断的尝试读取新的任务,如果线程取到了任务,那么就执行,如果没取到,就阻塞等待

java 复制代码
package Thread;


import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class MyThreadPool{
    public BlockingQueue<Runnable> blockingQueue=null;

    //初始化线程
    public MyThreadPool(int n){
        //初始化任务队列
        blockingQueue=new LinkedBlockingQueue<>();

        //创建n个线程
        for (int i = 0; i < n; i++) {  //只负责创建出n个线程
            Thread thread=new Thread(()->{  //对应的线程执行while(true)
                while (true){
                    try {
                        //从阻塞队列中取任务
                        Runnable runnable=blockingQueue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });

            thread.start();
        }
    }
    public void submit(Runnable runnable) throws InterruptedException {
        //将任务放到阻塞队列中
        blockingQueue.put(runnable);
    }
}

public class demo33 {

    public static void main(String[] args) throws InterruptedException {

        MyThreadPool myThreadPool=new MyThreadPool(4);//创建4个线程
        for (int i = 0; i < 100; i++) {
            int id=i;
            myThreadPool.submit(()->{ //重写run方法
                System.out.println("hello"+" "+id+" "+Thread.currentThread().getName());
            });
        }
    }
}

6.shutdown()方法

可以看到,不管是我们自己模拟实现的线程池还是Java开发文档自带的线程池,当把所有的任务都处理完之后,程序并没有结束,这是因为,这些线程一直还在阻塞等待,等待传入新的任务。

如果想要将线程池里面的线程全部关掉,那么就可以使用**shutdown()**方法

注意:shutdown()方法虽然能将所有的线程关掉,但是不能保证线程池里面的所有任务一定能全部能执行完毕。所以如果需要等待线程池里面的所有任务都执行完毕,需要调用awaitTermination()方法

相关推荐
_深海凉_2 小时前
LeetCode热题100-最长公共前缀
算法·leetcode·职场和发展
郝学胜-神的一滴2 小时前
PyTorch自动微分核心解析:从原理到实战实现权重更新
人工智能·pytorch·python·深度学习·算法·机器学习
大尚来也2 小时前
红黑树与AVL树:平衡二叉搜索树的博弈与抉择
开发语言
鱼鳞_2 小时前
Java学习笔记_Day22
java·笔记·学习
会编程的土豆2 小时前
【数据结构与算法】 拓扑排序
数据结构·c++·算法
今天又是充满希望的一天3 小时前
C++分布式系统知识
开发语言·c++
zth4130213 小时前
SegmentSplay‘s Super STL(v2.2)
开发语言·c++·算法
__土块__3 小时前
一次电商秒杀系统架构评审:从本地锁到分布式锁的演进与取舍
java·redis·高并发·分布式锁·redisson·架构设计·秒杀系统
她说..3 小时前
Java 注解核心面试题
java·spring boot·spring·spring cloud·自定义注解