# 单例模式

在有些系统中,为了节省内存资源、保证数据内容的一致性,对某些类要求只能创建一个实例。

单例模式:一个类只有一个实例,且该类能自行创建这个实例的一种模式。

在计算机系统中,windows的回收站、操作系统中的文件系统、多线程中的线程池、显卡的驱动程序、网站的计数器、系统中的缓存常常被设计成单例模式。

单例模式的三个特点

  • 单例类只有一个实例对象
  • 该单例对象必须由单例类自行创建
  • 单例类对外提供一个访问该单例的全局访问点

优点:

  • 单例模式可以保证内存只有一个实例,减少内存开销
  • 可以避免对资源的多重占用
  • 单例模式设置全局访问点,可以优化和共享资源的访问

缺点:

  • 单例模式一般没有接口,扩展困难
  • 在并发测试中,单例模式不利于代码调试
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则

适用场景:

  • 需要频繁创建的一些类,使用单例可以降低系统的内存压力,减少GC
  • 某类中只要生成一个对象的时候,如一个班的班长、每个人的身份证号
  • 某类需要频繁实例化,而创建的对象又频繁的被销毁时,如线程池、网络连接池
  • 频繁访问文件或数据库的对象
  • 对于一些控制硬件级别的操作
  • 当对象需要被共享的场合,如web中的配置对象、数据库连接池

实现单例模式的思路

  1. 构造私有 2.以静态方法返回实例 3.确保对象实例只有一个
//饿汉式
public class SingletonTest {

    private static final SingletonTest singleton = new SingletonTest();
    
    private SingletonTest(){}
    
    public static SingletonTest getInstance(){
        return singleton;
    }

}

因为提前创建好了实例,所以容易造成资源的浪费

//懒汉式
public class SingletonTest1 {

    private static SingletonTest1 singleton = null;

    private SingletonTest1(){

    }

    public static SingletonTest1 getInstance(){
        synchronized (SingletonTest1.class){
            if(singleton==null){
                singleton = new SingletonTest1();
            }
        }
        return singleton;
    }

}

懒汉模式解决了饿汉式可能引发的资源浪费问题,因为这种模式只有在用户要使用的时候才会实例化对象。但是这种模式在并发情况下会出现创建多个对象的情况,故需加锁。但加锁又影响性能。

//双重检查加锁
public class SingletonTest2 {
    
    private static SingletonTest2 singleton = null;
    
    private SingletonTest2(){}
    
    public static SingletonTest2 getInstance(){
        if(singleton==null){
            synchronized (SingletonTest2.class){
                if(singleton==null){
                    singleton = new SingletonTest2();
                }
            }
        }
        return singleton;
    }
    
}

为什么要判断两次singleton==null呢???

第一次检测:

由于单例模式只需要创建一次实例,如果后面再次调用getInstance方法时,则直接返回之前创建的实例,因此大部分时间不需要执行同步方法里面的代码,大大提高了性能。如果不加第一次校验的话,每次都要去竞争锁。

第二次检测:

如果没有第二次校验,假设线程t1执行了第一次校验后,判断为null,这时t2也获取了CPU执行权,也执行了第一次校验,判断也为null。接下来t2获得锁,创建实例。这时t1又获得CPU执行权,由于之前已经进行了第一次校验,结果为null(不会再次判断),获得锁后,直接创建实例。结果就会导致创建多个实例。所以需要在同步代码里面进行第二次校验,如果实例为空,则进行创建。

简单来说就是为了防止创建多个实例。

存在的问题:JVM的指令重排序会导致失效

双重检查加锁的优化(使用volatile修饰)

解决JVM指令重排序的问题

//双重检查加锁(优化版)
public class SingletonTest2 {
    
    private static volatile SingletonTest2 singleton;
    
    private SingletonTest2(){}
    
    public static SingletonTest2 getInstance(){
        if(singleton==null){
            synchronized (SingletonTest2.class){
                if(singleton==null){
                    singleton = new SingletonTest2();
                }
            }
        }
        return singleton;
    }
    
}

用内部类实现懒汉式

//内部类实现
public class SingletonTest3 {

    private SingletonTest3(){

    }

    private static class SingletonHolder{
        private static SingletonTest3 singleton = new SingletonTest3();
    }
    
    public static SingletonTest3 getInstance(){
        return SingletonHolder.singleton;
    }
    
}

当外部类被访问时,并不会加载内部类,只要不访问内部类,private static SingetonTest3 singleton = new SingleTest3()就不会实例化,这就相当于懒加载,只有访问内部类时才会实例化。这样既解决了饿汉式的资源浪费,又解决了懒汉式下的并发问题。

更新时间: 2022年5月4日 22:45