Java设计模式之--单例模式

Java设计模式系列:

  1. 设计模式入门:https://blog.csdn.net/u011863006/article/details/89223282
  2. 单例模式:https://blog.csdn.net/u011863006/article/details/84201592

Java各种技术、各种框架更新的速度越来越快,学习成本越来越高,但是我们学习Java要学习其中不变的部分,其中设计模式就是最高层次的抽象,是高出框架、语言的。所以学习的收益也是最高的,不会被时代淘汰,并且几乎在任何一个面试中都会被问到。
最近在看《Head First设计模式》这本书,准备将其中的感悟结合平时的积累总结一下,写一个设计模式系列博客。

首先就从最简单的单例模式开始吧。单例模式的定义就是确保一个类只有一个实例,并提供一个全局的访问点。那么什么时候需要单例模式呢,比如说:线程池、连接池、缓存、注册表、日志对象,还有打印机、显卡的驱动程序,这些类只能有一个实例,如果有多个实例就对造成混乱。

单例模式从对象生成的时间上可以分为懒汉模式和饿汉模式,懒汉模式比较懒:就是用到这个对象的时候就创建;饿汉模式比较饥渴:就是在类加载的时候就对对象进行创建。饿汉模式比较简单,我们先说饿汉模式。

饿汉模式

我们以一个打印机为例,因为我们只有一个打印机,所有我们对应的程序中只能有一个打印机的实例对象。为了不让程序随便的new出很多对象,我们最想想到的是将构造函数变成私有的。代码如下图:

package com.sheliming.singlenton.hungry;

/**
 * 打印机类
 * 饿汉模式
 */
public class Printer {
    private static Printer printer = new Printer();

    private Printer() {
    }

    public static Printer getInstance() {
        return printer;
    }
}

这段代码看似很完美,它的好处是只在类加载的时候创建一次实例,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题。它的缺点也很明显,即使这个单例没有用到也会被创建,而且在类加载之后就被创建,内存就被浪费了。
所以下面就进入到我们懒汉式的单例模式:

懒汉式

懒汉式比较复杂,我们一一道来:

1.入门级

懒汉式就是使用的时候new对象,那么就应该在getInstance的时候创建对象,代码如下:

package com.sheliming.singlenton.lazy;

/**
 * 打印机类
 * 懒汉模式
 */
public class Printer {
    private static Printer printer = null;

    private Printer() {
    }

    public static Printer getInstance() {
        if (printer == null) {
            printer = new Printer();

        }
        return printer;
    }
}

这段代码在单线程的时候没有任何问题,但是到了多线程中,就会出问题。例如:当两个线程同时运行到判断if (printer == null)语句,并且instance确实没有创建好时,那么两个线程都会创建一个实例。

2.加snychronized关键字

既然多线程下有问题,我们首先想到的是在getInstance方法上加上snychronized关键字,这样在多线程的方法中同时只有一个线程可以访问这个方法,这样对象只会被初始化一次,在访问的时候if (printer == null)已经不为null了。代码如下

package com.sheliming.singlenton.lazy;

/**
 * 打印机类
 * 懒汉模式,带synchronized关键字
 */
public class Printer2 {
    private static Printer2 printer = null;

    private Printer2() {
    }

    public synchronized static Printer2 getInstance() {
        if (printer == null) {
            printer = new Printer2();

        }
        return printer;
    }
}

但是这个方法也有缺点:每次通过getInstance方法得到singleton实例的时候都有一个试图去获取同步锁的过程。而众所周知,加锁是很耗时的。能避免则避免。

3.双重校验锁(Double-Check)

下面这种方法不仅可以避免线程安全的问题,而且可以避免每次获取对象的时候进行加锁,代码如下:

package com.sheliming.singlenton.lazy;

/**
 * 打印机类
 * 懒汉模式,双重校验锁
 */
public class Printer3 {
    private static Printer3 printer = null;

    private Printer3() {
    }

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

4.终极版本volatile关键字

我们看到双重校验锁即实现了延迟加载,又解决了线程并发问题,同时还解决了执行效率问题,是否真的就万无一失了呢?

这里要提到Java中的指令重排优化。所谓指令重排优化是指在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。JVM中并没有规定编译器优化相关的内容,也就是说JVM可以自由的进行指令重排序的优化。

这个问题的关键就在于由于指令重排优化的存在,导致初始化Singleton和将对象地址赋给instance字段的顺序是不确定的。在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址赋值给instance字段了,然而该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,取到的就是状态不正确的对象,程序就会出错。

以上就是双重校验锁会失效的原因,不过还好在JDK1.5及之后版本增加了volatile关键字。volatile的一个语义是禁止指令重排序优化,也就保证了instance变量被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题。代码如下:

package com.sheliming.singlenton.lazy;

/**
 * 打印机类
 * 懒汉模式,双重校验锁
 */
public class Printer4 {
    private static volatile Printer4 printer = null;

    private Printer4() {
    }

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

这种方法是可以在生产环境中使用的!!

5、静态内部类

除了以上几种方法,还有2中比较巧妙的方法。首先是静态内部类的方法:

package com.sheliming.singlenton.lazy.staticclass;

/**
 * 打印机类
 * 静态内部类
 */
public class Printer {
    private static class PrinterHolder{
        public static Printer printer = new Printer();
    }

    private Printer() {
    }

    public static Printer getInstance() {

        return PrinterHolder.printer;
    }
}

这种写法非常巧妙:

  1. 对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真·单例。
  2. 同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。
    ——它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现。

6.枚举实现

还有最后一种方式:

public enum SingleInstance {
    INSTANCE;
    public void fun1() { 
        // do something
    }
}
// 使用
SingleInstance.INSTANCE.fun1();

上面提到的四种实现单例的方式都有共同的缺点:

  1. 需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
  2. 可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
    而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。但是它仍然不是完美的——比如,在需要继承的场景,它就不适用了。

总结

说了这么多,总结一下生产中经常使用的是:

  1. 加voletile关键字的双重检查锁。
  2. 饿汉模式。

参考文献

《Head First设计模式》
https://blog.csdn.net/goodlixueyong/article/details/51935526
https://www.cnblogs.com/dongyu666/p/6971783.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值