剧前爆米花--爪哇岛寻宝】java多线程案例——单例模式、阻塞队列及生产者消费者模型、定时器、线程池

作者:困了电视剧

专栏:《JavaEE初阶》

文章分布:这是关于java多线程案例的文章,进行了对单例模式、阻塞队列及生产者消费者模型、定时器和线程池的讲解,希望对你有所帮助!

剧前爆米花--爪哇岛寻宝】java多线程案例——单例模式、阻塞队列及生产者消费者模型、定时器、线程池_第1张图片

目录

单例模式

懒汉模式实现

饿汉模式实现

阻塞式队列

标准库中的阻塞式队列

生产者消费者模型

降低耦合 

削峰填谷

低配版阻塞式队列的简单自我实现

定时器

标准库中的定时器

低配版定时器的简单自我实现

线程池

标准库中的线程池

代码实现

创建线程池的参数的意义 

线程池的拒绝策略

低配版线程池的简单自我实现 


单例模式

单例模式是一个非常常见的设计模式。

什么是设计模式?

设计模式好比象棋中的 "棋谱". 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有 一些固定的套路. 按照套路来走局势就不会吃亏. 软件开发中也有很多常见的 "问题场景". 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照 这个套路来实现代码, 也不会吃亏. 

单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例。我们在日常的编程中一定有类似这样的需求,比如我们现在有一个wife类,这个wife类只能实例化一个“wife”,否则就会发生逻辑上的错误,这时我们就需要这个单例模式来约束我们只能创建一个wife对象。单例模式主要分为“饿汉”和“懒汉”两种具体实现方式。

懒汉模式实现

以洗碗举例,懒汉模式就是我中午吃完饭,我留着碗不急着洗,等我到晚上需要再次吃饭的时候,我再去洗,而且我只洗我吃晚饭需要的碗,不需要的碗我仍然不洗,直到我需要为止。

抛开现实的卫生因素,这样做的效率会比我吃完就全洗的效率更高,占用的资源更少,具体到代码中就是,只有当我需要他的实例时我在创建,不需要就不创建。

class Singleton{
    //饿汉模式不调用就不创建实例
    //用static进行修饰,使其成为一个类变量,这样这个类中就只有一个
    private static Singleton instance = null;
    public static Singleton getSingleton(){
        //当这个不为空的时候,此时就是单纯的读操作,不需要再像第一次执行一样考虑线程安全问题
        if (instance == null){
            synchronized (Singleton.class){
                //判断是否为空,为空就进行实例化
                if (instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
    //解决了用方法进行调用的且只有一个需求,现在还需要封印用new来进行实例化的步骤,用private修饰构造方法即可
    private Singleton(){}
}

加锁是因为在多线程环境下,可能会导致不同线程new出来的对象不同所以要加锁。

同时这里的两个判断instance为空的条件虽然写了两次,但不可省略,原因如下:

1.最重要的一点是,初心不同,第一个判断是为了判断是否需要在进行写操作,如果只需要    进行读操作的话则不需要加锁,以提高线程的执行效率。加锁是一个非常低效的操作,所      以,非必要不加锁。第二个判断则是判断是否为空,为空则需要进行实例化,这里两者一      样,只是一种巧合。 

2.虽然这两个判断条件只隔有一个锁,但是在锁竞争的影响下他们之间被执行的间隔可能会     是很长时间,在这段时间里无法保证instance没有被删掉。

饿汉模式实现

饿汉模式就是,我很“饿”,所以我很“勤快”,当我中午饭吃完后我立即就把碗什么的全给洗了,在代码中的实现也是,我一开始就将其进行赋值,用不用,什么时候用不关心。

class Singleton{
    private static Singleton instance = new Singleton();
    public static Singleton getSingleton(){
        return instance;
    }
    private Singleton(){}
}

阻塞式队列

阻塞队列是一种特殊的队列. 也遵守 "先进先出" 的原则。

阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:

1.当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.

2.当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.

阻塞队列的一个典型应用场景就是 "生产者消费者模型". 这是一种非常典型的开发模型。

标准库中的阻塞式队列

剧前爆米花--爪哇岛寻宝】java多线程案例——单例模式、阻塞队列及生产者消费者模型、定时器、线程池_第2张图片

put方法是向队列中加入元素,而take方法则是从队列中拿出元素。 

生产者消费者模型

生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。

生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等 待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取

降低耦合 

举个栗子:比如现在有A,B两个服务器,A是生产者,B是消费者,现在我们不采用这种生产者消费者模型,那A和B就相当于直接进行交互,假如A向B发送一个请求,B给A一个回应,如果此时A崩了,那B也就没什么用了,即也就跟着崩了,这就是强耦合。

剧前爆米花--爪哇岛寻宝】java多线程案例——单例模式、阻塞队列及生产者消费者模型、定时器、线程池_第3张图片

而如果此时我们在中间加一个阻塞队列服务器,即采用生产者消费者模型。

剧前爆米花--爪哇岛寻宝】java多线程案例——单例模式、阻塞队列及生产者消费者模型、定时器、线程池_第4张图片

这样A和B两个服务器只认识这个阻塞队列服务器,并不认识对方,此时,如果A崩了,那B并不会受到太大的影响。这就大大降低了两个服务器之间的耦合程度,提高了稳定性。

削峰填谷

这个模型还有一个巨大的作用就是削峰填谷。

现在依旧是A,B两个服务器,假设A服务器是一个与网购相关的服务器,在某一时刻有一个促销活动,这个活动的出现使A服务器发送的请求在短时间内大大增加,这是就对需要对其进行回应的B服务器造成了极大的负担,很可能会导致服务器的的崩溃!

这时如果我们使用生产着消费者模型,那爆炸式增长的请求就会直接进入到阻塞队列服务器,而B服务器就会按照“自己”的速度一一进行回应,“峰”是这样,“谷”也是如此。

注:由于阻塞队列服务器的业务较少,所以相比于业务很多的A和B服务器,要稳定很多。

低配版阻塞式队列的简单自我实现

public class MyBlockingQueue {
    //循环队列但是加上了等待阻塞
    private int[] items = new int[1000];
    volatile private int size = 0;
    volatile private int head = 0;
    volatile private int tail = 0;
    
    synchronized public void put(int elem) throws InterruptedException {
        if (size == items.length){
            this.wait();
        }
        items[tail] = elem;
        tail++;
        size++;
        if (tail == items.length){
            tail = 0;
        }
        //此时阻塞队列已经put出了一个元素,也就是说现在一定不满,此时进行阻塞等待的就可以被唤醒了
        this.notify();
    }
    
    synchronized public int take() throws InterruptedException {
        if (size == 0){
            this.wait();
        }
        int ret = items[head];
        head++;
        size--;
        if (head == items.length){
            head = 0;
        }
        this.notify();
        return ret;
    }
}

 简易版的实现,希望对你有所启发。

定时器

标准库中提供了一个 Timer 类.。

Timer 类的核心方法为 schedule . schedule 包含两个参数. 第一个参数指定即将要执行的任务代码, 第二个参数指定多长时间之后 执行 (单位为毫秒).

标准库中的定时器

剧前爆米花--爪哇岛寻宝】java多线程案例——单例模式、阻塞队列及生产者消费者模型、定时器、线程池_第5张图片

TimerTask是一个实现了Runnable接口的抽象类,但由于不是函数式接口所以不能使用Lambda表达式进行书写。

剧前爆米花--爪哇岛寻宝】java多线程案例——单例模式、阻塞队列及生产者消费者模型、定时器、线程池_第6张图片

三秒后在控制台中打出了Hello。

低配版定时器的简单自我实现

class MyTask implements Comparable {
    public Runnable runnable;
    public long time;
    public MyTask(Runnable runnable,long delay){
        this.runnable = runnable;
        //这里的time是时间戳,所以需要先找到目前的时间戳然后加上需要延时的时间
        this.time = System.currentTimeMillis() + delay;
    }

    //因为要放入带有优先级的阻塞队列中所以我们要指定一个比较规则,实现comparable接口
    @Override
    public int compareTo(MyTask o) {
        return (int)(this.time - o.time);
    }
}

public class MyTimer {
    //这个结构,带有优先级的阻塞队列,核心数据结构
    private PriorityBlockingQueue queue = new PriorityBlockingQueue<>();

    private Object locker = new Object();

    //这个“时间表”方法,就是将我们的任务放到队列中
    public void shedule(Runnable runnable,long delay){
        MyTask myTask = new MyTask(runnable,delay);
        queue.put(myTask);
        locker.notify();
    }

    //这个构造方法是执行我放到“时间表”中的任务,当我实例化开始就开始计时
    public MyTimer(){
        Thread t = new Thread(() -> {
            while (true){
                try {
                    synchronized (locker){
                        MyTask myTask = queue.take();
                        long curTime = System.currentTimeMillis();
                        if (myTask.time <= curTime){
                            myTask.runnable.run();
                        }else{
                            queue.put(myTask);
                            locker.wait(myTask.time - curTime);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        t.start();
    }
}

相关代码已用注释标记出了意思,如有不清楚的地方还请评论区讨论~~ 

线程池

在CPU内部,每次需要使用一个线程都需要重新创建并且销毁,这每次的创建销毁都需要消耗资源,那能不能这样做,我一次创建一定数量的线程,并把它们放到一个名字叫“线程池”的容器中,这样,每当我们在需要用线程的时候就不需要再重新创建了,并且当这个线程的run方法执行完毕后就不需要在销毁这个线程,而是直接将这个线程再放进线程池当中以便下次使用,这样就又节省了销毁线程的资源。

举个栗子:

我现在开了一个小卖部,我每次需要去送外卖,不用线程池就是每次需要送外卖的时候都重新雇一个人,然后他送完外卖后我立刻将他解雇。

而线程池就是,你一次雇几个人,然后这几个人去送外卖,他们送完后也不解雇等着下次外卖到来。这样就节约了很多的资源。

标准库中的线程池

代码实现

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadDemo3 {
    public static void main(String[] args) {
        //通过Executors中的静态方法来进行线程池的创建,现在这个线程池中有十个线程
        ExecutorService pool = Executors.newFixedThreadPool(10);
        //我们可以通过submit方法来注册一个任务到线程池当中
        for ( int i=0;i<1000;i++ ){
            int num = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello" + num);
                }
            });
        }
    }
}

这里我们不能通过直接new来创建对象,必须要通过调用静态方法才行,这时一个叫做工厂模式的设计模式,在我们日常的一些需求中,仅仅使用构造方法的重载无法满足,有时候就需要工厂模式来帮助我们,举个栗子:

创建一个point对象,我需要描述他的坐标,假设是二维平面,我们可以通过x和y来进行描述,这时我们构造方法传两个参数就行了,但是用极坐标的ρ和角度也可以进行创建,这时候我们就发现这两种构造方法传的参数类型和数量都是一样的,无法区分开,于是工厂模式应运而生。 

Executors 创建线程池的几种方式

newFixedThreadPool: 创建固定线程数的线程池

newCachedThreadPool: 创建线程数目动态增长的线程池.

newSingleThreadExecutor: 创建只包含单个线程的线程池.

newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.

Executors 本质上是 ThreadPoolExecutor 类的封装.

这段代码的执行结果为:

剧前爆米花--爪哇岛寻宝】java多线程案例——单例模式、阻塞队列及生产者消费者模型、定时器、线程池_第7张图片

十个线程会将这些任务执行完,随机没有顺序。

创建线程池的参数的意义 

 corePoolSize是核心线程数

maxmumPoolSize是最大线程数

当目前任务数量较多时,线程池会多创建一些临时线程。

核心线程就相当于“正式员工”,而这些临时线程就相当于“实习生”,当任务变少时,线程池就会考虑将这些“实习生”辞退,即销毁这些临时线程,但核心线程则不会受影响会一直保留。

long keepAliveTime是临时线程保持存活的时间

TimeUnit unit是时间的单位

当任务较少时,这些临时线程并不会立即被销毁,而是暂时存活一段时间,因为线程池不确定过一会会不会又有大量的任务加进来,所以会保留一段时间来节约资源。 

workQueue是阻塞队列

threadFactory是工厂模式

线程池要管理很多个任务,这些任务的底层就是通过阻塞队列来管理的,程序员可以手动指定一个阻塞队列,这样就可以清楚地知道队列中的信息,submit方法就是将任务放到这个阻塞队列中。

工厂模式就是创建辅助线程的类 

handler是线程池的拒绝策略。

线程池的拒绝策略

我们可以将任务添加到线程池当中来提高我们程序运行的效率,但是线程池的承载能力毕竟是有限的,当我们向其中添加的任务过多时,线程池就会拒绝任务的添加,标准库提供了四种拒绝策略。

剧前爆米花--爪哇岛寻宝】java多线程案例——单例模式、阻塞队列及生产者消费者模型、定时器、线程池_第8张图片

1.如果满了还继续加任务,那添加操作就会直接抛出异常,新任务老任务都无法执行。

2.添加的线程自己负责执行这个任务,即劳资不仅不干,劳资还要怼回去。

3.丢弃最老的任务,将新的任务添加。

4.丢去最新的任务,即不理这个新任务,该咋咋还咋咋。

低配版线程池的简单自我实现 

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingDeque;

public class MyThreadPool {
    //用阻塞队列来存放任务
    private BlockingQueue queue = new LinkedBlockingDeque<>();

    public void submit(Runnable runnable) throws InterruptedException {
        queue.put(runnable);
    }

    public MyThreadPool(int n){
        //创建n个线程
        for ( int i=0;i{
                //每个线程内部做的任务是,只要队列中有任务我就抢着执行
                try {
                    while (true){
                        Runnable runnable = queue.take();
                        runnable.run();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            t.start();
        }
    }
}

相关代码注释已给出。

以上就是本篇博客的全部内容,如有疏漏还请指正!

你可能感兴趣的:(JavaEE初阶,单例模式,java,开发语言,线程池,阻塞式队列)