23种设计模式之单例模式(创建型模式)

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();//结果输出为: 我是枚举类里的一个普通方法
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值