单例模式是最常考的设计模式之一
关于设计模式:
设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有一些固定的套路.
按照套路来走局势就不会吃亏. 软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路.
按照这个套路来实现代码, 也不会吃亏.
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个.
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.
类加载的同时, 创建实例.
class Singleton{ //饿汉模式
//1.使用static创建一个实例,并且立即进行实例化
//这个instance对应的实例,就是该类唯一的实例
private static Singleton instance=new Singleton();
//2.为了防止程序猿在其他地方不小心new这个Singleton,就可以把构造方法设为private
private Singleton(){}
//3.提供一个方法,让外面能够拿到唯一实例
public static Singleton getInstance(){
return instance;
}
}
类加载的时候不创建实例. 第一次使用的时候才创建实例.
class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面的懒汉模式的实现是线程不安全的.
线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 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;
}
}
以下代码在加锁的基础上, 做出了进一步改动:
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 挡住了. 也就不会继续创建其他实例.
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列能是一种线程安全的数据结构, 并且具有以下特性:
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型.
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
(降低耦合性,提高内聚性)
阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力.
比如在“秒杀”这一场景下,服务器面临巨大数量的请求,这样做可以有效进行 “削峰”, 防止服务器被突然到来的一波请求直接冲垮.
阻塞队列也能使生产者和消费者之间 解耦. (降低了两个服务器之间的耦合性(关联性))
在 Java 标准库中内置了阻塞队列. 如果我们需要在一些程序中使用阻塞队列, 直接使用标准库中的即可.
put
方法用于阻塞式的入队列, take
用于阻塞式的出队列.BlockingQueue<String> queue = new LinkedBlockingQueue<>();
// 入队列
queue.put("abc");
// 出队列. 如果没有 put 直接 take, 就会阻塞.
String elem = queue.take();
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;
}
}
}
这里的生产者消费者模型我就用上面自己模拟实现的阻塞式队列实现,顺便测试了模拟实现的阻塞式队列是否成功
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个数字空间),然后由消费者线程再次消费,消耗一个即可让阻塞队列不满,进而继续生产数字 :