目录
🌟一、单例模式
单例是一种设计模式。这里面有两个关键字,一个是单例,一个是设计模式。什么是设计模式呢?设计模式指的就是业内的大神们根据以往的程序设计经验总结出来的一套方法。是类似于棋谱一样的东西。而单例是什么?单例指的是在全局范围内只有一个实例对象。比如之前数据库的JDBC中就只有一个DataSource。定义数据库的用户名,密码,连接串之后就可以通过DataSource的实例对象获取数据库的连接。参考链接
单例模式的实现方式主要有两种:饿汉模式和懒汉模式。这两个是手写代码都得写出来的重要程度~🤣
🌈1、饿汉模式
因为单例在全局范围内只有一个实例对象,因此我们需要清楚在Java中哪些对象是全局唯一的。在JavaSE部分我们学过,用static修饰的变量是类的成员变量,所有的实例对象,访问的都是同一个成员变量。因此通过类对象与static配合使用可以实现单例。
既然是单例,上述用new的方式是有歧义的,new用来表示产生新的对象。因此我们对上述的代码进行改造,将构造方法进行私有化,直接通过静态方法来调用。
上面这种当类一加载就完成初始化的方式称为饿汉模式。书写简单,不容易出错。 存在的问题:存在很多的资源浪费。
🌈2、懒汉模式(重点!)
在类加载的时候不创建实例,只有在第一次使用的时候才创建实例,避免程序启动的时候浪费过多的系统资源。
(1)单线程环境测试
在单线程环境下是没有问题的,我们接着看一下在多线程环境中。
(2)多线程环境测试
分析出出现线层不安全的原因:
解决办法:加锁——>确定加锁范围
(1)对整个方法加锁
(2)对代码块加锁:但是要加在if判断条件外。
注意:如果在If条件条件内加锁,是不正确的。
比如t1线程现在判断为空,t1执行if中的代码,t2也为空,执行If中的代码。也就是说对象还是创建了两次,所以不正确。
(3)对加锁范围优化
不过上述解决办法还是存在一定的问题:
synchronized代码块在整个程序运行过程中,只要去创建实例调用getInstance()方法,那么久都要参与锁竞争,非常浪费系统资源。实际上我们只需要执行一次就够了,在进行锁竞争之前,判断一下是否已经初始化创建了对象,如果没有再去执行。因此在外面再加入一层if条件判断,这种用两层If条件判断的方式叫做“双重检查锁”。
❓问题:为什么会浪费系统资源?(简单了解)
我们需要知道,用户态和内核态两个概念。用户态是指在Java层面,在JVM中执行的代码。内核态是指执行的是CPU指令,也就是说加入synchronized参与锁竞争之后就从应用层面进入了系统层面。内核态要进行的所有的操作,不单是我们自己写好的程序。
比如银行柜员和用户,银行柜员就相当于内核态,它不仅需要处理顾客的需求,帮忙复印文件,而且还要处理领导的事情,同事的事情等;顾客相当于用户态,只需要复印自己的文件就行。所以在交给内核态银行柜员处理业务之前,尽可能的将自己的事情做好,这样内核他就可以少做一些事情,从而提升效率。
(4)通过上述synchronized加锁解决了原子性,内存可见性。那么有序性如何保证呢?——>对共享变量加入volatile关键字。
❓ 问题:为什么要保证有序性?
在初始化的过程中,并不是一条指令。整个初始化过程中包括LOAD,ASSGIN,STORE。1在内存中开辟一片空间——>2初始化内存的属性——>3将内存中的地址赋值给instance变量。而程序执行的过程中也是按照123这个顺序来的。但是我们知道,编译器可能会对指令重排序,对代码的执行顺序进行优化。我们可以看出1和3是一个强相关的执行过程,都与内存有关系,2是单独的一条指令,因此编译器可能会对通过指令重排序从而改变代码的执行顺序变为132。如果执行顺序变成了132这种,那么在在指定到第三步的时候,将instance变量存储到内存中了,但是该变量还没有进行初始化(第二步还没有执行),那么其他线程就有可能拿到一个创建了一半的对象。因此说这个对象是一个不安全的对象。所以要用volatile修饰变量,禁止指令重排序。
💯 总结
一、单例模式
1、饿汉模式:随着程序加载就完成了初始化。工作中可以使用饿汉模式,书写简单且不容易出错。
2、饿汉模式存在的问题:资源浪费。由于计算机资源有限,为了节约资源,可以使用懒汉模式加载。
3、懒汉模式:在程序加载的时候完成的初始化。
4、懒汉模式存在的问题:在单线程下正常,在多线程环境下可能会出现线程不安全的现象。
5、懒汉模式线程不安全的解决办法:使用synchronized加锁(在方法中加锁或者if外的代码块加锁)
6、上述加锁存在的问题:由于初始化代码只执行一次,后续的线程在调用getInstance方法的时候,依然会产生锁竞争,频繁的进行用户态与内核态之间的切换,非常浪费计算机资源。
7、解决办法:使用DCL(double check lock)DCL双重锁的方式,双层If判断,外面的If用于非空校验,避免无用的锁竞争。
8、通过synchronized解决了原子性,内存可见性的问题,再使用volatile解决由于有序性问题:用volatile修饰共享变量即可。
9、有序性问题主要是因为指令重排序现象的发生。描述其过程。
🌟二、工厂模式
观察下述代码的现象:
解决办法:工厂模式就是用于解决构造方法创建对象的不足。
🌟三、阻塞式队列
🌈1、阻塞队列是什么?
之前我们学习过队列,是一种先进先出的数据结构。阻塞队列也满足队列的特性,主要表现为: 入队元素时,先判断队列是否已经满了,如果满了就阻塞等待,当有空闲空间再插入;
出队元素时,先判断队列是个已经空了,如果空了就阻塞等待,当队列中有元素时再出队。
🌈2、阻塞队列:生产者-消费者模型
阻塞队列的一个典型的应用场景就是“生产者消费者模型”,就是通过一个容器来解决生产者和消费者之间的强耦合关系。生产者和消费者之间并不直接通信,而是通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等消费者处理,直接交给阻塞队列;消费者也不需要找生产者,直接从阻塞队列中取数据就行。
消息队列本身就是一个阻塞队列,在此基础上为放入阻塞队列的消息打上一个标签。打标签可以实现分组的效果。
🌈3、消息队列的作用
(1)解耦
在设计程序的时候,一般要求“高内聚,低耦合”。高内聚:指的是将功能强相关的代码写在一起,方便维护;低耦合:指的是将功能相同的代码封装成一个方法,使用的时候直接调用即可。
(2)削峰填谷
栗子🌰:在工程环境中的应用:
当在微博发生热点事件或者是双十一这种场景下,任何一个节点出现问题都会影响整个业务流程。解决办法:可以使用多个A和B服务器解决,但是实际生活中这种流量暴增的时刻是短暂的,时间点过后都会回归平常,因此这种方式会造成很大的资源浪费,不太适用。因此比较好的方法就是使用消息队列来解决。(消息队列是非常吃内存的)。当然,在这种情况下也可以并行的使用多个A服务器,相较于前一种方法会减少资源的消耗。同样的,当调用第三方服务器接口的时候,在峰值的那一瞬间,是很难直接同时处理这么多的消息的。因此如果调用次数达到上限,那就阻塞一会。也就是说,服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求,服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程)。这个时候就可以把这些请求都放到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求。这样做可以有效进行 "削峰",防止服务器被突然到来的一波请求直接冲垮。
(3)异步
同步:请求方必须死等对方的回应;
异步:发出请求之后,自己去干别的事情,在有响应的时候会接收到通知从而处理响应。
🌈4、演示JDK中提供的阻塞队列
需要知道:
(1)创建阻塞队列。BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue
(2)往阻塞队列中添加元素
queue.put();
(3)从阻塞队列中取元素
queue.take(); 之前的普通的队列取元素是 poll();
public static void main(String[] args) throws InterruptedException {
//基于链表实现的BlockingQueue:还有其他的LinkedBlockingQueue, ArrayBlockingQueue
//1、创建的时候指定队列的容量:当队列满的时候再插入元素就会阻塞
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(3);
//2、往队列中写入元素
queue.put(1);
queue.put(2);
queue.put(3);
System.out.println("插入了3个元素");
System.out.println(queue);
//3、队列已满,此时会阻塞,不会打印下面这句话
// queue.put(4);
// System.out.println("插入了4个元素");
//4、阻塞队列中取出元素
System.out.println("获取的第一个元素是"+queue.take());
System.out.println("获取的第二个元素是"+queue.take());
System.out.println("获取的第三个元素是"+queue.take());
//此时队列已经空了,进入阻塞状态
System.out.println("获取的第四个元素是"+queue.take());
}
控制台输出:
🌈5、自己实现阻塞队列(循环队列-数组实现)
//之前队列底层实现用到的是链表和循环数组。现在阻塞队列就是在其基础上增加了阻塞等待的操作。
//此处以数组实现为例:定义初始化变量
//定义一个保存元素的数组
private volatile int[] elementData = new int[3];
//定义头部索引
private volatile int head;
//定义尾部索引
private volatile int tail;
//定义有效元素的个数
private volatile int size;
/**
* 插入一个元素
* @param val
*/
public void put(int val) throws InterruptedException {
//------在普通队列基础上加入等待和唤醒的操作,这两个操作与synchronized息息相关。
//------一般new出来的对象,锁对象都是this(指大部分情况)
synchronized(this){
//1、判断元素是否已满:满了就阻塞等待
while(size >= elementData.length){
this.wait();
}
//2、队尾插入元素
elementData[tail] = val;
//3、更新tail索引:循环队列(之前队列的实现用得是取模运算,不是简单的++)
tail++;
if(tail >= elementData.length){
tail=0;
}
//4、更新元素个数
size++;
//5、唤醒
notifyAll();
}
}
/**
* 取出元素
* @return
*/
public int take() throws InterruptedException {
//根据共享范围加锁,锁对象是this即可
synchronized (this) {
//1、判断队列是否为空:为空就阻塞
while(size <= 0) {
this.wait();
}
//2、从队首出队
int val = elementData[head];
//3、更新head下标
head++;
if (head >= elementData.length) {
head = 0;
}
//4、修改有效元素的个数
size--;
//5、唤醒
notifyAll();
//6、返回队首元素
return val;
}
}
测试类:
public static void main(String[] args) throws InterruptedException {
a02_MyBlockQueue deque = new a02_MyBlockQueue();
deque.put(1);
deque.put(2);
deque.put(3);
//Ctrl D复制当前行 Ctrl Y删除当前行
System.out.println("已经插入三个元素");
//阻塞等待
// deque.put(4);
// System.out.println(deque);
// System.out.println("已经插入四个元素");
deque.take();
deque.take();
deque.take();
System.out.println("已经出队3个元素");
//阻塞等待
deque.take();
System.out.println("已经出队4个队列");
}
🚨注意
(1) 虚假唤醒:
在第一次满足wait() 时,线程进入阻塞等待。但是在这个期间可能会发生很多事情,因此当被唤醒之后,应该再次检查线程的状态。使用while,而不是if。
(2)对于所有的共享变量都要加volatile。
(3)使用synchronized进行加锁控制。
( 4)上述是通过循环队列的方式来实现的。
🌈6、模拟实现生产者与消费者模型
分别用一个线程模拟生产者和消费者。
//模拟目标:将生产者生产的商品先放入消息队列中存储,等过10秒之后,消费者再开始消费
//1、定义一个阻塞队列
private static BlockingQueue queue = new LinkedBlockingQueue(100);
public static void main(String[] args) {
//2、创建生产者线程
Thread producer = new Thread(()->{
int goods = 1;
//3、生产者开始不停的生产
while (true){
System.out.println("生产了商品:"+goods);
try {
//4、将生产的商品先放入阻塞队列中
queue.put(goods);
goods++;
//等待10ms生产一个,不然打印太快
TimeUnit.MICROSECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
//5、创建消费者
Thread customer = new Thread(()->{
while (true){
//6、消费者开始从阻塞队列中获取商品
try {
int goods = (int) queue.take();
System.out.println("消费了商品:"+goods);
// 休眠1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}