文章目录
1.1 单例模式
啥是设计模式?
设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路. 按照套路来走局势就不会吃亏.
软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏.
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
这一点在很多场景上都需要. 比如 JDBC
中的 DataSource
实例就只需要一个.
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.
饿汉模式
类加载的同时, 创建实例.
public class SingletonHungry {
//static 修饰成员变量,全局只有一个
private static SingletonHungry instance = new SingletonHungry();
//构造方法私有化,使类对象只有一个
private SingletonHungry() {}
// 对外提供一个获取获取实例对象的方法
// 用static修饰方法
public static SingletonHungry getInstance(){
return instance;
}
}
懒汉模式-单线程版
类加载的时候不创建实例. 第一次使用的时候才创建实例.
public class SingletonLazy {
private static SingletonLazy instance = null;
private SingletonLazy() {}
public static SingletonLazy getInstance() {
if (instance == null) {
instance = new SingletonLazy();
}
return instance;
}
}
懒汉模式-多线程版
上面的懒汉模式的实现是线程不安全的.
线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance
方法, 就可能导致 创建出多个实例.
一旦实例已经创建好了, 后面再多线程环境调用 getInstance
就不再有线程安全问题了(不再修改 instance
了)
加上 synchronized
可以改善这里的线程安全问题.
public class SingletonLazy2 {
private static SingletonLazy2 instance = null;
private SingletonLazy2() {}
// 以下两种方法都可以
// 在获取成员变量时,先判断锁是否被占用
//
// 其实synchronized代码块只需要执行一次就够了,以现在的写法,只要调用了getInstance方法,都要竞争锁,锁竞争是非常耗费系统资源的
// 使用了synchronized就从用户态转到了内核态
public static synchronized SingletonLazy2 getInstance() {
if (instance == null) {
// 初始化过程只执行一次
instance = new SingletonLazy2();
}
return instance;
}
public static SingletonLazy2 getInstance1() {
synchronized(SingletonLazy2.class) {
if (instance == null) {
instance = new SingletonLazy2();
}
return instance;
}
}
// 错误的!!!!!!!!!!!!
// public static SingletonLazy2 getInstance() {
// if (instance == null) {
// 此时已经判断instance为空,争抢锁之后就会创建一个新的实例对象
// synchronized (SingletonLazy2.class){
// instance = new SingletonLazy2();
// }
// }
// return instance;
// }
}
懒汉模式-多线程版(改进)
以下代码在加锁的基础上, 做出了进一步改动:
- 使用双重 if 判定, 降低锁竞争的频率.
- 给
instance
加上了volatile
.
/**
* 使用双重 if 判定, 降低锁竞争的频率.
* 给 instance 加上了 volatile.
*
* 加锁 / 解锁是一件开销比较高的事情. 而懒汉模式的线程不安全只是发生在首次创建实例的时候.
* 因此后续使用的时候, 不必再进行加锁了.
* 外层的 if 就是判定下看当前是否已经把 instance 实例创建出来了.
* 同时为了避免 "内存可见性" 导致读取的 instance 出现偏差, 于是补充上 volatile .
* 当多线程首次调用 getInstance, 大家可能都发现 instance 为 null, 于是又继续往下执行来竞争锁,
* 其中竞争成功的线程, 再完成创建实例的操作.
* 当这个实例创建完了之后, 其他竞争到锁的线程就被里层 if 挡住了. 也就不会继续创建其他实例.
*/
//双重检查锁 DCL
public class SingletonDCL {
//synchronized只能保证原子性和可见性,不能保证有序性(其他线程可能得到一个创建了对象(instance != null),但没有得到某些数据初始化的对象)
//加上volatile保证有序性(可见性与有序性)
private volatile static SingletonDCL instance = null;
private SingletonDCL() {}
public static SingletonDCL getInstance() {
//为了让后面的线程不再获取锁,避免锁竞争
if (instance == null) {
synchronized (SingletonDCL.class) {
//完成初始化操作,只执行一次
if (instance == null) {
instance = new SingletonDCL();
}
}
}
return instance;
}
}
关于单例模式的饿汉和懒汉模式
- 工作中可以使用饿汉模式,因为书写简单且不易出现错
- 饿汉模式在程序加载时完成的初始化,但是由于计算机资源有限,为了节约资源,可以使用懒汉模式
- 懒汉模式就是在使用对象时再去完成初始化操作
- 懒汉模式在多线程模式可能出现线程安全问题
- 那么就需要使用
synchronized
包裹初始化代码块 - 初始化代码只执行一次,后序的线程在调用getInstance()时,依然会产生竞争锁,频繁进行用户态和内核态的切换,非常浪费所资源
- 这时候就是可以用
double check lock
(DCL)的方式,在外层加一个非空校验,避免无用的锁竞争 synchronized
只能保证原子性和可见性,不能保证有序性(其他线程可能得到一个创建了对象(instance != null
),但没有得到某些数据初始化的对象),再使用volatile解决有序性问题- 描述指令重排序可能出现的问题(使某些代码没有得到执行)
1.2 阻塞队列是什么
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.
生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
1) 阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.
比如在 “秒杀” 场景下, 服务器同一时刻可能会收到大量的支付请求. 如果直接处理这些支付请求, 服务器可能扛不住(每个支付请求的处理都需要比较复杂的流程). 这个时候就可以把这些请求都放 到一个阻塞队列中, 然后再由消费者线程慢慢的来处理每个支付请求.
这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮.
2) 阻塞队列也能使生产者和消费者之间 解耦.
比如过年一家人一起包饺子. 一般都是有明确分工, 比如一个人负责擀饺子皮, 其他人负责包. 擀饺子皮的人就是 “生产者”, 包饺子的人就是 “消费者”.
擀饺子皮的人不关心包饺子的人是谁(能包就行, 无论是手工包, 借助工具, 还是机器包), 包饺子的人也不关心擀饺子皮的人是谁(有饺子皮就行, 无论是用擀面杖擀的, 还是拿罐头瓶擀, 还是直接从超市买的).
标准库中的阻塞队列
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
BlockingQueue
是一个接口. 真正实现的类是LinkedBlockingQueue
.put
方法用于阻塞式的入队列,take
用于阻塞式的出队列.BlockingQueue
也有offer
,poll
,peek
等方法, 但是这些方法不带有阻塞特性.
BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
自己实现阻塞队列:
/**
* 通过 "循环队列" 的方式来实现.
* 使用 synchronized 进行加锁控制.
* put 插入元素的时候, 判定如果队列满了, 就进行 wait. (注意, 要在循环中进行 wait. 被唤醒时不一
* 定队列就不满了, 因为同时可能是唤醒了多个线程).
* take 取出元素的时候, 判定如果队列为空, 就进行 wait. (也是循环 wait)
*/
public class MyBlockingQueue {
private int[] elementData = new int[10];
private int head;
private int tail;
private volatile int size;
public void put(int val) throws InterruptedException {
synchronized (this) {
//判满
// 此处最好使用 while.(可能会出现虚假唤醒)
// 否则 notifyAll 的时候, 该线程从 wait 中被唤醒,
// 但是紧接着并未抢占到锁. 当锁被抢占的时候, 可能又已经队列满了
// 就只能继续等待
while (size >= elementData.length) {
wait();
}
// 插入元素
elementData[tail] = val;
tail++;
if (tail >= elementData.length) {
tail = 0;
}
size++;
this.notifyAll();
}
}
public int take() throws InterruptedException {
synchronized (this) {
while (size <= 0) {
wait();
}
int ret = elementData[head];
head++;
if (head >= elementData.length) {
head = 0;
}
size--;
//有空位就唤醒
this.notifyAll();
return ret;
}
}
}
生产者消费者模型
import java.util.concurrent.TimeUnit;
public class Demo03_ProducerConsumer {
// 定义一个阻塞队列
private static MyBlockingQueue queue = new MyBlockingQueue();
public static void main(String[] args) {
// 创建生产者线程
Thread producer = new Thread(() -> {
int num = 1;
while (true) {
// 生产一条打印一条日志
System.out.println("生产了元素 " + num);
try {
// 把消息放入阻塞队列中
queue.put(num);
num++;
// 10ms
TimeUnit.MILLISECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动生产者
producer.start();
// 创建消费者线程
Thread consumer = new Thread(() -> {
while (true) {
try {
// 从队列中获取元素(消息)
int num = queue.take();
// 打印一下消费日志
System.out.println("消费了元素 :" + num);
// 休眠1秒
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动消费者
consumer.start();
}
}