多线程案例 --- 单例模式(饿汉懒汉)阻塞式队列

Posted 满眼*星辰

tags:

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

设计模式

  1. 单例模式
  2. 工厂模式(简单工厂、抽象工厂)
  3. 模板模式
    。。。

单例模式

整个程序的运行中只存储一个对象

饿汉方式和懒汉方式

饿汉方式:

上来不管三七二十一先创建对象再说

class Singleton {
    //1.创建私有的构造函数(防止其他类直接创建)
    private Singleton() {

    }

    //2.定义私有变量(线程安全)
    private static Singleton singleton = new Singleton();

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

}

不用加锁(线程安全的)

缺点:
程序启动之后就会创建,但是创建完了之后有可能不会使用,从而浪费了系统资源。

懒汉方式:

当程序启动之后,并不会进行初始化,而是在什么时候调用什么时候初始化

版本一:线程不安全

非安全的单例模式——懒汉

	class Singleton {
        //1.创建一个私有的构造函数(防止其他地方直接实例化)
        private Singleton() {

        }

        //2.创建一个私有的类对象
        private static Singleton singleton = null;

        //3.提供统一的访问入口(方法)
        public static Singleton getInstance() {
            if (singleton == null) {
                //第一次访问
                singleton = new Singleton();
            }
            return singleton;
        }
    }

这个版本在单线程下是没有问题的,但是如果有两个线程同时想要创建Singleton的实例,那么会怎么样呢?

两个线程到达if (singleton == null) {这个条件判断的时候,连个线程的singleton都为null,所以两个线程都会执行里面的创建实例,所以就创建了两个实例,就不是单例模式了,所以并不适用于多线程,线程不安全。


证明两个创建的不是一个对象

    static class Singleton {
        //1.创建一个私有的构造函数(防止其他地方直接实例化)
        private Singleton() {

        }

        //2.创建一个私有的类对象
        private static Singleton singleton = null;

        //3.提供统一的访问入口(方法)
        public static Singleton getInstance() {
            if (singleton == null) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //第一次访问
                singleton = new Singleton();
            }
            return singleton;
        }
    }

    private static Singleton s1 = null;
    private static Singleton s2 = null;

    public static void main(String[] args) throws InterruptedException {
        //创建新线程执行任务
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                s1 = Singleton.getInstance();
            }
        });
        t1.start();

        //使用主线程执行任务
        s2 = Singleton.getInstance();
        //等待t1执行完
        t1.join();

        System.out.println(s1 == s2);
    }

让两个线程都实例化对象,比较两个实例对象是否是==,结果输出false,不是单例模式

版本二:性能不佳

要解决线程安全的问题,最简单的方式就是加锁

给实例化方法加锁 ,synchronized

    static class Singleton {
        //1.创建一个私有的构造函数(防止其他地方直接实例化)
        private Singleton() {

        }

        //2.创建一个私有的类对象
        private static Singleton singleton = null;

        //3.提供统一的访问入口(方法)
        public static synchronized Singleton getInstance() {
            if (singleton == null) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //第一次访问
                singleton = new Singleton();
            }
            return singleton;
        }
    }

    private static Singleton s1 = null;
    private static Singleton s2 = null;

    public static void main(String[] args) throws InterruptedException {
        //创建新线程执行任务
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                s1 = Singleton.getInstance();
            }
        });
        t1.start();

        //使用主线程执行任务
        s2 = Singleton.getInstance();
        //等待t1执行完
        t1.join();

        System.out.println(s1 == s2);
    }

结果输出true,是线程安全的单例模式

但是这样实现懒汉模式又有一个问题:
不管是不是第一次访问,它都会排队执行。
我们加锁为了确保第一次访问的时候线程是安全的,但是与此同时,当我们再三访问的时候,每次都需要加锁排队,这无疑让单例模式的性能非常低,所以这种方式还有优化的空间。

版本三:性能更加

既然我们在方法外加锁太浪费性能,那我们可不可以在方法体内的加锁呢?
我们来试想一下


    static class Singleton {
        //1.创建一个私有的构造函数(防止其他地方直接实例化)
        private Singleton() {

        }

        //2.创建一个私有的类对象
        private static Singleton singleton = null;

        //3.提供统一的访问入口(方法)
        public static Singleton getInstance() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    //第一次访问
                    singleton = new Singleton();
                }
            }
            return singleton;
        }
    }

这种方式可以吗?不可以!!!
因为第一次获取实例的时候,两个线程都会进入singleton为null的判断里,排队完后,依然是两个线程都执行了里面实例化的对象。

所以我们可以在锁里面再加一次判断singleton是否为null

    static class Singleton {
        //1.创建一个私有的构造函数(防止其他地方直接实例化)
        private Singleton() {

        }

        //2.创建一个私有的类对象
        private static Singleton singleton = null;

        //3.提供统一的访问入口(方法)
        public static Singleton getInstance() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        //第一次访问
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

这种方式就非常完美了,因为两个线程如果是第一次实例对象,先进到第一个为空的判断中,然后排队执行锁里面的内容,谁先竞争到了锁,谁就执行里面的实例化新对象,执行完后,第二个线程进来,又做一个判断,sigleton是否实例化过了,显然,第一个线程已经实例化过了,那么第二个线程就什么也不做,跳出循环返回已经实例化过的singleton对象

这就是著名的双重效验锁

但是!!!!!这个代码还不是最完美的!!!

版本四:完美无瑕

这是因为,
singleton = new Singleton();实例化过程中,看似是一行代码,但是会存在三个步骤

  1. 先在内存中开辟空间(买房)
  2. 初始化(装修)
  3. 将变量 singleton 指向内存区域(入住)

这就会存在一个指令重排序的问题,所有java代码都会有的问题

指令优化(指令重排序):
执行重排序(前):1 -》 2 -》 3
指令重排序(后):1 -》 3 -》 2

这样就1- 3- 2 的顺序就是错误的,这会导致什么问题呢?
在这里插入图片描述

会导致线程二返回了一个空对象。

要解决这个指令重排序的问题,我们只需要用 volatile 关键字即可

	class Singleton {
        //1.创建一个私有的构造函数(方法其他地方直接实例化)
        private Singleton() {

        }

        //2.创建一个私有的类对象
        private static volatile Singleton singleton = null;

        //3.提供统一的访问入口(方法)
        public static Singleton getInstance() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        //第一次访问
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

这个代码才是最终版的懒汉方式

自定义阻塞队列

他是基于生产者消费者模型

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

生产者:

  1. 添加数据
  2. 当数据量满了之后,不要尝试给队列添加数据了,而是阻塞等待
  3. 用wait()、notify()方式进行等待和唤醒

消费者:

  1. 取出数据
  2. 当队列为空的时候,就阻塞等待
    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;
                last++;
                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];
                first++;
                size--;
                //处理是否为最后一个元素
                if (first == values.length) {
                    first = 0;
                }
                //尝试唤醒生产者
                this.notify();;
            }
            return result;
        }
    }

    public static void main(String[] args) {

        MyBlockingQueue queue = new MyBlockingQueue(100);

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

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

实现结果:
在这里插入图片描述

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

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

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

多线程四大经典案例

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

多线程多线程案例

多线程与单例模式