戏说设计模式(三)单例模式

来源:互联网 发布:星型网络 编辑:程序博客网 时间:2024/05/16 18:19

0. 前言

找一个人惺惺相惜 找一颗心心心相印 在这个宇宙我是独一无二

上面这是一句歌词哈,来自梁静茹的《给未来的自己》,我很喜欢听这首歌。当然,今天我并不是要说音乐,而是讨论设计模式,上面的歌词中有句“在这个宇宙我是独一无二”,而“独一无二”就是今天主角。很明显,除开平行宇宙之类的东西,每个人在全宇宙都是独一无二的。那么,在Java的世界里,怎么让一个实例成为独一无二的呢。答:使用单例模式。下面我就介绍一下单例模式。

1. 单例模式

单例模式是很常见的一种设计模式,从名字可以看出来,是为了实现一个类只有一个实例。

1.1 定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

1.2 实现

要怎么实现一个类只有一个实例呢?其实有两种方式可以实现单例模式,分别是懒汉式饿汉式。现在假设我有一个Singleton类要求只能有该类的一个实例。

1.2.1 懒汉式

1.首先,要想一个类只有一个实例,那么就得控制一下创建实例的权限,不能暴露出去给外部的类,只能自己用。那怎么才能让外部不能创建一个类的实例呢?很简单,把构造方法设为私有(private)的就好了。如

private Singleton(){}

2.那现在又有个问题,你把构造方法私有了,外部的类还怎么拿到你的实例?这时候就需要提供一个方法可以返回这个类的实例。如

public Singleton getInstance(){     return new Singleton();}

3.那么问题又来了,获取实例的方法getInstance()是只有实例才可以调用的,你一方面不给人家创建实例,一方面又要让人家用实例调用这个方法,不是玩人家嘛。所以,应该将上面那个方法加上一个static,就变成了这样

public static   getInstance(){     return new Singleton();}

4.好了,获取实例的方法有了,但是还是有点问题,这样子每次调用获取实例的方法还是会创建一个新的实例。由于获取实例的方法是静态方法,所以我们可以用一个静态变量来保存一个唯一实例。

private static Singleton instance = null;

5.然后获取实例的方法就可以这样子写了

public static Singleton getInstance(){    if(instance == null){        //如果是第一次使用这个方法,instance为null,则创建一个实例        instance = new Singleton();    }    return instance;}

那么,整个类的代码如下

public class Singleton {    private static Singleton instance = null;    private Singleton(){}    public static Singleton getInstance(){        if(instance == null){            //如果是第一次使用这个方法,instance为null,则创建一个实例            instance = new Singleton();        }        return instance;    }}

1.2.2 饿汉式

饿汉式实现单例模式有两个地方与懒汉式不同,一个是静态变量,一个是获取实例方法。

1.静态变量instance,让类加载的时候初始化。

private static Singleton instance = new Singleton();

2.获取实例方法,不用再判断静态变量是否为null。

public static Singleton getInstance(){    return instance;}

整个类的代码如下

public class Singleton {    private static Singleton instance = new Singleton();    private Singleton(){}    public static Singleton getInstance(){        return instance;    }}

1.3 两种实现方式的思考

  1. 懒汉式是典型的时间换空间。听名字就知道它很懒了,不到不得已的时候都不会干活的。懒汉式只有用到获取实例方法的时候,才会创建一个实例,但是每次都要判断一下有没有,没有才创建,有就直接返回。
  2. 饿汉式是典型的空间换时间。听名字就知道它很饿了,所以一加载类的时候赶紧创建一个实例,这样子每次调用实例方法的时候就不用再进行判断了。

2. 具体应用

我就用一个案例来实现单例模式吧。在Java工程中,经常会用到配置文件,无论是properties还是xml文件,都必须被读入到内存中,而且可能会有很多地方都会用到一个配置文件。那我现在假设有两个类,一个是A一个是B,都需要用到配置文件config.properties,并且我还有一个类Config,让配置文件的参数映射到类中,并且提供读取配置文件的方法。那么代码如下。

2.1 未使用单例模式

配置文件config.properties

param1=Aparam2=B

Config

import java.io.InputStream;import java.util.Properties;public class Config {    //存放配置文件中参数param1的值    private String param1;    //存放配置文件中参数param2的值    private String param2;    public String getParam1() {        return param1;    }    public String getParam2() {        return param2;    }    public Config(){        readProperties();    }    /**     * 读取config.properties文件,并将对应的参数填充到实例中     * @throws Exception      */    private void readProperties(){        Properties prop = new Properties();        InputStream inStream = null;        try{        //加载配置文件        inStream = Config.class.//                getClassLoader().//                getResourceAsStream("config.properties");        prop.load(inStream);        //将配置文件对应的参数填充到实例中        this.param1 = prop.getProperty("param1");        this.param2 = prop.getProperty("param2");        }catch (Exception e) {            e.printStackTrace();        }finally {            try{                //关闭流                inStream.close();            }catch (Exception e) {                e.printStackTrace();            }        }    }}

A类

public class A {    public static void main(String[] args) {        Config config = new Config();        String param1 = config.getParam1();        System.out.println(param1);    }}

B类

public class B {    public static void main(String[] args) {        Config config = new Config();        String param2 = config.getParam2();        System.out.println(param2);    }}

A类运行结果

A

B类运行结果

B

那么这里就有个问题,可能到时候我不至只有两个类要读取这个配置文件,万一有几百个类都要用到呢,那我每次就读一下,创建一个新的实例,而且这个配置文件又不会变的,这不是极大的浪费嘛!!!所以此时就需要单例模式登场了,让这个配置文件对应的实例只有一个,大家都用这一个,不也是很美妙的事情嘛。

2.2 使用单例模式

使用单例模式(懒汉式实现)后,Config类就变成下面这样子

import java.io.InputStream;import java.util.Properties;public class Config {    private static Config instance = null;    public static Config getInstance(){        if(instance == null){            instance = new Config();        }        return instance;    }    //存放配置文件中参数param1的值    private String param1;    //存放配置文件中参数param2的值    private String param2;    public String getParam1() {        return param1;    }    public String getParam2() {        return param2;    }    //私有化构造参数    private Config(){        readProperties();    }    /**     * 读取config.properties文件,并将对应的参数填充到对象中     * @throws Exception      */    private void readProperties(){        Properties prop = new Properties();        InputStream inStream = null;        try{        //加载配置文件        inStream = Config.class.//                getClassLoader().//                getResourceAsStream("config.properties");        prop.load(inStream);        //将配置文件对应的参数填充到对象中        this.param1 = prop.getProperty("param1");        this.param2 = prop.getProperty("param2");        }catch (Exception e) {            e.printStackTrace();        }finally {            try{                //关闭流                inStream.close();            }catch (Exception e) {                e.printStackTrace();            }        }    }}

那么A类和B类调用就变成了这样

A类

public class A {    public static void main(String[] args) {        //将等号右边的new Config();变为Config.getInstance();        Config config = Config.getInstance();        String param1 = config.getParam1();        System.out.println(param1);    }}

结果

A

B类

public class B {    public static void main(String[] args) {        // 将等号右边的new Config();变为Config.getInstance();        Config config = Config.getInstance();        String param2 = config.getParam2();        System.out.println(param2);    }}

结果

B

这样子就用单例模式解决了一个具体应用了!等等,没那么简单,其实,这其中还有一个十分严重的问题,那就是线程安全问题

3. 单例模式线程安全问题

这里只考虑跟单例模式有关的东西,不考虑类中的其它方法,那么单例模式的线程安全问题出自哪呢?出自获取实例的方法上。

3.1 饿汉式

饿汉式是线程安全的,因为在饿汉式中,类一加载便创建了一个实例,而在装载一个类的时候是不会发生并发的。

3.2 懒汉式

不加同步的懒汉式是线程不安全的,为什么这么说呢?假设我有两个线程,一个线程t1一个线程t2同时调用了getInstance()方法,就可能导致并发问题。

假设某一时刻程序运行情况是下图这样子的

然后程序继续运行,变成了下面这样子

很明显,当两个线程都执行完这个方法后,会有两个不同的Config实例,这样子单例模式就失效了。

3.3 解决方案

3.3.1 解决方案一

一旦涉及到线程安全问题,synchronized就会登场。可以把获取实例方法设为同步的,于是就变成下面这样子。

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

但是synchronized有个问题,就是性能问题,那我们可以优化一下,缩小同步范围。

3.3.2 解决方案二

使用“双重检查加锁”的方式实现,这样子既能实现线程安全,又使性能不会有太大影响。那么双重检查加锁是个什么东东呢?

所谓双重检查加锁,就是指并不是每次进入getInstance方法都需要同步,而是先进入方法,然后检查实例是否存在,不存在再进入同步块,这是第一重检查。进入同步块后再检查一下实例是否存在,这是第二重检查,不存在再真正地创建实例。

注意,双重检查加锁机制的实现会使用一个关键字volatile,它的意思是被volatile修饰的变量的值不会被本地线程缓存,所有对此变量的读写操作都是直接操作共享内存的。关于为什么要加这个关键字在贴出代码后再解释。想了解更多关于volatile关键字的知识,大家可以去这里看看http://blog.csdn.net/timheath/article/details/53352572

具体实现如下

public class Singleton {    //注意,这里新加了一个volatile    private volatile static Singleton instance = new Singleton();    private Singleton(){}    public static Singleton getInstance(){        //先检查实例是否存在,不存在才进入同步块        if(instance == null){            //同步块            synchronized (Singleton.class) {                //再次检查实例是否存在,如果不存在才真正地创建实例                if(instance == null){                    instance = new Singleton();                }            }        }        return instance;    }}

3.3.3 解决方案三

上面的方案,要么性能不行,要么实现麻烦,那么有没有一种更优的方案呢?有!那就是Lazy initialization holder class模式,这个东西我之前有转载过,大家可以直接去这里看http://blog.csdn.net/timheath/article/details/53355073

4. 关于懒汉式的一点思考

4.1 延迟加载思想

其实懒汉式中,有一种延迟加载的思想。什么是延迟加载呢?就是一开始不要加载数据,一直等到要用到的时候才去加载,这样子够懒吧,躲不过了才去加载。延迟加载思想在很多地方都会用到,最常见的像Hibernate框架也有用到。

4.2 缓存思想

在懒汉式中,用一个字段去缓存一个实例。每次都先去找这个字段有没有指向一个实例,有就直接用,没有就创建一个并且将引用赋给这个字段,这样子下次再要用直接用就好了。像缓存思想用到的也很多,像基于内存的数据库Redis


如果上面的内容有错误的地方或者讲的不好的地方,还请大家指点一下,我好及时修改。

0 0
原创粉丝点击