多线程案例 -- 单例模式阻塞队列

Posted Putarmor

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了多线程案例 -- 单例模式阻塞队列相关的知识,希望对你有一定的参考价值。

设计模式

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性。

毫无疑问,设计模式于己于他人于系统都是多赢的,设计模式使代码编制真正工程化,设计模式是软件工程的基石,如同大厦的一块块砖石一样。项目中合理的运用设计模式可以完美的解决很多问题,每种模式在现在中都有相应的原理来与之对应,每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的核心解决方案,这也是它能被广泛应用的原因。简单说:

模式:在某些场景下,针对某类问题的某种通用的解决方案。

场景:项目所在的环境

问题:约束条件,项目目标等

解决方案:通用、可复用的设计,解决约束达到目标。

设计模式分类:

1.创建型模式:对象实例化的模式,创建型模式用于解耦对象的实例化过程。(如单例模式、工厂模式【简单工厂、抽象工厂】)

2.结构型模式:把类或对象结合在一起形成一个更大的结构。(如:适配器模式、桥联模式)

3.行为型模式:类和对象如何交互,及划分责任和算法。(如:访问者模式、模板模式)

接下来,我们主要看一下单例模式:


一.单例模式

单例模式:整个程序的运行中只存储一个对象或者说某个类只能有一个实例,提供一个全局的访问点。对于单例模式而言,它的创建方式有两种:饿汉方式懒汉方式

饿汉方式:

饿汉方式比较粗暴,什么都不管,先创建一个对象再说:
优点:线程安全(得益于定义的static私有变量,随程序启动而加载)

class Single{
    //1.声明‘私有’的构造函数,为了防止其他类进行创建
    private Single(){

    }
    //2.定义私有变量
    private static Single single = new Single();

    //3.提供公共的获取实例的方法
    public static Single getInstance(){
        return single;
    }
}

在这里插入图片描述
可以看出,单例模式私有构造方法不让其他类使用去创建实例对象。

饿汉方式缺点:程序启动之后就会创建对象,但是创建结束之后可能不使用,浪费了系统资源。


懒汉方式:

程序启动之后,不会进行初始化操作,而是什么时候调用什么时候初始化:

懒汉示例:

class Single{
    //1.创建私有的构造函数
    private Single(){

    }
    //2.创建一个私有的类对象
    private static Single single = null;
    //3.提供统一的访问入口
    public static Single getInstance(){
        //如果第一次访问则创建,以后直接返回
        if(single == null){
            single = new Single();
        }
        return single;
    }
}

public class ThreadDemo5 {
    public static void main(String[] args) {
        Single single1 = Single.getInstance(); //创建第一个对象
        Single single2 = Single.getInstance();  //创建第二个对象
        System.out.println(single1 == single2);
    }
}

在这里插入图片描述
从结果上看起来没有什么问题,但实际上这种方式是线程不安全的,之所以我们看到结果为true,是因为使用的是单个线程。

线程不安全原因:当多个线程访问getInstance()时,线程判断single都为null,因此会进行new操作,这样创建了不同的对象,导致不是单例模式。


1.线程不安全版本
lass Single2{
    //1.创建私有的构造函数
    private Single2(){

    }
    //2.创建一个私有的类对象
    private static Single2 single = null;
    //3.提供统一的访问入口
    public static  Single2 getInstance(){
        //如果第一次访问则创建,以后直接返回
        if(single == null){
            try {
                Thread.sleep(1000);  //让线程等待
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            single = new Single2();
        }
        return single;
    }
}

public class ThreadDemo5 {
    private static Single2 s1 = null;
    private static Single2 s2 = null;

    public static void main(String[] args) throws InterruptedException {
        //创建新线程执行任务
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                s1 = Single2.getInstance();
            }
        });
        t1.start();
        //使用主线程执行任务
        s2 = Single2.getInstance();
        t1.join();
        System.out.println(s1 == s2);
    }
}

在这里插入图片描述
可以发现子线程与主线程都进行了实例化对象操作,创建的是两个不同的对象,因此不是单例模式!

2.加锁版本,解决线程不安全

在这里插入图片描述
对于上面的代码而言,其执行结果为true,表明它是线程安全的单例模式。

缺点:无论是否第一次访问getInstance(),线程都会排队执行;我们给程序加锁是为了保证第一次访问时线程安全,而每次都加锁会导致单例模式的性能很低。

3.双重效验锁(完美提升性能)
public static Single3 getInstance(){
        //如果第一次访问则创建,以后直接返回
        if(single == null){
            synchronized (Single3.class){
                if(single == null){
                    single = new Single3();
                }
            }
        }
        return single;
    }

在synchronized锁内再加一次为空判断,当两个线程第一次访问getInstance()时,先进到一个为空判断,然后排队执行锁中的内容;这个过程中谁先竞争到锁,谁就率先执行实例化对象操作,执行结束后,第二个线发现single对象不为空已经被实例化,因此不进到if语句中,直接返回实例化后的single对象。

但是这种方案还不是最好的,有点小瑕疵。。。

4.优化方案
single = new Single()

分析上面这行代码:

实例化过程中,看起来一步,但实际上分了三步完成了实例化操作:
①先在内存中开辟空间(相当于买房)
②初始化操作(相当于装修房子)
③引用变量指向内存区域对象(相当于入住)

这里面会存在指令重排序问题:
在这里插入图片描述
比如:原来执行顺序① ② ③ 指令重排序后:① ③ ②

那么指令重排序后执行程序,会出现什么结果呢?

线程1先开辟了内存空间,然后将变量指向了内存空间,此时时间片用完;线程2执行程序时发现single不为null(引用已经指向内存,只不过未初始化),直接返回了一个null对象。

那么如何解决这个问题呢?
使用volatile关键字解决内存可见性问题以及指令重排序

最终懒汉单例模式版本:

class Single3{
    //1.创建私有的构造函数
    private Single3(){

    }
    //2.创建一个私有的类对象
    private static volatile Single3 single = null;
    //3.提供统一的访问入口
    public static Single3 getInstance(){
        //如果第一次访问则创建,以后直接返回
        if(single == null){
            synchronized (Single3.class){
                if(single == null){
                    single = new Single3();
                }
            }
        }
        return single;
    }
}

二.阻塞队列

基于生产者消费者模型实现

生产者消费者模型:生产者生产模型,消费者消费生产者生产的数据。

生产者:添加数据;当队列已满时,不要给队列尝试添加数据,而是让线程阻塞等待;利用wait()和notify()等待和唤醒(实现线程间通讯)
消费者:取出数据,阻塞点在于:队列为空时

/**
 * 自定义阻塞队列
 */
public class ThreadDemo7 {
    static class MyBlockingQueue {
        private int[] values; // 实际存储数据的数组
        private int first; // 队首
        private int last;  // 队尾
        private int size;  // 队里元素的实际大小

        public MyBlockingQueue(int initial) {
            // 初始化变量
            values = new int[initial];
            first = 0;
            last = 0;
            size = 0;
        }

        // 添加元素(队尾)
        public void offer(int val) {
            synchronized (this) {
                // 判断边界值
                if (size == values.length) {
                    // 队列已满,阻塞等待
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 添加元素到队尾
                values[last++] = val;
                size++;
                // 判断是否为最后一个元素
                if (last == values.length) {
                    last = 0;
                }
                // 尝试唤醒消费者
                this.notify();
            }
        }

        // 查询方法
        public int poll() {
            int result = -1;
            synchronized (this) {
                // 判断边界值
                if (size == 0) {
                    // 队列为空,阻塞等待
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 取元素
                result = values[first++];
                size--;
                // 判断是否为最后一个元素
                if (first == values.length) {
                    first = 0;
                }
                // 尝试唤醒生产者
                this.notify();
            }
            return result;
        }
    }

    public static void main(String[] args) {

        MyBlockingQueue myBlockingQueue = new MyBlockingQueue(100);

        // 生产者
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                // 每隔 500 毫秒生产一条数据
                while (true) {
                    int num = new Random().nextInt(10);
                    System.out.println("生产了随机数:" + num);
                    myBlockingQueue.offer(num);
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });
        t1.start();

        // 创建消费者
        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    int result = myBlockingQueue.poll();
                    System.out.println("消费了数据:" + result);
                }
            }
        });
        t2.start();
    }
}

在这里插入图片描述
基于阻塞队列,可以看出生产者生产了一个数据,消费者消费了一个数据;并不是生产者生产一个数据,消费者无限消费下去。

以上是关于多线程案例 -- 单例模式阻塞队列的主要内容,如果未能解决你的问题,请参考以下文章

JavaEE & 线程案例 & 单例模式 and 阻塞队列

多线程四大经典案例及java多线程的实现

多线程四大经典案例

多线程(七):单例模式+阻塞式队列

多线程(七):单例模式+阻塞式队列

多线程阻塞队列定时器线程安全的单例模式的原理及实现