Java 设计模式 之 单例模式

来源:互联网 发布:医疗数据公司 金豆 编辑:程序博客网 时间:2024/04/29 12:45

介绍 :

  • 单例模式是一种在日常开发中比较常见的设计模式,在这个模式当中只能允许被实例化一次。
  • 单例模式有两种构建方式,懒汉式与饿汉式,这两种构建方式会在后面详解
  • 单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。

对于那些还没有明白单例模式的朋友,我们可以用下面这张图来理解到底什么是单例模式

这里写图片描述

如果你还是不懂不要着急,我们用言语来形容一下单例模式


假如你现在正在工作,组长让你负责写一个关于对数据库进行操作的类(比如增删改查这些方法)学过jdbc的都知道,在这里我们暂且讲将这个对数据库操作的类称为 DateBaseTools, 如果我们要对数据库进行操作必须要连接数据库 通过一个Connection 的对象来对数据库进行操作,假如说现在有三个类需要对数据库进行操作,按照以往的惯例我们会在这三个类要对数据库进行操作的时候进行

DateBaseTools dbTools = new DateBaseTools();

上面的代码是我们最通常的做法,伟人说的好 量变引起质变,我们这个三个类对他进行操作就new了三个DateBaseTools对象,要是有几千次这种操作的话,就相当于new了几千次这个对象,虽然说在jvm中不用担心这种,jvm会对不用的对象进行回收,但是当jvm执行GC后,就会拖慢系统的运行速度,这并不是我们希望看到的,还有就是几千次实例化,就相当于调用了DateBasesTools的构造方法并且在构造方法中进行连接数据库又是比较费时的操作,这样一来还是比较耗时的。

这个时候单例模式就能解决这种问题,下面请看代码

public class DateBaseTools{    private static DateBaseTools instance = null;    private DateBaseTools {        //数据库连接操作    }    public static DateBaseTools getSingInstance() {        if (instance == null) {            instance = new DateBaseTools();        }        return  instance;    }    public void action() {    System.out.println(">>>>>>>>>>");    }}

可以看出我们在单例类中将自己实例化通过getSingInstance来获取到自己实例化的对象,只需要在第一次构造函数的时候将Connection保留就能在每次调用单例的时候对数据库进行操作了,以后只要这个对象不为空就不会实例化,如果我们要调用他里面的方法的话只需要

DateBaseTools.getSingInstance().action();

这样不管是调用了他多少次,他都只实例化了一次,调用了构造方法一次,当我们想起单例模式的时候。很多人的第一反应是写出如下的代码,包括教科书上也是这样教我们的。这也就是我们刚才所说的懒汉式

懒汉式 :

正如下面的代码

public class DateBaseTools{    private static DateBaseTools instance = null;    private DateBaseTools {        //数据库连接操作    }    public static DateBaseTools getSingInstance() {        if (instance == null) {            instance = new DateBaseTools();        }        return  instance;    }    public void action() {    System.out.println(">>>>>>>>>>");    }}

现在我们来说一下这种构建方式的优缺点

优点与缺点 :

  • 优点: 代码简单明了
  • 缺点: 线程不安全
    很多人看到优点觉得代码那么短,确实很简单,很好理解,但是看到缺点就不是很明白了,现在我们来说明下这个到底是怎么个不安全

很多时候我们为了高效会用到多线程,但是当有多个线程并行调用 getSingInstance() 的时候,就会创建多个实例。可能你还是不明白,为什么会创建多个实例呢,我们现在来模拟一下

第一个线程访问getSingInstance()的时候 发现引用的DateBaseTools为空,这样就会自己实例化一个DateBaseTools,但是在我们正在实例化的时候,或者是还没有实例化的时候,这个时候要是第二个线程 或者是其他线程来访问getSingInstace() ,这个时候的引用DateBaseTools还是为空 这样就会实例化,但是当我们其他的线程这个时候已经判断了为null,正在实例化,所以这样就违背了,我们用单例模式的初衷了


相信你只要仔细看了我上面的叙述,应该能够懂了为什么单例模式在多线程下不能正常工作了。
现在就让我们就可以在上面的代码稍微改动一下就能修复这个缺点
相信很多学过的线程的都会知道synchronized 所以我们只需要加上这个就能让我们的单例在多线程并发下运行了这也就是所谓的双重检验锁了

双重检验锁代码 :

public class DateBaseTools{    private static DateBaseTools instance = null;    private DateBaseTools {        //数据库连接操作    }    public static DateBaseTools getSingInstance() {        if (instance == null) {            synchronized (DateBaseTools.class) {                instance = new DateBaseTools();            }        }        return  instance;    }    public void action() {    System.out.println(">>>>>>>>>>");    }}

双重检验锁是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。


这段代码看上去已经很完美了,也解决了多线程的问题,但是我想说的是他还是有问题的,对于JVM内部的指令重排这段代码还是有非常大的问题,如果你对JVM的指令重排不明所以的话,可以看我的这篇博客,相信会让你会对指令重排有一个初步的认识的

点击进入 JVM编译之指令重排

在JVM中 instance = new DateBaseTools();这段代码实际上经历了三个指令才完成的

memory =allocate();    //1:分配对象的内存空间 ctorInstance(memory);  //2:初始化对象 instance =memory;     //3:设置instance指向刚分配的内存地址

但是我们假定有两个线程,第一个线程调用单例模式的getSingInstance() 开始执行1 ->2 -> 3要是这个时候在JVM中执行的是 1 -> 3 ->2,也就是分配好内存空间后,为instance分配内存地址,这个时候线程二抢占cpu资源,执行getSingInstacne发现instance不为空 就会返回instance,这个时候返回的instanc还没进行初始化,肯定会报错了

所以我们只需要在instance的后面加上volatile关键字,volatile关键字有一个作用就是防止JVM对其进行指令重排序
这就是关于懒汉式的写法了,下面来介绍一下饿汉式


饿汉式 :

饿汉式相对于懒汉式简单,在对单例进行引用的时候就对instance进行初始化并且用static final 进行修饰,这样就不会再多线程中造成bug了,下面我们来看一下代码

public class DateBaseTools{    private static final DateBaseTools instance = new DateBaseTools();    public static DateBaseTools getSingInstance() {       return  instance;    }    public void action() {    System.out.println(">>>>>>>>>>");    }}

这样看起来要是多线程操作这个单例也就不会出现多次new的操作了,但是这种方法在一些场景是没有办法被使用的,比如我们需要向单例类传递一些值,就需要通关构造函数来传递参数,饿汉式的写法就会在一开就是自己实例化了自己,根本就没有机会传递参数进去

总结 :

饿汉式也好懒汉式也好都有自己不同的使用场景关于单例模式还有很多种不同的写法,比如说静态内部类、枚举这些方法就不一一列举了,一法通则万法通,这些方法都离不开本质,关于单例模式就讲到这里了

1 0
原创粉丝点击