目录
1. 单例模式
单例模式是最常考的设计模式之一
关于设计模式:
设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路.
按照套路来走局势就不会吃亏. 软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路.
按照这个套路来实现代码, 也不会吃亏.
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个.
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.
1.1 饿汉模式
类加载的同时, 创建实例.
class Singleton{ //饿汉模式
//1.使用static创建一个实例,并且立即进行实例化
//这个instance对应的实例,就是该类唯一的实例
private static Singleton instance=new Singleton();
//2.为了防止程序猿在其他地方不小心new这个Singleton,就可以把构造方法设为private
private Singleton(){}
//3.提供一个方法,让外面能够拿到唯一实例
public static Singleton getInstance(){
return instance;
}
}
1.2 懒汉模式-单线程版
类加载的时候不创建实例. 第一次使用的时候才创建实例.
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
1.3 懒汉模式-多线程版
上面的懒汉模式的实现是线程不安全的.
线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance 方法, 就可能导致创建出多个实例.
一旦实例已经创建好了, 后面再多线程环境调用 getInstance 就不再有线程安全问题了(不再修改instance 了)
加上 synchronized 可以改善这里的线程安全问题.
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
1.4 懒汉模式-多线程版(改进-最终版)
以下代码在加锁的基础上, 做出了进一步改动:
- 使用双重 if 判定, 降低锁竞争的频率.
- 给 instance 加上了 volatile. (保证内存可见性)
class Singleton2{ //懒汉模式
//1.就不是立即初始化实例
private static volatile Singleton2 instance=null;
//2.把构造方法设为private
private Singleton2(){}
//3.提供一个方法来获取到上面单列的实例
//只有当真正需要用到这个实例的时候,才会真正去创建这个实例
public static Singleton2 getInstance(){
//如果这个条件成立,说明当前的单列未初始化过,存在线程安全风险,就需要加锁
if (instance==null){ //上面的volatile保证内存可见性
synchronized (Singleton2.class){
if (instance==null){
instance=new Singleton2();
}
}
}
return instance;
}
}
理解双重 if 判定 / volatile:
加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候. 因此后续使用的时候, 不必再进行加锁了.
外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.
同时为了避免 “内存可见性” 导致读取的 instance 出现偏差, 于是补充上 volatile .
当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁,
其中竞争成功的线程, 再完成创建实例的操作.
当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.
- 有三个线程, 开始执行 getInstance , 通过外层的 if (instance == null) 知道了实例还没有创建的消息. 于是开始竞争同一把锁.
- ) 其中线程1 率先获取到锁, 此时线程1 通过里层的 if (instance == null) 进一步确认实例是否已经创建. 如果没创建, 就把这个实例创建出来.
- 当线程1 释放锁之后, 线程2 和 线程3 也拿到锁, 也通过里层的 if (instance == null) 来确认实例是否已经创建, 发现实例已经创建出来了, 就不再创建了.
- 后续的线程, 不必加锁, 直接就通过外层 if (instance == null) 就知道实例已经创建了, 从而不再尝试获取锁了. 降低了开销.
2. 阻塞式队列
2.1 关于阻塞式队列
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.
2.2 关于生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
(降低耦合性,提高内聚性)
-
阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.
比如在“秒杀”这一场景下,服务器面临巨大数量的请求,这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮. -
阻塞队列也能使生产者和消费者之间 解耦. (降低了两个服务器之间的耦合性(关联性))
2.3 标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
- BlockingQueue 是一个接口. 真正实现的类是 LinkedBlockingQueue.
put
方法用于阻塞式的入队列,take
用于阻塞式的出队列.- BlockingQueue 也有 offer, poll, peek 等方法, 但是这些方法不带有阻塞特性.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
2.4 阻塞队列模拟实现
- 通过 “循环队列” 的方式来实现.
- 使用 synchronized 进行加锁控制.
- put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一定队列就不满了, 因为同时可能是唤醒了多个线程).
- take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
- 其他相关要点在代码中以注释形式给出
class MyBlockingQueue{
//保存数据的本体
private int[] data=new int[1000];
//有效元素个数
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){
//队列满了,阻塞等待
locker.wait();
}
//把新的元素放到tail位置上
data[tail]=value;
tail++;
//处理tail到达数组末尾的情况
if (tail>=data.length){
tail=0; //循环至队头
}
// 写法2:
// tail=tail%data.length;
size++;//插入完成之后修改有效元素个数
//如果入队成功,则队列非空,唤醒take中的阻塞等待
locker.notify();
}
}
//出队列
public Integer take() throws InterruptedException {
synchronized (locker){
if (size==0){
//如果队列为空,则阻塞等待
locker.wait();
}
//取出head位置的元素
int ret=data[head];
head++;
if (head>=data.length){
head=0; //循环至队头
}
size--;
//take成功之后,就唤醒put(满)中的等待
locker.notify();
return ret;
}
}
}
2.5 生产者消费者模型代码举列
这里的生产者消费者模型我就用上面自己模拟实现的阻塞式队列实现,顺便测试了模拟实现的阻塞式队列是否成功
private static MyBlockingQueue queue=new MyBlockingQueue();
public static void main(String[] args) {
//实现一个简单的生产者消费者模型
Thread producer=new Thread(()->{
int num=0;
while (true){
System.out.println("生产了:"+num);
try {
queue.put(num);
num++;
//当生产者生产的慢一些的时候, 消费者就得跟着生产者的步伐走.
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
producer.start();
Thread customer=new Thread(()->{
while (true){
int num= 0;
try {
num = queue.take();
System.out.println("消费了:"+num);
//Thread.sleep();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
customer.start();
}
在生产数字的produce线程中加sleep操作,导致消费数字线程customer跟着生产线程走:
在消费数字的customer线程中加sleep操作,导致生产线程一瞬间就生产满了1000个数字(阻塞队列初始化给了1000个数字空间),然后由消费者线程再次消费,消耗一个即可让阻塞队列不满,进而继续生产数字 :
- over ~