【设计模式】1. 单例模式

设计模式学习笔记


版权声明:本文由 Hov 所有,发布于 http://chenhy.com ,转载请注明出处。



1 单例模式

1.1 简介

单例模式是一种基本的常用的软件设计模式。单例模式的核心是确保一个类最多只有一个实例,并提供一个全局访问点。

1.2 思路

一个类只能返回对象的一个引用(始终为同一个)和一个获得此实例的方法(静态方法,通常用 getInstance 命名)。当我们调用 getInstance 方法,如果类保持的引用不为空,则返回此引用;如果类保持的引用为空,则创建该类的实例,并将实例的引用赋予该类保持的引用。
同时,我们还将该类的构造函数定义为私有方法,这样其它地方的代码就无法通过该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法得到该类的唯一实例。

1.3 使用场景

  • 需要频繁实例化然后销毁的对象;
  • 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
  • 有状态的工具类对象;
  • 频繁访问数据库或文件的对象;

1.4 优缺点

优点:

  1. 在内存中只有一个对象,节省内存空间;
  2. 避免频繁创建销毁对象,可提高性能;
  3. 避免对共享资源的多重占用;
  4. 可以全局访问。

缺点:

  1. 多线程情况下,应注意线程安全问题;
  2. 只能通过单例类提供的方法创建单例对象,不能使用反射;


2 经典实现

这是最简单的最经典的单例模式实现方式,又称为懒汉式(线程不安全)。由于该方式存在不少弊端,通常不采用。但是,我们还是有必要了解,因为它是后面几个优化方式的基础。

注:

  • 懒汉式:指全局的单例实例在第一次使用时被构建;
  • 饿汉式:指全局的单例实例在类装载时就被构建。
/* Singleton.java */

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


3 优化

单例模式的经典实现方式存在不少弊端,比如多线程情况下,两个线程同时调用 getInstance 方法,这样将创建两个实例。

下面提出4种优化的单例模式实现方法,它们各有优缺点,应视项目实际情况作出选择。

  1. 同步锁
  2. “急切”创建实例(饿汉式)
  3. 双重检查加锁
  4. 静态内部类(登记式)

3.1 同步锁

通过 synchronized 对 getInstance() 方法进行加锁,这样 getInstance() 方法只有在一个线程执行完毕后才能被另一个线程执行,从而保证线程安全。

缺点:同步锁是比较耗费资源的操作,如果我们要频繁调用 getInstance() 方法,那效率是比较糟糕的。因为我们在大多数情况下并不需要同步操作。

/* Singleton.java */

public class Singleton {
    private static Singleton uniqeInstance = null;
    
    private Singleton() {
        
    };
    
    public static synchronized Singleton getInstance() {        // synchronized 同步锁
        if(uniqeInstance == null) {
            uniqeInstance = new Singleton();
        }
        return uniqeInstance;
    }
}

3.2 “急切”创建实例(饿汉式)

饿汉式避免了多线程同步问题,因为 uniqeInstance 在类装载的时候就被实例化了,所以就算是两个线程同时执行,在类里面早就存在 uniqeInstance 对象了,自然不会发生线程安全问题。

缺点:因为饿汉式在类装载时就初始化实例,但如果我们的程序从头到尾都不需要用到该实例,这样就白白浪费了内存资源。

/* Singleton.java */

public class Singleton {
    private static Singleton uniqeInstance = new Singleton();   //类装载时将构造实例
    
    private Singleton() {
        
    };
    
    public static Singleton getInstance() {         //不用加同步锁
        return uniqeInstance;
    }
}

3.3 双重检查加锁

双重检查加锁和普通同步锁(见3.1)的方式不同,不再对 getInstance() 方法直接加锁,而是在方法里面才加锁,这样在安全且多线程的情况下保持高性能。

/* Singleton.java */

public class Singleton {
    private volatile static Singleton uniqeInstance = null; //注意volatile关键词
    
    private Singleton() {
        
    };
    
    public static Singleton getInstance() {
        if(uniqeInstance == null) {     
            synchronized (Singleton.class) {        //在方法里面才加锁
                if(uniqeInstance == null) {
                    uniqeInstance = new Singleton();
                }
            }
        }
        return uniqeInstance;
    }
}

3.4 静态内部类(登记式)

前面提到的饿汉式在类装载时,对象就被实例化,没有达到懒加载的效果。
登记式下,即使 Singleton 类被装载了, uniqeInstance 对象也不一定被实例化。因为 SingletonHolder 类并没有被主动使用,只有调用 getInstance 方法才会装载 SingletonHolder 类,从而实例化 uniqeInstance 对象。所以,登记式下不会在类装载时就直接构造实例,比饿汉式要合理一些。

public class Singleton {  
    private static class SingletonHolder {          //SingletonHolder类
        private static final Singleton uniqeInstance = new Singleton();  
    }  

    private Singleton (){

    }  

    public static final Singleton getInstance() {  //调用getInstance方法将装载SingletonHolder类
        return SingletonHolder.uniqeInstance; 
    }  
}


4 结语

前面我们提到了两种懒汉式和两种饿汉式的单例模式实现。

  • 懒汉式代码书写相对复杂,而且需要用同步锁,比较耗费资源。
  • 普通饿汉式代码书写简单,但浪费内存空间。
  • 静态内部类是对普通饿汉式的改进,较好地克服了内存空间浪费问题。


 
comments powered by Disqus