目录
一、单例模式
单例模式是常见的设计模式之一。
什么是设计模式?
设计模式,就相当于“棋谱"中一些固定的代码套路,按照棋谱来下,一般就不会下的很差。软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏。
单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例.
这一点在很多场景上都需要. 比如 JDBC
中的 DataSource
实例就只需要一个。
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种。
这里举一个例子:洗碗
- 中午这顿饭,使用了4个碗,吃完之后,立即把这4个碗给洗了 [饿汉]
- 中午这顿饭,使用了4个碗.吃完之后,先不洗,晚上这顿,只需要2个碗,然后就只洗2个即可 [懒汉]―>是一种更加高效的操作
饿汉的单例模式,是比较着急的去进行创建实例的.
懒汉的单例模式,是不太着急的去创建实例,只是在用的时候才真正创建.
1.1 饿汉模式
类加载的同时,创建实例。
一个Java程序中,一个类对象只存在一份(JVM
保证的)进—步的也就保证了类的static
成员也是只有一份的。
//用过Singleton来实现单例模式,保证Singleton这个类有唯一实例
//饿汉模式
class Singleton{
//static修饰的成员---“类成员”-》“类属性/方法”
//1.使用static来创建一个实例,并且立即进行实例化
//这个instance对应的实例,就是该类的唯一实例
private static Singleton instance = new Singleton();
//2.为了防止在其他地方new这个Singleton,就可以把这个Singleton设为私有的
private Singleton(){
};
//构造一个方法,让外面能够拿到唯一实例
public static Singleton getInstance(){
return instance;
}
}
public class Test06 {
public static void main(String[] args) {
Singleton instance = Singleton.getInstance();
}
}
饿汉模式中getlnstance
,仅仅是读取了变量的内容。如果多个线程只是读同一个变量,不修改,此时仍然是线程安全的。
1.2 懒汉模式
类加载的时候不创建实例. 第一次使用的时候才创建实例。
- 懒汉模式-单线程版
class Singleton1{
private static Singleton1 in = null;
private Singleton1(){
};
public static Singleton1 getInstance(){
//不是原子的,既包含读,又包含修改
if(in == null){
in = new Singleton1();
}
return in;
}
}
懒汉模式中,既包含了读,又包含了修改.而且这里的读和修改,还是分成两个步骤的(不是原子的)存在线程安全问题。
- 懒汉模式-多线程版
线程安全问题发生在首次创建实例时. 如果在多个线程中同时调用 getInstance
方法, 就可能导致创建出多个实例.
加锁操作,可以改变这里的线程安全问题。使用这里的类对象作为锁对象(类对象在一个程序中只有唯一一份,就能保证多个线程调用getInstance
的时候都是针对同一个对象进行的加锁)。
class Singleton1{
private static Singleton1 instance= null;
private Singleton1(){
};
public static Singleton1 getInstance(){
synchronized (Singleton1.class){
if(instance== null){
instance= new Singleton1();
}
}
return instance;
}
}
- 懒汉模式-多线程版(改进)
当前虽然加锁之后,线程安全问题得到解决了,但是又有了新的问题 :对于刚才这个懒汉模式的代码来说。线程不安全是发生在instance
被初始化之前的.未初始化的时候,多线程调用getinstance
,就可能同时涉及到读和修改.但是一旦instance
被初始化之后(一定不是nul
, if
条件一定不成立了),getInstance
操作就只剩下两个读操作也就线程安全了。
而按照上述的加锁方式,无论代码是初始化之后,还是初始化之前,每次调用 getinstance
方法都会进行加锁.也就意味着即使是初始化之后(已经线程安全了),仍然存在大量的锁竞争。
以下代码在加锁的基础上, 做出了进一步改动:
- 使用双重
if
判定, 降低锁竞争的频率。
改进方案: 让getInstance
初始化之前,才进行加锁,初始化之后,就不再加锁了。在加锁这里再加上一层条件判定即可.条件就是当前是否已经初始化完成 (instance == null
)。
在使用了双重if
判定之后,当前这个代码中还存在一个重要的问题:如果多个线程,都去调用这里的getlnstance
方法,就会造成大量的读instance
内存的操作,这样可能会让编译器把这个读内存操作优化成读寄存器操作。
—旦这里触发了优化,后续如果第一个线程已经完成了针对instance
的修改,那么紧接着后面的线程都感知不到这个修改,仍然把 instance
当成null
。所以这里需要给 instance 加上了 volatile。
- 给 instance 加上了 volatile
class Singleton2{
//不是立即初始化实例
//volatile 保证内存可见性
private static volatile Singleton2 instance = null;
private Singleton2(){
};
//只有在真正使用这个实例的时候,才会真正的去创建这个实例
public static Singleton2 getInstance(){
//使用这里的类对象作为锁对象,类对象在一个程序中只有一份,
//判定的是是否要加锁。降低了锁竞争
if(instance == null){
//加锁操作,保证了线程安全
synchronized (Singleton2.class){
//判定的是是否要创建实例
if(instance == null){
instance = new Singleton2();
}
}
}
return instance;
}
}
public class Test07 {
public static void main(String[] args) {
Singleton2 instance = Singleton2.getInstance();
}
}
二、阻塞式队列
阻塞队列是什么?
阻塞队列是一种特殊的队列. 也遵守 “先进先出” 的原则.
阻塞队列是一种线程安全的数据结构, 并且具有以下特性 : 产生阻塞效果
- 当队列满的时候, 继续入队列就会阻塞, 直到有其他线程从队列中取走元素.
- 当队列空的时候, 继续出队列也会阻塞, 直到有其他线程往队列中插入元素.
阻塞队列的一个典型应用场景就是 “生产者消费者模型”. 这是一种非常典型的开发模型。
2.1 生产者消费者模型
生产者消费者模式就是通过一个容器来解决生产者和消费者的强耦合问题。
生产者和消费者彼此之间不直接通讯,而通过阻塞队列来进行通讯,所以生产者生产完数据之后不用等待消费者处理,直接扔给阻塞队列,消费者不找生产者要数据,而是直接从阻塞队列里取.
- 阻塞队列也能使生产者和消费者之间解耦
生产者消费者模型,是实际开发中非常有用的一种多线程开发手段。尤其是在服务器开发的场景中:
假设有两个服务器AB
,A作为入口服务器直接接收用户的网络请求,B作为应用服务器,来给A提供一些数据。
如果不使用生产者消费者模型。此时A和B的耦合性是比较强的:在开发A代码的时候就得充分了解到B提供的一些接口;开发B代码的时候也得充分了解到A是怎么调用的;—旦想把B换成C,A的代码就需要较大的改动,而且如果B挂了,也可能直接导致A也顺带挂了。
使用生产者消费者模型,就可以降低这里的耦合.
对于请求:A是生产者,B是消费者.对于响应:A是消费者,B是生产者.阻塞队列都是作为交易场所 ,阻塞队列就相当于一个缓冲区,平衡了生产者和消费者的处理能力。
A只需要关注如何和队列交互,不需要认识B;
B也只需要关注如何和队列交互,也不需要认识A;
队列是不变的,如果B挂了,对于A没啥影响;如果把B换成C,A也完全感知不到。
- 能够对于请求进行“削峰填谷”
未使用生产者消费者模型的时候,如果请求量突然暴涨(不可控)