23种设计模式之单例模式(创建型模式)
设计模式分3大类
-
创建型模式
-
结构型模式
-
行为型模式
单例模式属于创建型模式
什么是单例模式
- 确保一个类只有一个实例,并提供全局访问点
写一个单例模式需要哪些部分
- 定义一个私有的静态化实例对象
- 将构造器私有化,防止外部直接实例化
- 提供一个访问实例的静态方法
拓展:为什么要将对象和方法都定义成静态的呢?
单例模式的类只有一个对象,意味着这个对象要被所有的类成员共享,所以要将对象设为静态的;静态的方法在类加载时就会创建且只创建一次,这个方法属于类本身,不是属于某个实例,可以用类名直接调用这个方法而不需要去通过创建一个对象来调用
实现单例模式有几种写法
- 懒加载(懒汉模式)
- 饿汉模式
- 静态内部类
- 枚举
单例模式的使用场景
- 需要频繁创建和销毁的对象
- 需经常用到,但创建对象时特别耗时和耗资源
- 资源通信时可以提供一个统一的访问点,例如线程池
- 需生成唯一序列的环境时可以确保序列的唯一性
Java代码实现单例模式
懒加载(用到对象时再去创建)
public class lazyLoading {
//静态实例对象
private static lazyLoading lazyLoadingInstance;
//私有构造器
private lazyLoading(){
}
//访问实例的静态方法
public static lazyLoading getInstance(){
if (lazyLoadingInstance == null) {
lazyLoadingInstance = new lazyLoading();
}
return lazyLoadingInstance;
}
}
以上这种写法在单线程模式下是没问题的,在多线程模式下,则有可能会导致两个及多个线程同时通过if的空判断,从而导致创建多个对象。下面我们来写个测试模拟多线程的情况
public class lazyLoadingTest {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread( ()-> {
System.out.println(Thread.currentThread().getName()
+ "线程创建的实例化对象:"+lazyLoading.getInstance());
}).start();
}
}
}
程序运行的结果如下:
Thread-3线程创建的实例化对象:com.cc.singletonPattern.lazyLoading@47437153
Thread-4线程创建的实例化对象:com.cc.singletonPattern.lazyLoading@453cc24
Thread-2线程创建的实例化对象:com.cc.singletonPattern.lazyLoading@453cc24
Thread-1线程创建的实例化对象:com.cc.singletonPattern.lazyLoading@701957ad
Thread-0线程创建的实例化对象:com.cc.singletonPattern.lazyLoading@47437153
显然,创建的不是同一个对象
解决多线程并发我们一般采取的是加锁,那我们先尝试一下加synchronized锁
锁加在哪里呢?
- 给静态方法加synchronized
public static synchronized lazyLoading getInstance(){
if (lazyLoadingInstance == null) {
lazyLoadingInstance = new lazyLoading();
}
return lazyLoadingInstance;
}
这种方式会导致每次调用这个方法都需要同步,而我们只需要在第一次创建方法时同步即可,因为后面再调用时对象已经不是null了,不需要加锁了。
- 给创建对象的语句加synchronized
public static lazyLoading getInstance(){
if (lazyLoadingInstance == null) {
synchronized (lazyLoading.class){
if (lazyLoadingInstance == null) {
lazyLoadingInstance = new lazyLoading();
}
}
}
return lazyLoadingInstance;
}
为什么不直接加锁再判断呢?加锁之后为什么要再进行一次null的判断呢?
直接加锁其实和在方法上加锁效果一样,多次调用时判断不成立后面的代码就不用执行了。
当两个或多个线程同时都通过了第一个非空判断,第1个线程拿到锁创建对象完成;另一个线程拿到锁又会创建对象,如果这时我们加上了再次判断的语句就直接结束了。
经过上面的操作,我们以为很完美了,实际上,大佬告诉我们,new对象那句代码在多线程并发的情况下依然是有风险的,什么风险呢?
指令重排
lazyLoadingInstance = new lazyLoading();
上面的代码会分为下面的3行伪代码
memory = allocate();//1.分配内存空间
ctorInstance(memory);//2.初始化
lazyLoadingInstance = memory;//3.将实例lazyLoadingInstance指向内存空间
要注意的是,上面的2和3可能会换顺序执行,也就是指令重排,这样就会导致实例对象都没有初始化就先指向内存空间了
那怎么办?
加volatile关键字
既然实例对象可能会指令重排,那我们在定义的时候就给他加上
private volatile static lazyLoading lazyLoadingInstance;
所以,安全的完整的懒加载代码如下:
public class lazyLoading {
private volatile static lazyLoading lazyLoadingInstance;
private lazyLoading(){
}
public static lazyLoading getInstance(){
if (lazyLoadingInstance == null) {
synchronized (lazyLoading.class){
if (lazyLoadingInstance == null) {
lazyLoadingInstance = new lazyLoading();
}
}
}
return lazyLoadingInstance;
}
多线程测试结果:
Thread-3线程创建的实例化对象:com.cc.singletonPattern.lazyLoading@47437153
Thread-0线程创建的实例化对象:com.cc.singletonPattern.lazyLoading@47437153
Thread-2线程创建的实例化对象:com.cc.singletonPattern.lazyLoading@47437153
Thread-4线程创建的实例化对象:com.cc.singletonPattern.lazyLoading@47437153
Thread-1线程创建的实例化对象:com.cc.singletonPattern.lazyLoading@47437153
饿汉(定义实例对象时就创建对象)
public class hungryMan {
private static hungryMan hungryManInstance = new hungryMan();
private hungryMan(){
}
public static hungryMan getInstance(){
return hungryManInstance;
}
}
同时我们可以把下面这句代码
private static hungryMan hungryManInstance = new hungryMan();
使用静态代码块再创建也行,都是在类初始化就会被加载,静态代码块只会加载一次所以多线程下也是安全的
private static hungryMan hungryManInstance;
static{
hungryManInstance = new hungryMan();
}
静态内部类(在内部类中创建对象,外部类通过类名调用)
public class staticInterior {
private static class staticInteriorHold{
private static staticInterior staticInteriorInstance = new staticInterior();
}
private staticInterior(){
}
public static staticInterior getInstance(){
return staticInteriorHold.staticInteriorInstance;
}
}
类加载时不一定将staticInteriorInstance初始化,只有调用getInstance方法才会实例化
staticInteriorInstance
枚举(枚举项就是单例的实例对象)
枚举类默认继承了Serializable接口,保证序列化和反序列化一致,用枚举实现单例还有一个好处是避免反射创建对象
public enum enumSingleton {
INSTANCE;
public void test(){
System.out.println("我是枚举类里的一个普通方法");
}
public static void main(String[] args) {
enumSingleton.INSTANCE.test();//结果输出为: 我是枚举类里的一个普通方法
}
}
建对象
```java
public enum enumSingleton {
INSTANCE;
public void test(){
System.out.println("我是枚举类里的一个普通方法");
}
public static void main(String[] args) {
enumSingleton.INSTANCE.test();//结果输出为: 我是枚举类里的一个普通方法
}
}