java 设计模式之单例模式 一篇就够了

版权声明:本文为HCG原创文章,未经博主允许不得转载。请联系[email protected] https://blog.csdn.net/qq_39455116/article/details/82936637

单例模式


单例对象(Singleton)是一种常用的设计模式。
在Java应用中,单例对象能保证在一个JVM中,该对象只有一个实例存在。这样的模式有几个好处:

1、某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。

2、省去了new操作符,降低了系统内存的使用频率,减轻GC压力。

3、有些类如交易所的核心交易引擎,控制着交易流程,
			如果该类可以创建多个的话,系统完全乱了。
		(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),
所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。
    1. 经常面试也会问什么是单例模式

Java Singleton模式主要作用是保证在Java应用程序中,一个类Class只有一个实例存在。
使用Singleton的好处还在于可以节省内存,因为它限制了实例的个数,有利于Java垃圾回收 garbage collection

    1. 单例模式适用场景

我们在浏览BBS、SNS网站的时候,常常会看到“当前在线人数”这样的一项内容。

对于这样的一项功能,我们通常的做法是把当前的在线人数存放到一个内存、文件或者数据库中,
每次用户登录的时候,就会马上从内存、文件或者数据库中取出,在其基础上加1后,
作为当前的在线人数进行显示,然后再把它保存回内存、文件或者数据库里,
这样后续登录的用户看到的就是更新后的当前在线人数;同样的道理,当用户退出后,当前在线人数进行减1的工作。
所以,对于这样的一个需求,我们按照面向对象的设计思想,可以把它抽象为“在线计数器”这样一个对象。

网站代码中凡是用到计数器的地方,只要new一个计数器对象,然后就可以获取、保存、增加或者减少在线人数的数量。
不过,我们的代码实际的使用效果并不好。 假如有多个用户同时登录,那么在这个时刻,通过计数器取到的在线人数是相同的,
于是他们使用各自的计数器加1后存入文件或者数据库。这样操作后续登陆的用户得到的在线人数,
与实际的在线人数并不一致。所以,把这个计数器设计为一个全局对象,所有人都共用同一份数据,
就可以避免类似的问题,这就是我们所说的单例模式的其中的一种应用

单例模式能够保证一个类仅有唯一的实例,并提供一个全局访问点。

我们是不是可以通过一个全局变量来实现单例模式的要求呢?我们只要仔细地想想看,全局变量确实可以提供一个全局访问点,但是它不能防止别人实例化多个对象。通过外部程序来控制的对象的产生的个数,势必会系统的增加管理成本,增大模块之间的耦合度。所以,最好的解决办法就是让类自己负责保存它的唯一实例,并且让这个类保证不会产生第二个实例,同时提供一个让外部对象访问该实例的方法。自己的事情自己办,而不是由别人代办,这非常符合面向对象的封装原则。

单例模式主要有3个特点,:

1、单例类确保自己只有一个实例。

2、单例类必须自己创建自己的实例。

3、单例类必须为其他对象提供唯一的实例。

单例模式的实现方式:懒汉单例类和饿汉单例类

· 懒汉式单例类

对于懒汉模式,我们可以这样理解:该单例类非常懒,
只有在自身需要的时候才会行动,从来不知道及早做好准备。
它在需要对象的时候,才判断是否已有对象,如果没有就立即创建一个对象,
然后返回,如果已有对象就不再创建,立即返回
懒汉模式只在外部对象第一次请求实例的时候才去创建。

· 饿汉式单例

对于饿汉模式,我们可以这样理解:该单例类非常饿,
迫切需要吃东西,所以它在类加载的时候就立即创建对象。

我们对比一下懒汉模式和饿汉模式的优缺点:

懒汉模式,它的特点是运行时获得对象的速度比较慢,但加载类的时候比较快。
		它在整个应用的生命周期只有一部分时间在占用资源。
饿汉模式,它的特点是加载类的时候比较慢,但运行时获得对象的速度比较快。
	    它从加载到应用结束会一直占用资源。

这两种模式对于初始化较快,占用资源少的轻量级对象来说,没有多大的性能差异,选择懒汉式还是饿汉式都没有问题。

但是对于初始化慢,占用资源多的重量级对象来说,就会有比较明显的差别了。
所以,对重量级对象应用饿汉模式,类加载时速度慢,但运行时速度快;
懒汉模式则与之相反,类加载时速度快,但运行时第一次获得对象的速度慢。

从用户体验的角度来说,我们应该首选饿汉模式。
我们愿意等待某个程序花较长的时间初始化,却不喜欢在程序运行时等待太久,给人一种反应迟钝的感觉,
所以对于有重量级对象参与的单例模式,我们推荐使用饿汉模式。

而对于初始化较快的轻量级对象来说,选用哪种方法都可以。
如果一个应用中使用了大量单例模式,我们就应该权衡两种方法了。轻量级对象的单例采用懒汉模式,减轻加载时的负担,缩短加载时间,提高加载效率;同时由于是轻量级对象,把这些对象的创建放在使用时进行,实际就是把创建单例对象所消耗的时间分摊到整个应用中去了,对于整个应用的运行效率没有太大影响。


什么情况下使用单例模式

单例模式也是一种比较常见的设计模式,它到底能带给我们什么好处呢?其实无非是三个方面的作用:

第一、控制资源的使用,通过线程同步来控制资源的并发访问;

第二、控制实例产生的数量,达到节约资源的目的。

第三、作为通信媒介使用,也就是数据共享,它可以在不建立直接关联的条件下,
		让多个不相关的两个线程或者进程之间实现通信。

单例模式使用场景

比如,数据库连接池的设计一般采用单例模式,数据库连接是一种数据库资源。

实际上,配置信息类、管理类、控制类、门面类、代理类通常被设计为单例类。

像Java的Struts、Spring框架,.Net的Spring.Net框架,以及Php的Zend框架都大量使用了单例模式。

1. 最简单的单例模式

    1. 代码

public class SimpleSingleton {
    private static SimpleSingleton instance = null;

    private SimpleSingleton() {

    }

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

    public Object readResolve() {
        return instance;
    }

}
    1. 这个类可以满足基本要求,但是,像这样毫无线程安全保护的类,如果我们把它放入多线程的环境下,肯定就会出现问题了,如何解决?我们首先会想到对getInstance方法加synchronized关键字,如下:
package moshi.single;

public class SimpleSynSingleton {
    private static SimpleSynSingleton instance = null;

    private SimpleSynSingleton() {

    }

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

    public Object readResolve() {
        return instance;
    }

}

    1. 但是,synchronized关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。我们改成下面这个:
package moshi.single;

public class SimpleSynFirstSingleton {
    private static SimpleSynFirstSingleton instance = null;

    private SimpleSynFirstSingleton() {

    }

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

    public Object readResolve() {
        return instance;
    }

}

    1. 似乎解决了之前提到的问题,将synchronized关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在instance为null,并创建对象的时候才需要加锁,性能有一定的提升。
    1. 但是,这样的情况,还是有可能有问题的,看下面的情况:在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的。但是JVM并不保证这两个操作的先后顺序,也就是说有可能JVM会为新的Singleton实例分配空间,然后直接赋值给instance成员,然后再去初始化这个Singleton实例。这样就可能出错了,我们以A、B两个线程为例:

a>A、B线程同时进入了第一个if判断

b>A首先进入synchronized块,由于instance为null,所以它执行instance = new Singleton();

c>由于JVM内部的优化机制,JVM先画出了一些分配给Singleton实例的空白内存,并赋值给instance成员(注意此时JVM没有开始初始化这个实例),然后A离开了synchronized块。

d>B进入synchronized块,由于instance此时不是null,因此它马上离开了synchronized块并将结果返回给调用该方法的程序。

e>此时B线程打算使用Singleton实例,却发现它没有被初始化,于是错误发生了。

所以程序还是有可能发生错误,其实程序在运行过程是很复杂的,从这点我们就可以看出,尤其是在写多线程环境下的程序更有难度,有挑战性。我们对该程序做进一步优化:

    1. 至此我们得到一个暂时完美的单例模式
package moshi.single;
        /*
        在内部创建一个实例,构造器全部设置为private,所有方法均在该实例上改动,
        在创建上要注意类的实例化只能执行一次,可以采用许多种方法来实现,
        如Synchronized关键字,或者利用内部类等机制来实现。
        */
public class Singleton {
    private Singleton() {
    }

    private static class SingletonFactory {
        private static Singleton instance = new Singleton();
    }

    public Singleton getInstance() {
        return SingletonFactory.instance;
    }

    public Object readResolve() {
        return getInstance();
    }

}

    1. 其实说它完美,也不一定,如果在构造函数中抛出异常,实例将永远得不到创建,也会出错。所以说,十分完美的东西是没有的,我们只能根据实际情况,选择最适合自己应用场景的实现方法。也有人这样实现:因为我们只需要在创建类的时候进行同步,所以只要将创建和getInstance()分开,单独为创建加synchronized关键字,也是可以的:
package moshi.single;

public class SynSingleton {

    private static SynSingleton instance = null;

    private SynSingleton() {

    }

    private static synchronized void synInit() {
        if (instance == null) {
            instance = new SynSingleton();
        }
    }

    public static SynSingleton getInstance() {
        if (instance == null) {
            synInit();
        }
        return instance;
    }
}

    1. 影子单例模式
      采用"影子实例" 的办法为单例对象的属性同步更新
package moshi.single;

import java.util.Vector;

public class ProPertiesSingleton {


    private static ProPertiesSingleton instance = null;
    private Vector properties = null;

    public Vector getProperties() {
        return properties;
    }

    private ProPertiesSingleton() {

    }

    private static synchronized void synInit() {
        if (instance == null) {
            instance = new ProPertiesSingleton();
        }
    }

    public static ProPertiesSingleton getInstance() {
        if (instance == null) {
            synInit();
        }
        return instance;
    }


    public void updataPeoperties() {
        ProPertiesSingleton shadow = new ProPertiesSingleton();
        properties = shadow.getProperties();
    }
}

通过单例模式的学习告诉我们:

1、单例模式理解起来简单,但是具体实现起来还是有一定的难度。

2、synchronized关键字锁定的是对象,在用的时候,一定要在恰当的地方使用(注意需要使用锁的对象和过程,可能有的时候并不是整个对象及整个过程都需要锁)。


其实上面说的这些方法都是在改进单例模式,而且上面也没有说懒汉模式和饿汉模式、静态方法模式
那下面就详细说一下

  • 饿汉模式
public class Singleton{
    private static Singleton instance = new Singleton();
    private Singleton(){}
    public static Singleton newInstance(){
        return instance;
    }
}

  • 懒汉模式
public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static Singleton newInstance(){
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

但是这里的懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例,因此需要加锁解决线程同步问题,实现如下。

  • 线程安全懒汉模式
public class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    public static synchronized Singleton newInstance(){
        if(null == instance){
            instance = new Singleton();
        }
        return instance;
    }
}

  • 双重校验锁懒汉模式

加锁的懒汉模式看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。因此就有了双重校验锁,先看下它的实现代码。

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

  • 双重校验锁失效,用volatile懒汉模式
public class Singleton {
    private static volatile Singleton instance = null;
    private Singleton(){}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

  • 静态内部类实现单例模式

这种方式同样利用了类加载机制来保证只创建一个instance实例。它与饿汉模式一样,也是利用了类加载机制,因此不存在多线程并发的问题。不一样的是,它是在内部类里面去创建对象实例。这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,也就不会创建单例对象,从而实现懒汉式的延迟加载。也就是说这种方式可以同时保证延迟加载和线程安全。

public class Singleton{
    private static class SingletonHolder{
        public static Singleton instance = new Singleton();
    }
    private Singleton(){}
    public static Singleton newInstance(){
        return SingletonHolder.instance;
    }
}

  • 枚举实现单例模式

不过,在实际工作中,很少看见有人这么写。

public enum Singleton{
    instance;
    public void whateverMethod(){}    
}

到这儿,单例模式基本已经讲完了,结尾处,笔者突然想到另一个问题,就是采用类的静态方法,实现单例模式的效果,也是可行的,此处二者有什么不同?

首先,静态类不能实现接口。(从类的角度说是可以的,但是那样就破坏了静态了。因为接口中不允许有static修饰的方法,所以即使实现了也是非静态的)

其次,单例可以被延迟初始化,静态类一般在第一次加载是初始化。之所以延迟加载,是因为有些类比较庞大,所以延迟加载有助于提升性能。

再次,单例类可以被继承,他的方法可以被覆写。但是静态类内部方法都是static,无法被覆写。

最后一点,单例类比较灵活,毕竟从实现上只是一个普通的Java类,只要满足单例的基本需求,你可以在里面随心所欲的实现一些其它功能,但是静态类不行。

从上面这些概括中,基本可以看出二者的区别,但是,从另一方面讲,我们上面影子懒汉模式实现的那个单例模式,内部就是用一个静态类来实现的,所以,二者有很大的关联,只是我们考虑问题的层面不同罢了。两种思想的结合,才能造就出完美的解决方案,就像HashMap采用数组+链表来实现一样,其实生活中很多事情都是这样,单用不同的方法来处理问题,总是有优点也有缺点,最完美的方法是,结合各个方法的优点,才能最好的解决问题!

同时附上代码链接https://github.com/HouChenggong/java-factory.git传送门



工厂模式就介绍到这里了,欢迎留言说出你的观点,有什么问题,或者有不同的见解欢迎联系我 
邮箱:[email protected]












猜你喜欢

转载自blog.csdn.net/qq_39455116/article/details/82936637
今日推荐