设计模式之单例模式

设计模式

1.创建型模式
因为在软件设计方面分工越来越细,因此对象的创建和对象的使用分开也就成为了必然趋势。因为对象的创建会消耗掉系统的很多资源,所以单独对对象的创建进行研究,从而能够高效地创建对象就是创建型模式要探讨的问题。这里有6个具体的创建型模式可供研究,它们分别是:

简单工厂模式(Simple Factory)
工厂方法模式(Factory Method)
抽象工厂模式(Abstract Factory)
创建者模式(Builder)
原型模式(Prototype)
单例模式(Singleton)

2.结构型模式
在解决了对象的创建问题之后,对象的组成以及对象之间的依赖关系就成了开发人员关注的焦点,因为如何设计对象的结构、继承和依赖关系会影响到后续程序的维护性、代码的健壮性、耦合性等。对象结构的设计很容易体现出设计人员水平的高低,这里有7个具体的结构型模式可供研究,它们分别是:

外观模式/门面模式(Facade门面模式)
适配器模式(Adapter)
代理模式(Proxy)
装饰模式(Decorator)
桥梁模式/桥接模式(Bridge)
组合模式(Composite)
享元模式(Flyweight)

3.行为型模式
在对象的结构和对象的创建问题都解决了之后,就剩下对象的行为问题了,如果对象的行为设计的好,那么对象的行为就会更清晰,它们之间的协作效率就会提高,这里有11个具体的行为型模式可供研究,它们分别是:

模板方法模式(Template Method)
观察者模式(Observer)
状态模式(State)
策略模式(Strategy)
职责链模式(Chain of Responsibility)
命令模式(Command)
访问者模式(Visitor)
调停者模式(Mediator)
备忘录模式(Memento)
迭代器模式(Iterator)
解释器模式(Interpreter)

设计模式之单例模式

简介:

一种常用的软件设计模式,定义是单例对象的类只能允许一个实例存在,
单例的实现主要是通过以下两个步骤:1.将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象
只有通过该类提供的静态方法来得到该类的唯一实例;
2.在该类内提供一个静态方法,当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建
该类的实例并将实例的引用赋予该类保持的引用

应用场景:

​ spring里的bean对象

​ 多线程的线程池的设计,这是由于线程池需要方便对池中的线程进行控制

单例模式的优缺点:

优点: 节省内存空间
避免频繁的创建销毁对象,可以提高性能
避免对共享资源的多重占用,简化访问
缺点:
不适用于变化频繁的对象
将数据库连接池对象设计为的单例类可能会导致共享连接池对象的程序过多而出现连接池溢出,
如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失

单例模式的实现:

饿汉式

// 饿汉式单例
public class Singleton {
 
    // 指向自己实例的私有静态引用,主动创建
    private static Singleton singleton = new Singleton();
 
    // 私有的构造方法
    private Singleton(){}
 
    // 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Singleton getSingleton(){
        return singleton;
    }
}

​ 我们知道,类加载的方式是按需加载,且加载一次。。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例。

优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费

懒汉式单例

// 懒汉式单例
public class Singleton {
 
    // 指向自己实例的私有静态引用
    private static Singleton singleton;
 
    // 私有的构造方法
    private Singleton(){}
 
    // 以自己实例为返回值的静态的公有方法,静态工厂方法
    public static Singleton getSingleton(){
        // 被动创建,在真正需要使用时才去创建
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

​ 我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。

这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

懒汉式线程安全:

为了解决上面的问题,最简单的方法是将整个 getInstance() 方法设为同步(synchronized)。

public class Singleton {
    private static Singleton singleton;
    private Singleton (){}

    public static synchronized Singleton getSingleton() {
    if (singleton == null) {
        singleton = new Singleton();
    }
    return singleton;
}
}

懒汉式:双重检验锁

双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。

public static Singleton getSingleton() {
    if (singleton == null) {                         //Single Checked
        synchronized (Singleton.class) {
            if (singleton == null) {                 //Double Checked
                singleton = new Singleton();
            }
        }
    }
    return singleton ;
}

这段代码看起来很完美,很可惜,它是有问题。主要在于singleton= new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

1给 singleton分配内存

2调用 Singleton 的构造函数来初始化成员变量

3将singleton对象指向分配的内存空间(执行完这步 singleton就为非 null 了)

但是在JVM的即时编译器中存在指令重排序的优化。 
也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 singleton已经是非 null 了(但却没有初始化),所以线程二会直接返回 singleton,然后使用,然后顺理成章地报错。
  
再稍微解释一下,就是说,由于有一个『singleton已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (singleton==null)这里,这里读取到的singleton已经不为null了,所以就直接把这个中间状态的singleton拿去用了,就会产生问题。这里的关键在于线程T1对singleton的写操作没有完成,线程T2就执行了读操作。

懒汉式:指令重排序

我们只需要将 instance 变量声明成 volatile 就可以了。

public class Singleton {
    private volatile static Singleton singleton; //声明成 volatile
    private Singleton (){}

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

使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。

如何防止反射、克隆、序列化对单例模式的破环

1、防止反射破环(虽然构造方法已私有化,但通过反射机制使用newInstance()方法构造方法也是可以被调用)

​ 1.1解决反射破坏,可以在构造器里添加判断条件,判断实例是否为空,不为空抛出异常

if (singleton != null) {       
                   throw new RuntimeException("你居然敢破坏我的单例.....");
                }

在单例模式的构造器中使用计数的方式确保只生产一个对象,因为反射也是调用构造器创建对象。

/**
 * 双重检测锁的单例模式
 */
class DoubleIfSynchronizedSingleton {
    private static int count = 0;
    private volatile static DoubleIfSynchronizedSingleton singleton = null;
 
    private DoubleIfSynchronizedSingleton() {
        synchronized (DoubleIfSynchronizedSingleton.class) {
            if (count > 0){
                throw new RuntimeException("你居然敢破坏我的单例.....");
            }
            count++;
        }
    }
 
    public static DoubleIfSynchronizedSingleton getSingleton() {
        if (singleton == null) {
            synchronized (DoubleIfSynchronizedSingleton.class) {
                if (singleton == null) {
                    singleton = new DoubleIfSynchronizedSingleton();
                }
            }
        }
        return singleton;
    }
}

2、防止克隆破环

  • 重写clone(),直接返回单例对象

     @Override
        protected Singleton clone() throws CloneNotSupportedException {
            return singleton;
        }
        
    

3、防止序列化破环

  • 添加readResolve(),返回Object对象

    private Object readResolve() {
            return singleton;
        }
    

静态内部类 static nested class

我比较倾向于使用静态内部类的方法,这种方法也是《Effective Java》上所推荐的。

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

这种写法仍然使用JVM本身机制保证了线程安全问题;由于 SingletonHolder 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。

不能防止反射,

枚举 Enum

用枚举写单例实在太简单了!这也是它最大的优点。下面这段代码就是声明枚举实例的通常做法。

public enum EasySingleton{
   SINGLETON;
}

线程安全,所以不需要担心double checked locking

防止序列化,

防止反射

我们可以通过EasySingleton.SINGLETON来访问实例,这比调用getSingleton()方法简单多了。

结语

当然,单例模式的实现方法还有很多。从这几种实现中,我们可以总结出,要想实现效率高的线程安全的单例,我们必须注意以下两点:

  • 尽量减少同步块的作用域;
  • 尽量使用细粒度的锁。
上一篇:设计模式——单例模式


下一篇:单例模式