设计模式-单例模式(Singleton)

2018-08-24  本文已影响0人  初夏倾城

引言

这里举个例子,古代的时候,一般只会有一个皇帝,比如秦朝。可以这样理解,皇帝是一个类,秦始皇则是一个对象实例。只会有一个皇帝实例,不然就乱套了,这在代码设计的思想中,就被称之为单例模式(Singleton Pattern)。

单例模式的定义和使用场景

定义

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

单例模式的使用场景

在一个系统中,要求一个类有且仅能有一个对象,如果出现多个对象就会出现一系列不可预知的错误,就可以采用单例模式了,具体如下:

1.定义了大量静态常量和静态方法的工具类;
2.需要生成唯一序列的环境;
3.需要频繁创建然后销毁的实例;
4.频繁访问数据库或文件的对象。

具体的应用场景:
1.网站的计数器,如果不是单例的话就无法实现同步;
2.Windows的任务管理器(无法打开两个任务管理器);
3.Web应用的配置对象的读取;
4.数据库连接池;
5.Web应用配置文件的读取。

单例模式的要素

如果想让一个类只拥有一个实例对象,很简单:
1.私有的构造方法,禁止外部访问;
2.私有静态引用指向实例;
3.将自己实例当做返回值的静态共有方法。

单例模式的具体实现

单例模式,在我们平常使用中,主要有两种实现方法:饿汉式、懒汉式。

饿汉式

何为饿汉式,饿汉,饥不择食,此处同义:在加载类的时候就会创建类的单例,并保存在类中。

代码实现如下:

public class SingleTon {
    private static SingleTon instance = new SingleTon();
    private SingleTon() {

    }
    public static SingleTon getInstance() {
        return instance;
    }

    public void say() {
        System.out.println("我是皇帝秦始皇");
    }
}

因为实例对象由static修饰,所以在类加载的时候就会调用私有的构造方法,创建类的单例,保存在类中。

这样做,优点是:
1.借由JVM实现了线程安全;
2.因为在类加载时就创建了类的单例,调用速度会比较快。

缺点也很明显,因为类加载就会创建该类的单例,不管用户是否需要,可能我们永远不会用到getInstance方法,但执行该类的其他静态方法或者加载了该类(class.forName),那么这个实例仍然初始化,这样做会占用大量的内存资源。

懒汉式

何为懒,就是延时,特点就是单例在类第一次被使用的时候才会构建,延时初始化。

代码实现如下:

public class SingleTon {
    private static SingleTon instance = null;
    private SingleTon() {

    }
    public static SingleTon getInstance() {
          if (instance == null) {
              //多个线程判断instance都为null时,在执行new操作时多线程会出现重复情况
                        instance = new SingleTon();
                }
                return instance;
    }

    public void say() {
        System.out.println("我是皇帝秦始皇");
    }
}

该方法相较于饿汉式的优点就是资源利用效率高,在不执行类的静态方法getInstance的时候,类的实例就不会被创建。
该方法的缺点在于,当系统压力增大,并发量增加的时候就可能会出现多个实例,当线程A执行到instance = new SingleTon()的时候,对象的创建是需要时间的,此时线程B执行到了if(instance == null),判断条件也为真,于是继续执行下去,线程A获得了一个对象,线程B也获得了一个对象,在内存中就出现了两个对象。

单例模式的额外扩展

解决线程不安全的问题,可以对方法进行同步加锁,对getInstance()进行同步,代码实现如下:

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

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

但这样做也有缺点,每个线程在想获得类的实例时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。

也可以采用同步代码块的方式,代码如下:

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

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

但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。

为了解决这个问题,可以采取双重加锁,就是在同步代码块内部在进行一次判断,杜绝这个问题的产生:

public class Singleton {

    private static volatile Singleton instance;

    private Singleton() {}

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

这种方法在多线程的开发中被称之为Double-Check双重检查,这样实现单例模式的方法,不仅具备了懒汉式延时加载,资源占用较少的有点,也避免了线程不安全以及同步方法效率低的缺点,是比较推荐的方式。
这里采用了volatile关键字保证了instance的可见性,因为Java在运行过程中分主内存和工作内存,为了效率,都是从工作内存中去读取,于是就有可能存在主内存和工作内存不一样的情况,volatile关键字的加上避免了这种情况,但是volatile不可以保证原子性,具体的话可以参考一下网上关于volatile的解析,这里不做过多解释。

单例模式还有其他的实现方式,比如静态内部类、枚举等,在这里Po一下枚举的写法

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {

    }

借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。可能是因为枚举在JDK1.5中才添加,所以在实际项目开发中,很少见人这么写过。
这里可以参考Blog:
单例模式的八种写法比较

多例模式

一个类想要有多个对象,很简单,new关键字就可以实现,一个类如果想只有一个对象,则可以使用单例模式,但是一个类如果想产生指定个数的对象,又该如何实现呢?
这里提供一个思路,在类里定义一个静态int变量,用来表明最多能产生的实例数量,然后定义一个List用来存放产生的对象,然后在类的静态代码块里写生成对象的逻辑,这样就可以实现一个类产生指定个数的对象。(同样,这是在类加载的时候就产生实例的)
这里举一个皇帝类作为代码的时候,具体可参照《设计模式之禅》第七章《单例模式》。

代码实现如下:

public class Emperor {
    private static int maxEmperorNum = 2;

    private static int numOfEmperorNum = 0;

    private static List<Emperor> emperorList = new ArrayList<>();

    private static List<String> emperorNameList = new ArrayList<>();

    static {
        for (int i = 0; i < maxEmperorNum; i++) {
            emperorList.add(new Emperor("皇" + (i+1) + "帝"));
        }
    }

    private Emperor() {

    }

    private Emperor(String emperorName) {
        emperorNameList.add(emperorName);
    }

    // 随机取得一个皇帝
    public static Emperor getInstance() {
        Random random = new Random();
        int numOfEmperorNum = random.nextInt(maxEmperorNum);
        return emperorList.get(numOfEmperorNum);
    }

    public void say() {
        System.out.println(emperorNameList.get(numOfEmperorNum));
    }

}

最佳实践

单例模式是23个设计模式中比较简单的模式,应用也很广泛,最最常见的,Spring中的Bean就是单例的。Spring可以管理这些Bean,决定他们什么时候创建,什么时候销毁。

上一篇下一篇

猜你喜欢

热点阅读