设计模式之单例模式

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。

1、懒汉式单例,非线程安全

这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程,所以严格意义上它并不算单例模式。

该实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例。

/**
 * 懒汉式单例,在第一次调用的时候进行实例化
 * @author wangbo
 */
public class Singleton {
    
    //静态实例私有化,防止在外部被引用
    private static Singleton singleton = null;
    
    //构造方法私有化,防止在外部被实例化
    private Singleton(){}
    
    //提供公开的静态方法,用来在外部获取实例
    public static Singleton getInstance(){
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
    
}

2、懒汉式单例,单一同步锁

在getInstance()方法上加同步锁,synchronized关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了。

/**
 * 懒汉式单例,在第一次调用的时候进行实例化
 * @author wangbo
 */
public class Singleton {
    
    //静态实例私有化,防止在外部被引用
    private static Singleton singleton = null;
    
    //构造方法私有化,防止在外部被实例化
    private Singleton(){}
    
    //提供公开的静态方法,用来在外部获取实例
    public static synchronized Singleton getInstance(){
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
    
}

3、懒汉式单例,双检锁/双重校验锁

这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

在getInstance()方法内部加同步锁,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在 singleton 为null,并创建对象的时候才需要加锁,性能有一定的提升。

但是,这种方式还是有可能出问题的,考虑这种情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说 singleton = new Singleton() 语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给 singleton 成员变量,然后再去初始化这个Singleton实例。如果多个线程的话,可能某个线程就会获取到为null的实例,这样就出错了。

/**
 * 懒汉式单例,在第一次调用的时候进行实例化
 * @author wangbo
 */
public class Singleton {
    
    //静态实例私有化,防止在外部被引用
    private static Singleton singleton = null;
    
    //构造方法私有化,防止在外部被实例化
    private Singleton(){}
    
    //提供公开的静态方法,用来在外部获取实例
    public static Singleton getInstance(){
        if (singleton == null) {//第一重校验
            synchronized(Singleton.class){//锁定
                if (singleton == null) {//第二重校验
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    
}

4、懒汉式单例,登记式/静态内部类

这种比上面两种方式都好一些,既实现了线程安全,又避免了同步带来的性能影响。

这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。

/**
 * 懒汉式单例,在第一次调用的时候进行实例化
 * @author wangbo
 */
public class Singleton {
    
    //构造方法私有化,防止在外部被实例化
    private Singleton(){}
    
    //使用静态内部类维护单例
    private static class SingletonFactory{
        private static Singleton singleton = new Singleton();
    }
    
    //提供公开的静态方法,用来在外部获取实例
    public static Singleton getInstance(){
        return SingletonFactory.singleton;
    }
    
}

静态内部类实现方式与饿汉式单例的区别:

两种方式同样利用了 classloder 机制来保证初始化 instance 时只有一个线程,不同的是:饿汉式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到延迟加载的效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 静态内部类 SingletonFactory 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonFactory 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比饿汉式就显得更合理。

5、饿汉式单例,线程安全

饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

这种方式比较常用,但容易产生垃圾对象。类加载时就初始化,浪费内存。

该方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到延迟加载的效果。

/**
 * 饿汉式单例,在类初始化的时候进行实例化
 * @author wangbo
 */
public class Singleton {
    
    //使用静态内部类维护单例(直接初始化)
    private static final Singleton singleton = new Singleton();
    
    //构造方法私有化,防止在外部被实例化
    private Singleton(){}
    
    //提供公开的静态方法,用来在外部获取实例
    public static Singleton getInstance(){
        return singleton;
    }
    
}

6、枚举实现单例

单例的枚举实现在《Effective Java》中有提到,因为其功能完整、使用简洁、无偿地提供了序列化机制、在面对复杂的序列化或者反射攻击时仍然可以绝对防止多次实例化等优点,单元素的枚举类型被作者认为是实现Singleton的最佳方法。

public enum Singleton {
    
    INSTANCE;
    
    private Singleton(){}

}

下面我们用一个枚举实现单个数据源例子来简单验证一下:

声明一个枚举,用于获取数据库连接

public enum DataSourceEnum {
    
    DATASOURCE;
    
    private DBConnection connection = null;

    private DataSourceEnum() {
        connection = new DBConnection();
    }

    public DBConnection getConnection() {
        return connection;
    }
    
}

模拟一个数据库连接类

public class DBConnection {

}

测试通过枚举获取的示例是否相同

public static void main(String[] args) {
    DBConnection con1 = DataSourceEnum.DATASOURCE.getConnection();
    DBConnection con2 = DataSourceEnum.DATASOURCE.getConnection();
    System.out.println(con1 == con2);
}

输出结果为:true  结果表明两次获取返回了相同的实例。

此处参考博客:https://blog.csdn.net/gavin_dyson/article/details/70832185

注意:

一般情况下,不建议使用第 1 种和第 2 种懒汉式单例实现方式,建议使用饿汉式单例实现方式。只有在要明确实现延迟加载效果时,才会使用静态内部类方式。如果涉及到反序列化创建对象时,可以尝试使用枚举方式。如果有其他特殊的需求,可以考虑使用双检锁方式。

懒汉式和饿汉式的区别:

  1)线程安全方面:

饿汉式在类加载时直接创建静态实例,天生就是线程安全的,可以直接用于多线程而不会出现问题。

懒汉式第一次需要时才创建一个实例,本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的2、3、4,这三种实现在资源加载和性能方面有些区别。

  2)资源加载和性能:

饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成。

而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

至于 2、3、4 这三种实现又有些区别:

第2种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的。

第3种,在 getInstance 中做了两次 null 检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗。

第4种,利用了 classloader 的机制来保证初始化 instance 时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以这一种是相对最好的。

posted @ 2018-04-17 19:24  一线大码  Views(176)  Comments(0Edit  收藏  举报