目录
单例设计模式
单例设计模式——饿汉式
单例设计模式——懒汉式
单例设计模式——懒汉式(优化步骤)
生产者消费者模型
介绍
优点
补充:关于阻塞队列
单例设计模式
单例设计模式能够保证某个类的实例在程序运行过程中始终都只会存在一份。这一点在很多场景上都有需要,比如JDBC中DataSource的实例就只需要一个就可以了。为了满足这种需求,程序开发中的大佬们设计出了单例模式的一种方式供我们使用。单例模式的版本有饿汉式和懒汉式两种,接下来一块看一下这两种设计模式的使用。
单例设计模式——饿汉式
这里的‘饿’可以理解为着急的意思,在程序中的体现为:“在类加载的时候就进行了实例对象的创建”,同时为了保证外部能够获得这个类的实例并且只能获得同一个实例对象,遂将该类的构造方法进行了私有化,然后对外提供了获得该唯一实例的方法。单例设计模式——“饿汉式”程序设计如下:
/** * 单例设计模式——饿汉式 */ public class SingleTonHungry { private static SingleTonHungry instance = new SingleTonHungry(); //对构造方法进行私有化,不让外部创建该类的实例对象 private SingleTonHungry() {} //对外提供获得该类在类中的成员的唯一实例对象的方法 public static SingleTonHungry getInstance() { return instance; } }
关于线程安全与否:
单例设计模式——‘饿汉式’由于在类加载时就指定了类实例属性的值,在后续的过程中不再创建新的类实例,而只是通过该类提供的的类方法获得这个类实例对象,因此在多线程环境中,相当于说是对共享资源进行只读操作,因此饿汉式的单例设计模式是线程安全的。
单例设计模式——懒汉式
与“饿汉式”版本类似,为了保证只能有一份实例,它也对自己的构造方法进行了私有化封装并向外提供了获取类属性实例的方法。这里‘懒’字的含义并不是贬义词,在程序中是指当使用到这个类的实例时,我才进行它的实例对象的创建,并且只创建一份。在还没有使用到这个类的实例时,不进行实例对象的创建。“懒汉式”的设计模式相对于“饿汉式”有诸多的优势并且在开发中通常更多的进行“懒汉式”单例设计模式的使用。它的程序设计如下:
/** * 单例设计模式——懒汉式 */ publiic class SingleTonIdler { private static volatile SingleTonIdler instance; //构造方法私有化,防止外部new对象 private SingleTonIdler() {} //向外提供获得该类的单一实例的方法 public static SingleTonIdler getInstance() { if(instance == null) { synchronized(SingleTonIdler.class) { if(instance == null) { instance = new SingleTonIdler(); } } } return instance; } }
关于线程安全与否:
应为在高并发的环境下存在对共享资源的修改操作,因此懒汉式版本的单例设计模式不是线程安全的。
单例设计模式——懒汉式(优化步骤)
优化成线程安全版本:
😄因为对象实例的创建除主要发生在对外提供的获取该类实例对象的方法中,因此我们主要考虑在这个方法中对实例对象的创建只进行一次,并且只有一个线程来创建一次。
😄最简单的方法就是直接对获取实例的方法使用synchronized进行加锁操作,确保每次都有且只有一个线程进入该方法执行。但是我们想一下:实例对象的在线程安全的环境下创建一次后此后仅被进行读操作,就不再进行修改了,那么在实例对象后创建后如果继续对方法加锁,那么势必会降低程序的运行效率,造成严重的线程阻塞。
😄因此,我们可以先让所有线程进行该类实例对象是否创建的判断,如果创建了,就直接让这些并发线程带走已经创建好的实例对象,如果没有创建,就对创建过程加锁,让这些线程竞争该锁,拿到锁的线程进行一次单一实例对象的创建即可。这样,单一实例对象创建后,并发线程就不会再去争夺锁资源造成阻塞而造成程序运行效率的损失,而且也可以实现安全的并发访问操作。于是我们对单线程环境下的懒汉版本的单例设计模式程序进行了下面的改造。
- 在锁的外层先加一层实例对象是否已创建的判断,如果没有创建,则进入判断条件满足的语句块,否则直接返回已经创建了的实例对象
- 进入语句块后,使用synchronized对创建实例的过程进行加锁,某个线程竞争得到锁后,再进行一次实例对象否否已创建的判断,如果实例还没有创建,就创建,否则不满足判断条件,释放锁。这样来确保即使第一次并发有多个线程进入了判断体语句块,实例对象也只会被创建一次。
- 使用volatile修饰实例成员对象的引用,确保其表现出内存可见性。因为编译器可能一开始在判断到有多次对实例对象的访问的情况下会将该实例对象的值放在寄存器中,以至于其他线程对实例对象的引用的修改不能及时被另外的线程感知到,就会出现误判的结果。
生产者消费者模型
介绍
😄生产者和消费者模式就是通过一个容器来解决“生产者”和“消费者”之间高度耦合的情况。即“生产者”和“消费者”之间不是直接通信的,而是通过阻塞队列。
😄这样,“生产者”生产出物品后将物品扔给某条通道而不用关心“消费者”具体是谁;同理,“消费者”从这条通道中获得物品,也不必关系生产者究竟是谁。这就实现了生产者和消费者之间的解耦合。
优点
- 能够让多个服务器之间充分的解耦合
😄例如,有两台服务器用户处理用户的请求,其中一台服务器用来接收用户的请求,另一台服务器用来具体处理用户的请求。接受请求的服务器和处理请求的服务器之间不必关心两者之间具体是怎么通信的,接受请求的服务器只需要将数据写入阻塞队列而不必关系数据是怎么进行处理的;同理,处理请求的服务器只需要从阻塞队列获取用户的请求并将用户的请求进行处理即可。处理请求的服务器返回处理结果的过程也类似,只需要将处理结果写入阻塞队列,接收用户请求的服务器从阻塞队列获得结果后返回即可。- 能够对于用户请求进行“削峰填谷”
😄未使用生产者消费者模型时,如果请求梁突然暴增将导致处理用户请求服务器的压力暴增。对于接收用户请求的服务器来说,计算量较小,用户请求量暴增并不可怕;但对于处理用户请求的服务器来说,计算量较大,当请求暴增时,压力骤增,很有可能程序就挂掉了。
😄而对于使用了生产者消费者模型的结构来说,接收用户请求的服务器将请求扔到阻塞队列中,处理请求的服务器从阻塞队列中获取请求进行处理。当某个时间段用户的请求骤增时,阻塞队列能够起到缓冲用户请求的效果,使得处理请求的服务器仍保持自己的节奏从阻塞队列中获取请求,有条不紊的进行处理,而不用担心处理请求的服务器因请求暴增而带来的性能消耗问题,这就起到了上面我们说的“削峰”的效果。同理,当用户请求量降低时,阻塞队列继续工作,将阻塞在接受用户请求的服务器中的用户请求仍然有条不紊的输送往处理请求的服务器,这就实现了我们上边说的“填谷”。这个这就好比我们的三峡大坝(接受用户请求的服务器),在水量(用户请求)骤增时蓄水,在水量(用户请求)减少时进行开闸放水,为下游正常生活(处理用户请求的服务器)提供了保障。
补充:关于阻塞队列
简介
😄与普通队列相比,阻塞队列增加了以下的特性:
- 是线程安全的
- 当队列容量满的时候,继续入队就会阻塞,直到其他线程从队列中取走元素后阻塞恢复
- 当队列容量为空时,继续出队也会产生阻塞,直到其他线程往队列中写入数据时,阻塞恢复
Java标准库中内置的阻塞队列
Java标准库中内置的阻塞队列时BlockingQueue<T>,这是一个接口,LinkedBlockingQueue<T>和ArrayBlockingQueue<T>实现了这个接口。
我们可以直接使用Java中内置的阻塞队列:
- 其中的put方法用于向阻塞队列中添加数据;take方法用于从阻塞队列中取出数据。
- 当然因为它也实现了Queue接口也有普通队列的offer/poll方法,但这两个方法是不能实现阻塞效果的,因此我们通常使用前两个方法。
练习:自己实现一个数组版本的阻塞队列
public class MyBlockingQueue { private final Object locker; private final int[] elem; private int head; private int tail; private int size; public MyBlockingQueue() { locker = new Object(); elem = new int[1000]; //初始化队列默认大小为1000 } //入队列 public void put(int value) throws InterruptedException { synchronized (locker) { if(size == elem.length) { //已满,阻塞. locker.wait(); } elem[tail++] = value; if(tail >= elem.length) { tail = 0; } size++; //入队列成功,唤醒出队列的阻塞 locker.notify(); } } //出队列 public int take() throws InterruptedException { synchronized (locker) { if(size == 0) { //队列为空,阻塞 locker.wait(); } size--; //元素出队列成功,唤醒入队列的阻塞队列 //如果有多条线程阻塞,这里的唤醒是随机的 locker.notify(); return elem[head++]; } } public int getSize() { return this.size; } }