设计模式学习之如何正确使用单例模式

    今天正式开始设计模式实践学习篇,将遵从3W学习思路,What、Why、How的思路,学习笔记也遵守3W的思路排版和规划

什么是单例?

    用小学老师教的拆字组词理解法单例就是当个实例。结合定义来说,单例就是一个类只实例化一次,并且实例化的对象可供系统全局调用。

为什么使用单例模式?

单例的优势总共有以下几点:

  1. 单机模式最明显有天然的性能优势,无序频繁的创建和销毁对象;
  2. 一次加载,终身受用。需要加载资源配置时,在程序启动时加载到一个单例中,永久留在内存中;
  3. 避免资源多重占用,比如写文件动作中,只有这一个实例可以操作。对于多线程环境中,也一直是单例在操作。

万物并不是完美的,单例其实也有自己的劣势:

  1. 首先单例不易于扩展,用单例就是为了使用实际对象,所以无需扩展,也就没有必要实现接口,要想扩展的只能修改本尊;
  2. 测试的缺点,必须等到实例化才能使用,当然也分场景来看,如果随着系统启动就能实现,那其实也不影响测试。但是如果方法级别的mock的话,那就找不到对象了;
  3. 单例违背了单一原则,单例的目的只是为了要单例,而单一原则是约束逻辑行为单一,单例把要单例和逻辑行为融合到了一起。

怎么实现单例?

根据单例产生的时机可以分为懒汉式和饿汉式

懒汉式标准:

  • 不能让其他类任意实例化此类对象,将构造方法私有化;
  • 内部提供一个私有、静态,本对象的属性,设置为null;
  • 内部提供一公有的,获取此属性的一个方法getInstance,先判读属性是否为null,如果为实例化则先实例化在返回,否则直接返回对象。
/**
 * 懒汉式
 */
public class Sutdent {
    //对象属性,私有静态且不可修改的
    private static Sutdent sut = null;

    //构造方法私有化
    private Sutdent(){

    }
    //静态方法实例化对象
    public static Sutdent getInstance(){
        if(null == sut){
            new Sutdent();
        }
        return sut;
    }
}

饿汉式标准:

  •  不能让其它类任意的实例化此类对象,将构造方法私有化;
  •  内部提供一个私有、静态(最好是final),本对象的属性;
  •  内部提供一个公有的,获取此属性的一个方法getInstance;
/**
 * 饿汉式
 */
public class Techer {

    //随类加载而实例化的对象
    private static final  Techer techer = new Techer();
    //构造方法私有化
    private Techer(){

    }
    //获取对象方法
    public static Techer getInstance(){
        return techer;
    }
}

 对比懒汉式和饿汉式,相同点都将构造方法私有化,不允许其他类任意实例化该对象。区别在于或者该对象的时机,懒汉式是需要使用的时候在实例化对象,起到延时加载的作用。而饿汉式,在类加载时就会实例化对象,调用时速度块。不过这两种实现单例的方式都有自己的弊端:

懒汉式的弊端:

懒汉式在单线程环境是没有问题的,但是在多线程、高并发的环境下,问题就来了。上面饿汉式的例子中,两个线程同时访问null == sut判断,那么就两个线程都生成了对象,这样一来的可就不是单例了。该如何规避这个问题,加锁貌似是最有效的方式,代码 getInstance()方法修改如下:

    //静态方法实例化对象
    public static Sutdent getInstance() {
        if (null == sut) {
            synchronized (Sutdent.class) {
                if (null == sut) {
                    new Sutdent();
                }
            }
        }
        return sut;
    }

这样修改就看似完美了,通过 synchronized 锁来同步代码块(选择同步方法效率会更低),限制两个线程的同时访问,达到互斥的作用,这也是比较的经典的双重检查机制。不过看似无懈可击的代码,还存在一个致命的缺点,这个点就是new关键子。在程序员的眼中,new是原子性,只要new就会生成一个对象,但是在JVM和计算机的眼中,一个new却分成了三个步骤:

  • 首先JVM会在创建对象时会分配一块内存;
  • 在内存上初始化Student对象;
  • 然后内存的地址赋值给 sut变量。

理想状态中这三个步骤是顺序执行,但是编译器为了执行效率,在不影响执行结构的前提下有优化执行顺序,那么实际的执行顺序可能是:

  • 首先JVM会在创建对象时会分配一块内存;
  • 然后将内存的地址赋值给 sut变量。
  • 在内存上初始化Student对象;

这样一来,问题也就来了。假设线程A在访问getInstance方法,当执行到将内存地址值赋值到变量sut时,发生了线程切换。B线程开始访问,但是此B判断sut已经不是null,直接返回了,但实际上Student还没有初使化完成,有可能sut造成空指针异常。想要解决这个bug,就是给变量sut加volatile修饰禁止编译优化造成的指定重排,最终代码如下:

/**
 * 懒汉式
 */
public class Sutdent {
    //对象属性,私有静态且不可修改的
    private static volatile Sutdent sut = null;

    //构造方法私有化
    private Sutdent(){

    }

    //静态方法实例化对象
    public static Sutdent getInstance() {
        if (null == sut) {
            synchronized (Sutdent.class) {
                if (null == sut) {
                    new Sutdent();
                }
            }
        }
        return sut;
    }
}

饿汉式的弊端: 

饿汉式是线程安全的,因为类只会加载一次,因此首次调用迅速。但是还存在一个问题,如果实例化的对象长时间没有使用的化,会被垃圾收集器回收,对象的状态会丢失,再想生成就有点走头无路了。

怎么用单例?

既然选择单例就是因为单例本身的优点,使用单例也应该再使用其优点的状态下使用:

  • 池化资源控制,避免资源过度利用,比如线程池、数据库连接池
  • 全局数据加载,项目中的参数配置或者全局变量,有些数据可以跟随系统启动加载到内存中
  • 在开源框架中,Spring的每个bean默认都是单例。
  • 还有比如网页的访问数量统计,要求全系统提供给一个唯一的序列号等等

参考:《设模式之禅》秦小波著

 

你可能感兴趣的:(Java设计模式,java)