Java设计模式-单例模式

Java设计模式-单例模式

定义:

确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

应用场景:

确保某个类只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有一个。例如,创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源。

实现单例模式的关键点:

  1. 构造函数不对外开放,一般为private
  2. 通过一个静态方法或者枚举返回单例类对象
  3. 确保单例类的对象有且只有一个,尤其是在多线程的环境下
  4. 确保单例类对象在反序列化时不会重新构建对象

单例模式的简单示例

//普通员工
public class Staff {
    public void work(){
        //干活
    }
}
//CEO,饿汉单例模式
public class CEO  extends Staff{
    private static final CEO ceo = new CEO();
    //构造函数私有
    private CEO(){

    }
    //公有的静态方法,对外暴露获取单例对象的接口
    public static CEO getCEO(){
        return ceo;
    }

    public void work(){
        //管理VP
    }
}
//副总裁
public class VP extends Staff{
    public void work(){
        //管理下面的经理
    }
}
//公司类
public class Company {
    private List<Staff> allStaffs = new ArrayList<>();

    public void addStaff(Staff staff){
        allStaffs.add(staff);
    }

    public void showAllStaffs(){
        for (Staff s :
                allStaffs) {
            System.out.println("Obj :"+s.toString());
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Company company = new Company();
        //CEO对象只能通过getCEO函数获取
        Staff ceo1 = CEO.getCEO();
        Staff ceo2 = CEO.getCEO();
        company.addStaff(ceo1);
        company.addStaff(ceo2);
        //通过new创建VP对象
        Staff vp1 = new VP();
        Staff vp2 = new VP();
        company.addStaff(vp1);
        company.addStaff(vp2);
        //通过new创建Staff对象
        Staff staff1 = new Staff();
        Staff staff2 = new Staff();
        Staff staff3 = new Staff();
        company.addStaff(staff1);
        company.addStaff(staff2);
        company.addStaff(staff3);
        company.showAllStaffs();
    }
}

从上述的代码可以看到,CEO类不能通过new的形式构造对象,只能通过CEO.getCEO()函数来获取,而这个CEO对象是静态对象,并且在声明的时候就已经初始化,这就保证了CEO对象的唯一性。

单例模式的其他实现方式

1. 懒汉模式

懒汉模式是声明一个静态对象,并在用户第一次调用getInstance()时进行初始化,而上述的饿汉模式(CEO类)是在声明静态对象时就已经初始化。实现如下:

public class Singleton {
    private static final Singleton instance;
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

getInstance()方法添加了synchronized关键字,也就是getInstance是一个同步方法,这就是上面所说的在多线程情况下保证单例对象的唯一性的手段。但是这样就会有一个问题,即使instance已经被初始化,每次调用getInstance方法都会进行同步,这样会消耗不必要的资源,这也是懒汉单例模式存在的最大问题。

2. Double Check Lock(DCL)实现单例

public class Singleton {
    private static Singleton instance = null;

    private Singleton(){

    }

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

getInstance进行了两次判空,第一次判空是为了避免不必要的同步,第二次判空则是为了在null的情况下创建实例。

假设线程A执行到instance=new Singleton()语句,这里看起来是一句代码,但实际上它并不是一个原子操作,这句代码最终会被编译成多条汇编指令,它大致做了三件事情:

  1. 给Singleton的实例分配内存
  2. 调用Singleton()的构造函数,初始化成员字段
  3. 将instance对象指向分配的内存空间(此时instance就不是null了)

注意:由于Java编译器允许处理器乱序执行,以及JDK1.5之前JMM(Java Memory Model,即Java内存模型)中Cache,寄存器到主内存回写顺序的规定,上面的第二和第三的顺序是无法保证的。也就是说,执行顺序可能是1-2-3或者1-3-2.如果是后者,并且在3执行完毕,2未执行之前,被切换到B线程上,这时候instance因为已经在线程A内执行了第三步,instance已经是非空了,所以,线程B直接取走instance,再使用时就会出错,这就是DCL失效问题,而且这种难以跟踪难以重视的错误很可能会隐藏很久。

DCL的优点:资源利用率高,第一次执行getInstance时单例对象才会被实例化,效率高。缺点:第一次加载反应稍慢,也由于Java内存模型的原因偶尔会失败。在高并发环境下也有一定的缺陷,虽然发生概率很小。DCL模式是使用最多的单例实现方式,它能够在需要时才实例化单例对象,并且能够在绝大多数场景下保证单例对象的唯一性。

3. 静态内部类单例模式

DCL虽然在一定程度上解决了资源消耗,多余的同步,线程安全问题,但是,它还是在某些情况下会出现失效的问题。这个问题被称为双重检查锁定失效,建议使用如下代码代替。

public class Singleton{
    private Singleton(){}
    
    public static Singleton getInstance(){
        return SingletonHolder.sInstance;
    }
    
    private static class SingletonHolder{
        private static final Singleton sInstance = new Singleton();
    }
}

当第一次加载SIngleton类时并不会初始化sInstance,只有在第一次调用Singleton的getInstance方法时才会导致sInstance被初始化。因此,第一次调用getInstance方法会导致虚拟机加载SingletonHolder类,这种方式不仅能够确保线程安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化。

你可能感兴趣的:(Java)