创建型模式之--单例模式

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

优点:

  1. 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销,可以使用单例模式,只创建一个对象,一直保留在内存中。
  2. 降低了系统内存的使用频率,减轻GC压力。
  3. 可以避免对资源的多重占用,如 Print Spooler服务,管理所有本地和网络打印队列及控制所有打印工作,只能有一个实例,避免两个打印作业同时输出到打印机中,出现错乱。
  4. 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如设置一个单例类,负责向前端控制台打印消息。

缺点:

  1. 一般没有接口,扩展困难。因为它要求 自行实例化,且提供单一实例,接口或抽象类是不可能被实例化的。
  2. 对测试不利。在并行开发中,如果单例模式没有完成,是不能进行测试的。

使用场景:

  • 要求生产唯一序列号的环境;
  • 在整个项目中需要一个共享访问点或共享数据,例如一个Web页面上的计数器,使用单例模式保持计数器的值,并保证线程安全;
  • 创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源;
  • 需要定义大量的静态常量和静态方法(如工具类)的环境,可以采用单例模式(也可以直接声明为static的方式)。

在Spring中,每个Bean 默认就是单例模式,这样做的优点是Spring容器可以管理这些Bean的声明周期,决定什么时候创建出来,什么时候消耗,消耗的时候做什么处理。

一、懒汉式单例

//懒汉式单例类.在第一次调用的时候才实例化自己   
public class Singleton {  
    private static Singleton instance=null; 

    private Singleton() { //私有构造方法,避免被其他类new出来一个对象  
    }

    //静态工厂方法   
    public static Singleton getInstance() {  
         if (instance == null) {    
             instance = new Singleton();  //自己可以调用自己的构造方法,new 一个对象,调用的时候才new,实现延迟加载
         }    
        return instance;  
    }  
} 

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。

它是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式:

1、getInstance() 加锁

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

由于getInstance()是静态方法,所以synchronized 关键字锁住的是当前类本身。
存在问题:
性能不佳,因为每次调用getInstance(),都要给当前类上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。

2、双重检查锁定

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

将 synchronized 关键字加在了内部,使用静态代码块,只有首次调用, instance 为 null,需要创建对象的时候才需要加锁,之后每次调用的时候不需要加锁,性能有一定的提升。

为什么需要两个 if (instance == null) ?
第一个判断 if (instance == null),是为了提高效率,只有首次调用getInstance() 对象为null 的时候才加锁,后面再次调用不用运行同步代码块,直接返回instance;
第二个判断if (instance == null),是为了线程安全,首次调用getInstance() 时,单例对象还没有创建,假如两个线程A、B,都进入了同步代码块,依次instance = new Singleton(); 并返回,这样就创建了两个对象。所以还需要在同步代码块中增加if (instance == null)判断。

存在问题:
线程不安全。
java对象的创建分为两个过程:

  1. 分配内存空间来存放对象自己的实例变量及其从父类继承过来的实例变量;这些实例变量会被赋予默认值(零值)。
  2. 对新创建的对象按照程序猿的意志进行初始化。在Java对象初始化过程中,主要涉及三种执行对象初始化的结构,分别是 实例变量初始化、实例代码块初始化 以及 构造函数初始化。

对于 instance = new Singleton(); 这条指令需求完成的动作:分配内存,初始化对象,将内存地址赋给 instance 变量。

由于指令重排优化的存在,导致初始化堆内存中的 Singleton() 对象和将对象地址赋给 instance 字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给 instance 字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用 getInstance,判断instance!=null,返回,但是取到的其实是没有初始化的对象(实例变量还是默认值,而不是初始值),程序就会出错。

指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。

改进:
JDK1.5及之后版本增加了volatile关键字。volatile的一个语义是禁止指令重排序优化(即必须初始化好堆内存后,才将地址赋值给 instance 字段),也就保证了instance 变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。

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

3、静态内部类(最优)

public class Singleton{  
    private Singleton(){}  

    private static class SingletonHolder{  
        private static Singleton instance = new Singleton();  
    } 
    public static Singleton newInstance(){  
        return SingletonHolder.instance;  
    }  
}  

它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载线程安全

二、饿汉式单例

public class Singleton{  
    private static Singleton instance = new Singleton();  
    private Singleton(){
    }  
    public static Singleton newInstance(){  
        return instance;  
    }  
}

类的构造函数定义为private的,保证其他类不能实例化此类,然后提供了一个静态实例并返回给调用者。
它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。在第一次调用时速度也会更快,因为其资源已经初始化完成。
它的缺点也很明显,即使这个单例没有用到也会被创建,并实例在整个程序周期都存在,内存就被浪费了。

三、有上限的多例模式

它是单例模式的一种扩展,采用有上限的多例模式,可以在设计时决定在内存中有多少个实例,方便系统进行扩展,修正单例可能存在的性能问题,提供系统的响应速度。

例子:一个朝代有两个皇帝,Emperor类有且仅有两个实例:

public class Emperor{
    //  最多可以产生多少个实例对象
    private static int maxNumOfEmperor=2;
    //  每个皇帝都有名字,使用ArrayList容纳,每个对象的私有属性
    private static ArrayList<String> nameList=new ArrayList<String>();
    //  定义一个列表,容纳所有皇帝实例
    private static ArrayList<Emperor> emperorList=new ArrayList<Emperor>();
    //  当前皇帝序号
    private static int countNumOfEmperor=0;

    //类加载初始化时,产生所有对象,天生线程安全
    static{
        for(int i=0;i<maxNumOfEmperor;i++){
            emperorList.add(new Emperor("皇"+(i+1)+"帝"));
        }
    }

    //私有构造方法,不让外界对象再产生类的实例
    private Emperor(){

    }
    private Emperor(String name){
        nameList.add(name);
    }

    //随机获取一个皇帝对象,返回
    public static Emperor getInstance(){
        Random random=new Random();
        countNumOfEmperor=random.nextInt(maxNumOfEmperor);
        return emperorList.get(countNumOfEmperor);
    }

    //皇帝的方法
    public static void say(){
        System.out.println(nameList.get(countNumOfEmperor));
    }

}

臣子参拜皇帝:

public class Minister {
    public static void main(String[] args) {
        //定义5个大臣
        int ministerNum=5;
        for(int i=0;i<ministerNum;i++){
            Emperor emperor=Emperor.getInstance();
            System.out.print("第"+(i+1)+"个大臣参拜");
            emperor.say();
        }
    }
}
第1个大臣参拜皇1帝
第2个大臣参拜皇2帝
第3个大臣参拜皇2帝
第4个大臣参拜皇1帝
第5个大臣参拜皇2帝

类加载初始化时,产生所有对象,相当于饿汉模式,天生线程安全。但对ArrayList 的访问非线程安全,可以用 Vector代替。

四、破坏单例的情况

1、反射
2、序列化,反序列化

/* 如果该对象被用于序列化,可以保证对象在序列化前后保持一致 */
 public Object readResolve() {
     return getInstance();
 }

参考资料:
【1】《设计模式之禅》-秦小波
【2】尚硅谷 java之23种设计模式解析-宋红康
【3】https://blog.csdn.net/justloveyou_/article/details/72466416#t3
【4】https://blog.csdn.net/goodlixueyong/article/details/51935526

猜你喜欢

转载自blog.csdn.net/zxm1306192988/article/details/80386392