单例模式 + 阻塞队列 + 定时器 + 线程池

在这里插入图片描述

写在前面:
博主主页:戳一戳,欢迎大佬指点!
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个小菜鸟嘿嘿
-----------------------------谢谢你这么帅气美丽还给我点赞!比个心-----------------------------

在这里插入图片描述


多线程

  • 五,单例模式
    • 5.1,饿汉模式
    • 5.2,懒汉模式
    • 5.3,单例模式的线程安全问题
  • 六,阻塞队列
    • 6.1,认识阻塞队列
    • 6.2,代码实现
      • 6.2.1,标准库阻塞队列
      • 6.2.2,自己实现阻塞队列
  • 七,定时器
    • 7.1,标准库中的定时器
    • 7.2,自己实现定时器
  • 八,线程池
    • 8.1,标准库中的线程池
    • 8.2,自己实现线程池
    • 8.3,线程池构造方法


五,单例模式

单例模式是常见的设计模式之一,所谓设计模式就是大佬根据常用的需求场景而整理出来的一套类似于模板的解决问题的方法。利用设计模式,我们写代码就可以有了参照,按部就班的处理一些特定的业务场景。


单例模式能够保证某个类只能创建出一个实例,不能有多个实例,只能创建一个实例这是业务场景决定的。单例模式的实现有饿汉模式与懒汉模式两种。

单例的核心我们是用static修饰,将某个实例作为某个类的类属性。因为一个类只会加载一次,对应的.class文件也会只有一份,类属性说是属于类,其实更准确的可以认为是属于类对象,类对象来自于.class文件,那么自然类对象只会有一个,所以属于类对象的属性也会只有一份。


5.1,饿汉模式

饿汉模式,在类加载的时候就创建了实例,创建时机很早。

class Singleton{
    //这个作为一个单例类,只能有一个实例
    private static Singleton instance = new Singleton();//这个就是那个唯一实例,直接赋值好

    public static Singleton getInstance() {//提供一个静态的get方法让类外能够拿到这个实例,必须是静态的,不然类外不能new对象拿不到方法
        return instance;
    }

    private Singleton(){//将构造方法私有化,使得在类外不能new实例

    }
}
public class Demo1 {
    public static void main(String[] args) {
        Singleton instance = Singleton.getInstance();//调用方法拿到这个唯一实例
    }
}

注意,我们这里是不考虑反射的问题的,确实这里也防不了反射,因为反射机制是Java的一种特殊机制,大部分情况下不会用到,如果想要防止,也需要使用一些特殊手段,比如改造单例类的构造方法,利用信号量等等。另外枚举实现单例类是最安全的。


5.2,懒汉模式

懒汉模式,创建实例的时机更晚,效率也会更高一些。

class SingletonLazy{
    //懒汉模式实现单例模式
    private static SingletonLazy instance = null;//这里没有马上new对象

    public static SingletonLazy getInstance() {
        if(instance == null){
            instance = new SingletonLazy();
        }
        return instance;
    }

    private SingletonLazy(){};
}
public class Demo2 {
    public static void main(String[] args) {
        SingletonLazy instance = SingletonLazy.getInstance();
    }
}

对于上面为什么说效率会更高一些,那是因为懒汉模式实例的创建并不是一开始在类加载的时候就创建好了,它是你什么时候用,第一次调用了getInstance方法的时候才会创建,如果一直没有人调用,那么创建对象的过程也就省了,就算有人调用,那时机也会更晚一些,刚开始类加载的时期系统要初始化的东西很多,资源比较紧张,那么晚一点new对象刚好就可以错开前面的加载的高峰期。


5.3,单例模式的线程安全问题

上面说了两种实现单例模式的方式,那么放到多线程的环境下,是否能保证线程安全呢?

对于饿汉模式而言,对于那个实例对象,只是单纯的读取返回操作,所以显然是线程安全的。但是懒汉模式里面对于实例是有修改,比较,赋值等操作的,这个时候多线程下就会出现线程安全的问题了。如下图:

单例模式 + 阻塞队列 + 定时器 + 线程池_第1张图片


class SingletonLazy{
    //懒汉模式实现单例模式
    private static SingletonLazy instance = null;//这里没有马上new对象

    public static SingletonLazy getInstance() {
        synchronized (SingletonLazy.class){//锁对象是类对象
            if(instance == null){
                instance = new SingletonLazy();
            }
        }
        return instance;
    }

    private SingletonLazy(){};
}
public class Demo2 {
    public static void main(String[] args) {
        SingletonLazy instance = SingletonLazy.getInstance();
    }
}

但是,上述代码解决了线程不安全的问题,却又引入了新的问题。其实在第一次锁竞争将实例创建好了之后,相当于就又变成了饿汉模式了,后续其实就已经线程安全了,但是我们还是会一次次的加锁,解锁,这个过程其实也挺消耗时间资源的,所以也就不能这么无脑的加锁,解决的办法就是再套一层if,在加锁前先判断一下实例是不是已经创建好了。

class SingletonLazy{
    //懒汉模式实现单例模式
    private static SingletonLazy instance = null;//这里没有马上new对象

    public static SingletonLazy getInstance() {
        if(instance == null){
            synchronized (SingletonLazy.class){//锁对象是类对象
                if(instance == null){
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }

    private SingletonLazy(){};
}

这段代码重点需要理解的就是两层if的各自的意思需要理解清楚。

单例模式 + 阻塞队列 + 定时器 + 线程池_第2张图片


到这里,还没完,还有问题,涉及到指令重排序。

单例模式 + 阻塞队列 + 定时器 + 线程池_第3张图片


那么如何解决呢,还是利用volatile修饰这个实例对象的引用,就会禁止指令重排序了。

private volatile static SingletonLazy instance = null;

六,阻塞队列

6.1,认识阻塞队列

对于队列而言,我们之前学过的都是先进先出的队列,那有不有队列不是先进先出的呢?答案是有的,比如这里扩展一个消息队列,消息队列中的每一个元素都会引入一个业务类型,这些元素在入队的时候没啥区别,但是出队的时候会指定某个类型的元素先出,并不是按照先进先出的规则。

假设银行有多个柜台,每个柜台上处理的业务不一样,然后这个时候有很多客户在排队等待办理业务,有的是存钱,有的是办理信用卡等等,虽然可能有的客户排在后面,比如办理信用卡的,但是恰巧这个时候办理信用卡的柜台没人了,然乎柜台人员就说办理信用卡的来一个,那么这个时候即使这个客户排在后面,他也可以很快去进行办理。

上面这种情况就是消息队列,在日常开发中我们会把消息队列这样的数据结构单独实现为一个程序并且部署在服务器上,这样就成为了一个消息队列服务器,也就是我们所说的MQ(常用的中间件)。【中间件就是一类通用服务器的统称】


阻塞队列:先进先出,但是是线程安全的,带有阻塞功能(队列满了之后,入队会阻塞,队列空了,出队会阻塞)。

阻塞队列常用的应用场景就是生产消费者模型,描述的是多线程协同工作的模式。

阻塞队列的优点:

1,削峰,平衡生产者与消费者的处理能力,相当于一个缓冲区。

比如双十一,客户端像服务器发送大量的订单支付请求,这个时候流量突然的骤增,我们的服务器可能会处理不过来,所以使用阻塞队列,先将请求放入阻塞队列里面,然后服务器这边按照正常的处理速度进行处理,这样就可以减缓一下服务器的压力,不至于出现宕机的情况。

2,解耦合,降低生产者与消费者的关联关系。

如果是两个服务器之间直接交流,那么自然避免不了互相的影响,但是使用阻塞队列之后,交互请求就直接放到阻塞队列里面了,服务器之间只关心做好自己的本职工作就好了,也不用担心可能其中一个服务器出现问题而直接影响到了另外的服务器。


6.2,代码实现

6.2.1,标准库阻塞队列

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

public class Demo6 {
    public static void main(String[] args) throws InterruptedException {
        BlockingQueue<Integer> queue = new LinkedBlockingQeque<>(100);//BlockingQueue<>这是一个接口,具体实现类很多

        Thread customer = new Thread(()->{
            while (true){
                try {
                    int ret = queue.take();//带有阻塞功能的出队元素方法
                    System.out.println("消费元素 " + ret);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        Thread producer = new Thread(()->{
            while (true){
                try {
                    queue.put(1);//带有阻塞功能的入队元素方法
                    System.out.println("生产元素 " + 1);
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        producer.start();
        customer.start();

        producer.join();
        customer.join();
    }
}

单例模式 + 阻塞队列 + 定时器 + 线程池_第4张图片


注意,阻塞队列是特殊的队列,所以基本队列的方法也还是有的,比如offer(),poll(),但是它们都是不带有阻塞功能的,所以在这里说实话也没有什么意义。另外,我们的阻塞队列你想要查看队首元素只能是使用take()把它拿出来,这里是没有具有阻塞功能的类似于peek()的方法。


6.2.2,自己实现阻塞队列

实现一个普通队列 + 线程安全 + 阻塞功能

import java.lang.reflect.Array;
import java.util.Arrays;

//首先自己实现一个普通队列 + 线程安全 + 阻塞实现
class MyBlockingQueue<T> {
    private T[] elem;//定义一个泛型数组
    public MyBlockingQueue(Class<T> clazz,int capacity){
        elem = (T[]) Array.newInstance(clazz,capacity);
    }

    volatile int head = 0;//头指针
    volatile int tail = 0;//尾指针
    volatile int size = 0;//记录有效元素个数
    //入队函数
    public void put(T val)  {
        synchronized (this){
            //首先判断是否满
            while (size == elem.length){
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //没有满就把元素放进去
            elem[tail] = val;
            tail++;
            if(tail >= elem.length){
                tail = 0;//因为是循环数组,所以转到头去
            }
            size++;
            this.notify();
        }
    }

    //出队函数
    public T take()  { // 出队操作与入队操作相互唤醒,两个不可能同时阻塞,因为不可能同一时间队列是空的也是满的
        synchronized (this){
            while (size == 0){//队列为空就不能出元素
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            T ret = elem[head];
            head++;
            if(head >= elem.length){
                head = 0;
            }
            size--;
            this.notify();
            return ret;
        }
    }
}
public class Demo7 {
    public static void main(String[] args) throws InterruptedException {
        MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(Integer.class,100);

        Thread customer = new Thread(()->{
            while (true){
                int ret = queue.take();
                System.out.println("消费元素 " + ret);
            }
        });

        Thread producer = new Thread(()->{
            while (true){
                queue.put(1);
                System.out.println("生产元素 " + 1);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        producer.start();
        customer.start();

        producer.join();
        customer.join();
    }
}

单例模式 + 阻塞队列 + 定时器 + 线程池_第5张图片


单例模式 + 阻塞队列 + 定时器 + 线程池_第6张图片


七,定时器

7.1,标准库中的定时器

首先,定时器大家就可以理解成一个闹钟,它可以设定是在指定时间之后执行某一个任务。

import java.util.Timer;
import java.util.TimerTask;

public class Demo9 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到1");
            }
        },3000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("时间到2");
            }
        },5000);

    }
}

标准库中的定时器类叫做Timer,它的核心在于schedule()方法,schedule(TimerTask task, long delay),前一个参数代表我们在指定时间需要做的任务,后一个参数是多长时间后执行(单位是毫秒)。TimerTask是一个抽象类,实现了Runnable接口,相当于对Runnable接口进行了再一次的封装,我们在传参的时直接new一个匿名的内部类,然后重写run方法就好了。同一个定时器对象可以安排多个任务。

在这里插入图片描述

在执行完任务之后,进程没有结束,因为我们定时器的内部会创建线程执行任务,这些线程都是前台线程,所以会影响进程的结束。


7.2,自己实现定时器

1,首先设计定时器类和任务类 2,使用带有优先级的阻塞队列统筹任务 3,创建一个扫描线程,不断的来扫描我们的队首元素,看是否到了执行时间

问题:1,任务入队列的比较问题 2,忙等问题 3,原子性问题,空打一炮

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.PriorityBlockingQueue;

class MyTimerTask implements Comparable<MyTimerTask>{//这是一个描述任务的类,要实现Comparable接口,因为优先级阻塞队列中的元素会进行比较
    //要执行的任务
    private Runnable runnable;
    //定时时间 是一个时间戳
    private long executeTime;

    public MyTimerTask(Runnable runnable, long executeTime) {
        this.runnable = runnable;
        this.executeTime = System.currentTimeMillis() + executeTime;//获取到系统时间,将其转化成一个时间戳值
    }

    public long getExecuteTime() {
        return executeTime;
    }

    public Runnable getRunnable() {
        return runnable;
    }

    @Override
    public int compareTo(MyTimerTask o) {
        return (int)(this.executeTime - o.executeTime);//创建的是小根堆,this代表的是你要加入队列的任务对象
    }
}
class MyTimer {//这是一个定时器类
    public BlockingQueue<MyTimerTask> queue = new PriorityBlockingQueue<>();//带有优先级的阻塞队列,管理任务
    public Object locker = new Object();//锁对象
    public MyTimer() {
        //创建一个扫描线程
        Thread t = new Thread(()->{
            while (true){
                //循环去取队首元素进行判断
                try {
                    synchronized (locker){
                        MyTimerTask task = queue.take();
                        //取出之后进行判断
                        long curTime = System.currentTimeMillis();//获取当前时间
                        if(curTime >= task.getExecuteTime()){
                            //如果当前系统时间的时间戳大于你设定的时间的时间戳就说明可以开始执行了
                            task.getRunnable().run();
                        }else{
                            //如果还没到时间,就把任务又塞回去
                            queue.put(task);
                            locker.wait(task.getExecuteTime() - curTime);//没到时间就wait让出cpu资源
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //开启线程
        t.start();
    }

    public void schedule(Runnable runnable, long delay) throws InterruptedException {
        MyTimerTask myTimerTask = new MyTimerTask(runnable,delay);
        queue.put(myTimerTask);
        synchronized (locker){
            locker.notify();//唤醒wait
        }

    }
}
public class Demo10 {
    public static void main(String[] args) throws InterruptedException {
        MyTimer myTimer = new MyTimer();
        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                //这是任务本体
                System.out.println("时间到1,开始执行任务!");
            }
        },3000);

        myTimer.schedule(new Runnable() {
            @Override
            public void run() {
                //这是任务本体
                System.out.println("时间到2,开始执行任务!");
            }
        },5000);
    }
}


【忙等问题:】

单例模式 + 阻塞队列 + 定时器 + 线程池_第7张图片


【原子性问题,notify空打一炮:】

但是呢,如果如上面加锁,会有原子性的问题。

单例模式 + 阻塞队列 + 定时器 + 线程池_第8张图片


【死锁问题:】

单例模式 + 阻塞队列 + 定时器 + 线程池_第9张图片


八,线程池

说到池,可能大家会想起字符串常量池,以及数据库连接池,它们的作用都是为了实现复用,从而提高效率。那么这里的线程池也是一样的作用,因为虽说线程的创建销毁的开销都比较小,但是当我们十分频繁的创建销毁,其实效率也会比较低,那么为了提高效率也就有了线程池。线程池的作用就是创建一个池子,然后里面放很多已经创建好的线程,等到需要执行任务的时候,就可以直接用这些现成的线程而不用重新创建,用完了也不会销毁线程而是放回线程池。

单例模式 + 阻塞队列 + 定时器 + 线程池_第10张图片


8.1,标准库中的线程池

public class Demo11 {
    public static void main(String[] args) {
        ExecutorService pool = Executors.newCachedThreadPool();
        pool.submit(new Runnable() {//把任务提交给线程池
            @Override
            public void run() {
                System.out.println("这是一个任务!");
            }
        });

    }
}

单例模式 + 阻塞队列 + 定时器 + 线程池_第11张图片


8.2,自己实现线程池

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


class MyThreadPool{
    private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();//阻塞队列里面放任务

    public void submit(Runnable runnable) throws InterruptedException {//这是提交任务,把任务放入阻塞队列
        queue.put(runnable);
    }

    public MyThreadPool(int m){//构造方法,创建m个线程来执行任务
        for (int i = 0;i < m;i++){
            Thread t = new Thread(()->{
                //开启线程去执行任务
                while (true){//这个线程开启之后你要让它循环的去取任务,执行任务
                    try {
                        Runnable runnable = queue.take();
                        runnable.run();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            t.start();//这些线程都是前台线程,不会立即结束
        }
    }

}
public class Demo12 {
    public static void main(String[] args) throws InterruptedException {
        MyThreadPool pool = new MyThreadPool(20);
        for (int i = 0;i < 50;i++){//循环去提交任务
            int taskId = i;
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("任务执行,编号为: " + taskId);
                }
            });
        }
    }
}


//注意Thread 线程一旦跑起来之后,除非任务做完,不然不会被销毁。自然Thread对象也不会提前销毁

单例模式 + 阻塞队列 + 定时器 + 线程池_第12张图片


8.3,线程池构造方法

单例模式 + 阻塞队列 + 定时器 + 线程池_第13张图片


【线程池的拒绝策略:】

单例模式 + 阻塞队列 + 定时器 + 线程池_第14张图片


今天分享就这么多了啰,大家如果觉得写的不错的话还请点点赞咯,十分感谢呢!
在这里插入图片描述

你可能感兴趣的:(JavaEE,单例模式,java)