Java设计模式之单例模式

栏目: 后端 · 发布时间: 7年前

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

一、前期回顾

上一篇《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("求见皇帝");
        }
    }
}

复制代码

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

Java设计模式之单例模式

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

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一个对象,否则直接返回之前实例化过的对象。我们按照原来的场景类运行下,结果如下:

Java设计模式之单例模式

这和我们预期结果一样,难道这样就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();
        }
    }
复制代码

执行结果:

Java设计模式之单例模式

出问题了,多线程环境下皇帝都不是同一个了,这在古代是要出大问题啊。那么为什么会出现这样的情况呢?因为在多线程环境下,可以理解为是并行去获取皇帝对象,那么第一个线程获取的时候,发现皇帝对象为空,那么就去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接口。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Art of Computer Programming, Volume 3

The Art of Computer Programming, Volume 3

Donald E. Knuth / Addison-Wesley Professional / 1998-05-04 / USD 74.99

Finally, after a wait of more than thirty-five years, the first part of Volume 4 is at last ready for publication. Check out the boxed set that brings together Volumes 1 - 4A in one elegant case, and ......一起来看看 《The Art of Computer Programming, Volume 3》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具