目录
一. 单例模式
单例模式是一种设计模式,单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例 ,就是保证类在内存中只有一个对象。
1.1 饿汉模式
举例饿汉模式:早上吃饭使用了三个碗,吃完后,马上把三个碗都洗了;
而如何保证类在内存中只有一个对象?
通过 Singleton 这个类 来实现单例模式,保证 Singleton 这个类只有唯一实例;
在下面程序中使用到的 static; static 修饰的成员更加准确的说,是“类成员”=>“类属性/类方法”。一个java 程序中,一个类对象只存在一份(JVM保证的),进一步的也就保证了类的 static 成员也是只有一份的。
//通过Singleton 这个类 来实现单例模式,保证 Singleton 这个类只有唯一实例
/**
* 饿汉模式
* 通过Singleton 这个类 来实现单例模式,保证 Singleton 这个类只有唯一实例
*/
class Singleton {
//1.使用static 创建一个实例,并且立即实例化
//这个instance 对应的实例,就是该类的唯一实例
private static Singleton instance = new Singleton();
//2.为了防止程序员在其他地方不小心 new 这个 Singleton ,就可以把构造方法设为private
private Singleton() {};
//3.提供一个方法,让外面(main方法里)拿到唯一实例
public static Singleton getInstance() {
return instance;
}
}
public class Test18 {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
}
}
上诉代码针对唯一的实例初始化,比较着急,在类加载阶段,就会直接创建实例(程序中用到这个类,就会立即加载)实例就行,如下面程序所示:
//使用 static 创建一个实例,并且立即实例化
//这个 instance 对应的实例,就是该类的唯一实例;
private static Singleton instance = new Singleton();
饿汉模式中 getInstance,仅仅是读取了变量的内容,如果多个线程只是读取一个变量,不修改,此时任然是线程安全的。如下面程序所示:
//提供一个方法,让外面(main 方法里)拿到唯一实例;
public static Singleton getInstance(){
return instance;
}
1.2 懒汉模式
举例懒汉模式:早上使用了三个碗,吃完后,先放着,晚上需要一个就洗一个,需要两个就洗两个,剩下的不管。
相对于饿汉模式来说,懒汉模式更加高效。
如下代码是完全体的线程安全单例模式,具有:1.正确的位置加锁;2.双重 if ;3.volatile。
/**
* 实现单例模式 - 懒汉模式
*/
class Singleton2 {
//1.把不是立即就初始实例
private static volatile Singleton2 instance2 = null;
//2.把构造方法设为 private
private Singleton2() {}
//3.提供一个方法来获取到上述单例模式
//只有当真正需要到这个 实例 的时候,才会真正去创建这个实例
public static Singleton2 getInstance() {
// 如果这个条件成立, 说明当前的单例未初始化过的, 存在线程安全风险, 就需要加锁~~
if (instance2 == null) {
synchronized (Singleton2.class) {
if (instance2 == null) {
instance2 = new Singleton2();
}
}
}
return instance2;
}
}
public class Test19 {
public static void main(String[] args) {
Singleton2 instance2 = Singleton2.getInstance();
}
}
懒汉模式中即包含了读,又包含了修改。但是这里的读和修改,是分为两个步骤的(不是原子性),存在线程安全问题,,因此需要加锁 synchronized。加锁可能导致代码出现阻塞,外层条件可能执行几分钟几分钟之后里层才开始执行。在这个执行的时间差中间, instance 也可能被其他线程给修改的。
二. 阻塞队列
阻塞队列是一种特殊的队列 ,也遵守 " 先进先出 " 的原则 。
阻塞队列能是一种线程安全的数据结构 , 并且具有以下特性 :
当队列满的时候 , 继续入队列就会阻塞 , 直到有其他线程从队列中取走元素 ;
当队列空的时候 , 继续出队列也会阻塞 , 直到有其他线程往队列中插入元素 ;
阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。
生产者消费者模型 :生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等 待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取。
(1)阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
比如在 " 秒杀 " 场景下 , 服务器同一时刻可能会收到大量的支付请求 . 如果直接处理这些支付请求 ,服务器可能扛不住( 每个支付请求的处理都需要比较复杂的流程 )。 这个时候就可以把这些请求都放到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求。
这样做可以有效进行 " 削峰 ", 防止服务器被突然到来的一波请求直接冲垮。
(2)阻塞队列也能使生产者和消费者之间解耦。
比如做饭吃。在一个家里都是有明确分工的,一个人负责买菜回来洗,另一个人负责将菜煮熟。
那么买菜回来洗的人就是 “生产者”,煮菜的人就是“消费者”。
两个人各干各的,有就做。
应用:
生产者消费者模型,是实际开发中非常有用的一种多线程开发手段,尤其是在服务器开发场景中。
假设,有两个服务器A、B。A作为入口服务器直接接收用户的网络请求,B作为应用服务器,来给A提供数据。
1)通常情况下
未使用生产者消费者模型如下图所示:
如果不使用生产者消费者模型,此时的A和B的耦合性是比较强的,在开发A代码的时候就得到充分了解B的一些接口。开发B代码的时候也得充分了解到A是怎样调用的。一旦想把B换成C,A的代码就需要较大的改动。如果B没了,也可能直接导致A也没了。
因此,使用生产者消费者模型,就可以降低这里的耦合,如下图所示:
优点:能够让多个服务器程序之间更充分的解耦合;
对于请求:A是生产者,B是消费者 对于响应:A是消费者,B是生产者
阻塞队列是作为交易场所。
A只需要关注如何与队列交互,不需要认识B。 B也只需要关注如何与队列交互,也不需要认识A。A与B直接互不影响。
2)暴涨情况下
未使用生产者消费者模型如下图所示:
当请求量突然暴涨不可控的时候,A作为入口服务器,计算量很轻,请求暴涨,问题不大。但是B作为应用服务器,计算量可能很大,需要的系统资源也更多,如果请求更多了,需要的资源进一步增加,如果主机的硬件不够,可能程序就撑不住了。
使用生产者消费者模型如下图所示
优点:能够对于请求进行“削峰填谷”;
当A请求暴涨时=>阻塞队列的请求暴涨。由于阻塞队列没什么计算量,就单纯的存个数据,就能抗住跟大的压力。B任然按照原来的速度来消费数据,不会因为A的暴涨而引起暴涨。
“削峰” 指的是峰值很多时候不是持续的,就一阵子,过去了就又恢复了。
“填谷” B任然按照原来的频率来处理之前的积压数据。
2.1 阻塞队列的实现(生产者消费者模型)
通过 " 循环队列 " 的方式来实现 ;
使用 synchronized 进行加锁控制 ;
put 插入元素的时候 , 判定如果队列满了 , 就进行 wait. ( 注意 , 要在循环中进行 wait. 被唤醒时不一
定队列就不满了 , 因为同时可能是唤醒了多个线程 );
take 取出元素的时候 , 判定如果队列为空 , 就进行 wait. ( 也是循环 wait)。
class MyBlockingQueue {
//保存数据的本体
private int[] data = new int[1024];
//有效元素个数
private int size = 0;
//队列下标
private int head = 0;
//队尾下标
private int tail = 0;
//专门的锁对象
private Object locker = new Object();
// 入队列
public void put(int value) throws InterruptedException{
synchronized (locker) {
if (size == data.length) {
//队列满了,暂时先直接返回。
//return
locker.wait();
}
//把新的元素放到 tail 位置上
data[tail] = value;
tail++;
//处理tail 到达数组末尾的情况
if (tail >= data.length) {
tail = 0;
}
//tail = tail % data.length;
size++;//插入完成之后要修改元素个数
//如果入队成功,则队列排空,于是就唤醒take 中的阻塞等待
locker.notify();
}
}
//出队列
public Integer take() throws InterruptedException {
synchronized (locker) {
if (size == 0) {
//如果队列为空,就返回一个非法值
//return null;
locker.wait();
}
//取出head 位置的元素
int ret = data[head];
head++;
if (head >= data.length) {
head = 0;
}
size--;
//take 成功之后。就唤醒put 中的等待
locker.notify();
return ret;
}
}
}
/**
* 实现一个简单的生产者消费者模型
*/
public class Test21 {
private static MyBlockingQueue queue = new MyBlockingQueue();
public static void main(String[] args) throws InterruptedException {
Thread producer = new Thread(() -> {
int num = 0;
while (true) {
System.out.println("生产了 :" + num);
try {
queue.put(num);//入队
} catch (InterruptedException e) {
e.printStackTrace();
}
num++;
}
}) ;
producer.start();
Thread customer = new Thread(() -> {
while (true) {
try {
int num = queue.take();
System.out.println("消费了 : " + num);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}
}