Java设计模式之单例模式

1,990 阅读6分钟

一、前期回顾

上一篇《Java设计模式之开篇》介绍了设计的六大原则,分别是,单一职责、里氏替换原则、依赖倒置、迪米特法则、接口隔离、开闭原则。每一个原则都通过定义解释和代码实战进行详细体现,最后也总结了这六大原则,原则是死的,人是活的,我们要根据实际情况是使用六大原则,不要生搬硬套,为了原则而原则,为了模式而模式。这一篇,我们来介绍下设计模式最简单的一个模式,单例模式。

二、释义以及实战

  • 2.1 单例模式的定义

单例模式,英文:Singleton Pattern,英文解释:Ensure a class has only instance,and provide a global point of access to it.翻译过来就是说,要确保一个类只有一个实例,而且自行实例化并且向整个系统提供这个实例。

  • 2.2 单例模式的使用场景

单例模式的使用场景要求可以用一句话表示,如果一个类有多个对象会导致系统可能出现问题就要采用单例模式,一般的场景如下:

a.创建一个对象需要耗费大量资源或者时间,如IO,数据库连接等。

b.生成唯一id情况。

c.工具类,一般就可以采用静态类可以满足。

  • 2.3 单例模式的实战

我们来设计一个单例模式,场景:中国古代,一般情况,某一段时间只能有一个皇帝,当然特殊情况除外,无论是大臣还是平民,求见的皇帝都是同一个,我们用代码实现这个场景

//皇帝接口
public interface Iemperor {
    //皇帝下命令
    public void sayCommand(String str);
}

//明朝皇帝实现类
public class MingEmperor implements Iemperor {
    private static  MingEmperor emperor=new MingEmperor(new Random().nextInt(10)+"");
    //皇帝身份id
    private String id;
    //防止破坏单例
    private MingEmperor(String id) {
        this.id = id;
    }

    @Override
    public void sayCommand(String str) {
        System.out.println(str+"----------我是皇帝,这是我的id="+id);
    }
    public static MingEmperor getEmperor(){
        return emperor;
    }
}

//场景客户类
public class Client {
    public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            MingEmperor.getEmperor().sayCommand("求见皇帝");
        }
    }
}

执行下场景类,输出结果为

这说明,我们的单例模式成功了,这里我们通过声明一个全局静态变量,在类的初始化阶段就实例化一个对象,然后每次获取都是同一个对象。这种方式被称之为:饿汉氏单例模式。该单例模式的缺点就是要在初始化时候实例化对象,如果这种模式对象太多,就会创建大量的对象,而且有些可能还用不到。所以我们就改造下这种模式,变为懒汉氏单例模式,只有在真正需要用到对象的时候才开始实例化对象。我们改造下皇帝实现类代码:

public class MingEmperor implements Iemperor {
    private static  MingEmperor emperor;
    //皇帝身份id
    private String id;
    //防止破坏单例
    private MingEmperor(String id) {
        this.id = id;
    }

    @Override
    public void sayCommand(String str) {
        System.out.println(str+"----------我是皇帝,这是我的id="+id);
    }
    public static MingEmperor getEmperor(){
        if (emperor==null){
            emperor= new MingEmperor(new Random().nextInt(10)+"");
        }
        return emperor;
    }
}

这里获取皇帝对象的时候,判断是否为空,如果为空就new一个对象,否则直接返回之前实例化过的对象。我们按照原来的场景类运行下,结果如下:

这和我们预期结果一样,难道这样就ok了吗?既然是单例的,那么多线程环境下肯定也是单例的,我们换成多线程试试。

 public static void main(String[] args) {
        for (int i = 0; i <10 ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    MingEmperor.getEmperor().sayCommand("求见皇帝");
                }
            }).start();
        }
    }

执行结果:

出问题了,多线程环境下皇帝都不是同一个了,这在古代是要出大问题啊。那么为什么会出现这样的情况呢?因为在多线程环境下,可以理解为是并行去获取皇帝对象,那么第一个线程获取的时候,发现皇帝对象为空,那么就去new一个对象,第二个线程也有可能获取为空,那么自己也去new一个皇帝对象,所以就会出现上图这样的情况。那么我们如何改造来保证线程安全呢?有人说给获取实例的方法加上synchronized锁,没错,这样是可以解决问题,但是效率太低了,那么有没有什么更高效的方法呢?答案是,有,我们改造下代码:

public class MingEmperor implements Iemperor {
    //增加volatile修饰,防止虚拟机指令重排序
    private static volatile   MingEmperor emperor;
    //皇帝身份id
    private String id;
    //防止破坏单例
    private MingEmperor(String id) {
        this.id = id;
    }


    @Override
    public void sayCommand(String str) {
        System.out.println(str+"----------我是皇帝,这是我的id="+id);
    }
    public static synchronized MingEmperor getEmperor(){
        if (emperor==null){
        //采用同步代码块,缩小锁定范围,比直接同步方法效率要略高
            synchronized (MingEmperor.class){
            //这里再判断为空,是防止别的线程已经完成了实例化,这里重复实例化了,就违反了单例。
                if (emperor==null) emperor= new MingEmperor(new Random().nextInt(10)+"");
            }
        }
        return emperor;
    }
}

以上代码改造,主增加了volatile修饰全局变量,该变量主要功能就是增加线程之间的可见性,同时防止指令重排序(关于volatile变量,后续我会出一片文章详细说)。另外缩小了synchronized的范围,采用同步代码块。这样就完成了线程安全的懒汉式单例模式,该写法被称为,双重检查锁定(DCL)。

其实我们结合上一篇的知识,再看看这部分代码,发现这个单例模式违反了一个原则,就是单一职责原则,按照单一职责原则,皇帝类不用关心什么单例不单例,我只要传达命令就好了。所以这个模式也告诉了大家,原则要灵活使用。

  • 2.4 单例模式的缺点

1.刚刚上面提到的,单例模式违反了单一职责。

2.单例模式严格意义上说是没有接口的,要扩展只能修改,虽然上面的例子实现了接口,但是并不能针对接口做一个单例模式,因为单例模式要求“自行实例化”,接口和抽象类是不能被实例化的。所以在每个实现类进行单例模式,就算实现了接口,每个实现类都要自己实现一套单例的逻辑,也就是造成了代码重复。

  • 2.5 单例模式的优点

单例模式主要优点就算减少了系统资源消耗,优化了系统性能。

三、总结

其实,我们用到的池化技术可以理解为单例模式的一种扩展,池化技术就是可以允许创建指定数量的实例,而单例模式就相当于池化数量为1 。所以,模式在于要消化理解,然后灵活变通使用。另外需要注意的是,我们在设计单例模式的时候还需要考虑到一种破坏单例模式的情况,就是克隆方式,虽然我们私有化了构造方法,但是克隆对象并不需要执行构造方法,所以这里也是一个潜在破坏单例模式的方式。解决方法就是单例类不要实现Cloneable接口。

四、参考

《设计模式之禅》

六、推荐阅读

JAVA设计模式之开篇

带你走进java集合之ArrayList

带你走进java集合之HashMap

Java锁之ReentrantLock(一)

Java锁之ReentrantLock(二)

Java锁之ReentrantReadWriteLock

JAVA NIO编程入门(一)

JAVA NIO 编程入门(二)

JAVA NIO 编程入门(三)