美文网首页
设计模式-单例模式(Singleton)

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

作者: 初夏倾城 | 来源:发表于2018-08-24 12:52 被阅读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,决定他们什么时候创建,什么时候销毁。

    相关文章

      网友评论

          本文标题:设计模式-单例模式(Singleton)

          本文链接:https://www.haomeiwen.com/subject/awubiftx.html