概述
单例模式
单例模式是创建型模式的一种,它的目的是使一个对象在整个程序的生命周期中只有一个实例。是的,单例模式的唯一目的就是保证一个对象在整个系统中只有一个实例,如果你想让某个对象只有一个实例的话,可以考虑使用单例模式。
如何保证系统中只有一个实例
为了保证系统中只有一个实例,我们肯定不能让别人调这个类的构造器。这里,我们不保证使用者已经知道不能去实例化这个类,而是通过一定的手段保证系统中只有这一个实例。然后为了让别人可以使用这个对象,我们必须对外提供一个入口,通过这个入口可以保证对这个对象的访问。其次就是在别人访问之前,我们要创建出这个实例,避免空指针访问。
单例模式的实现步骤
一般的实现思路是这样的:首先私有化构造器,避免其他人创建对象,然后做一个静态方法,通过这个静态方法得到该单例对象。最后则是考虑一下初始化的一些细节。
单例模式的具体实现
我们谈谈单例模式的6种实现方式以及它们的优缺点。
饿汉式实现单例
饿汉式指类一加载就创建出单例对象,无论要不要用到,我们都去创建。就像一个饿汉一样,不管三七二十一,看到就吃。实现思路也很简单,在定义单例类引用的时候就初始化就可以了。具体如下:
class Singlon {
private Singlon inst = new Singlon();
private Singlon() {}
public Singlon getInstance() {
return inst;
}
}
为了让别人可以很方便地得到单例对象,我们使用静态方法返回单例对象。为了能够在静态方法中访问数据,我们把单例对象的引用定义成static
的,同时在定义时就初始化,Singlon inst = new Singlon();
语句是在类加载过程中执行的,Java保证在类加载时是线程安全的,所以饿汉式是线程安全的,没有线程安全问题。缺点就是有可能会浪费内存,毕竟不管用不用,类一加载就创建出来单例对象,可能会降低程序性能,我们推荐的是懒加载,即用到的时候再加载。
懒汉式实现单例
懒汉式指只有用到的时候才创建单例对象。就像一个懒汉一样,不到万不得已绝不干活。它的实现也很简单,别人通过getInstance
方法访问单例对象,我们保证调用者在调这个方法时有单例对象就可以。具体如下:
class Singlon {
private static Singlon inst;
private Singlon() {}
public static Singlon getInstance() {
if (inst == null) {
inst = new Singlon();
}
return inst;
}
}
我们私有化了构造器,外界无法创建对象。提供了一个公有方法,来获得单例对象,在这个方法里,我们先判断inst
是不是null
,如果是,我们就创建对象,否则,直接返回inst,这样做到了懒加载,用到才创建对象。但问题也有,这种方式不是线程安全的,当这个代码运行在多线程环境时,单例对象可能会有多个,也就是说做不到单例了,单例变成了多例,这个方法就无法实现单例了。
具体的原因分析需要有多线程的基础,我简要介绍一下,看不懂也正常。其实也很简单,多个线程同时执行这份代码时,有可能出现线程1判断完了if (inst == null)
,准备进入if
语句内部,执行inst = new Singlon()
语句时被OS调度走,然后线程2执行,它也判断if (inst == null)
,这时候inst
也是null
,然后线程2创建了一个Singlon
对象,return inst
之后结束了方法调用,现在系统中已经有了一个Singlon
对象。之后OS调度线程1,线程1继续执行,它又创建了一个Singlon
对象,用新对象的地址更新了inst
成员,然后return
走掉了,现在系统中就有两个Singlon
对象了,也就不是单例了。但是这种实现方式在单线程环境下是没有问题的,可以正常使用。
线程安全的懒汉式实现单例
谈到线程安全,我们第一个想到的就是Java提供的synchronized
关键字,它是Java语言本身提供的一种同步方式。很明显,可以直接把synchronized
加在方法上,保证同一时间只有一个线程可以访问这个方法。具体如下:
class Singlon {
private static Singlon inst;
private Singlon() {}
public static synchronized Singlon getInstance() {
if (inst == null) {
inst = new Singlon();
}
return inst;
}
}
这样就不会出现多个线程同时访问getInstance
方法的情况,但这又引入了一个新的问题,想访问getInstance
方法的线程都得排队,都得排到这个方法上,一个个地去访问,这就降低了并发度。就像马路上一个个的小电驴正在自由自在地行驶在马路上,忽然前面堵了,一次只能让一辆电驴通过,那很显然,在这个地方会堵着一大堆小摩托,小电驴在这里就再也不能自由自在,快快乐乐的行驶了,它们必须得通过这个地方才能行驶。这里也是一样,我们希望能够保证线程安全,还要尽可能高地提供并发度,使多个线程可以同时执行这个代码。
基于双重检查的线程安全的懒汉式实现单例
于是有大佬拍了一下脑袋,做出了一个基于双重检查的线程安全的懒汉式实现。具体是这样的:
class Singlon {
private static Singlon inst;
private Singlon() {}
public static Singlon getInstance() {
if (inst == null) {
synchronized(Singlon.class) {
if (inst == null) {
inst = new Singlon();
}
}
}
return inst;
}
}
它的实现很巧妙。方法不再是同步方法,现在所有线程均可以进入该方法,之后在外面判断了一下单例对象有没有创建,如果没创建,就先竞争一个全局唯一的锁,只有拿到锁的线程才能才能往下走。拿到锁后,又判断了一下单例对象有没有创建,这是为了应对有多个线程同时进入第一个if
语句的情况。如果没有创建,就创建一个对象,如果已经创建了,就什么也不做。这种方法既保证了线程安全,又保证了并发,多个线程可以同时执行代码,互斥执行同步代码的情况也只会在单例对象还没创建的时候才会出现,一旦单例对象创建完成,整个方法就和synchoronized
代码块没有一点关系了。
但是他还会有问题,这个问题出现的概率极低。具体是这样:Java中使用new
关键字创建对象分成三个过程:为对象分配内存空间,初始化对象/初始化这块内存空间,将对象的地址返回给变量。JVM可能会把这三个过程中的后两个过程给重排序,也就是为对象分配内存空间,将对象的地址返回给变量,初始化对象/初始化这块内存空间。这就导致其他的线程在执行到第一个if (inst == null)
时,判定inst
不为null
,然后返回了inst
,结果在使用时发现单例对象还没有完成初始化(我们假设执行inst = new Singlon()
的线程在执行完将对象的地址返回给变量的字节码指令之后被OS调度走,没有执行初始化对象/初始化这块内存空间)。这就会导致很大的问题。这实际上是一个有序性问题,解决的办法也很简单,加一个volatile
禁止JVM对指令重排序就可以。完成的DCL是这样的:
class Singlon {
private static volatile Singlon inst;
private Singlon() {}
public static Singlon getInstance() {
if (inst == null) {
synchronized(Singlon.class) {
if (inst == null) {
inst = new Singlon();
}
}
}
return inst;
}
}
使用内部类实现单例
谈到这里,我们想做懒加载,还要保证线程安全,多线程环境下不出问题。我们可以使用Java的语法规则来做。类的加载过程是线程安全的,只有在一个类的静态成员或构造器被调用时,类才会加载,于是我们就可以使用内部类。代码如下:
class Singlon {
private Singlon() {}
public static Singlon getInstance() {
return Inner.INST;
}
private static class Inner {
private static final Singlon INST = new Singlon();
}
}
我们私有化单例类的构造器,然后创建一个内部类,让这个内部持有单例类的一个引用,并在类加载时初始化。这样在加载单例类Singlon
的时候并没有创建单例对象,只有在通过调用getInstance
方法访问单例对象的时候,JVM才会使用了Inner类的静态成员INST
,然后加载内部类Inner
,在加载过程中创建单例,做到了懒加载。线程安全则由JVM自身保证。
可以说这是一种比较理想的实现单例的方法,但缺点可能就是每个单例类都得再配一个内部类吧。
通过枚举类实现单例
枚举本身是一个类,只是它比较特殊,在定义类的时候就已经声明完了本身的所有实例对象。枚举实现单例很简单。
enum Singlon {
INST;
// other methods
}
创建了唯一一个枚举实例:INST,这保证了单例,同时Java保证枚举类的每个实例只会有一份,且保证加载过程的线程安全,所以通过枚举实现单例很方便。
对单例模式的破坏
但是,不得不提的是,有两种方法可以破坏单例,创建出另外的单例对象,让系统中不只有一个单例对象。最容易想到的一个办法是反射,通过反射可以访问一个类型的所有资源,包括私有化的构造器,所以调用者可以通过反射拿到构造器,然后通过构造器方法创建新的对象。
第二种方法就是使用对象的序列化机制,将内存中的单例对象以二进制流的形式拷贝出来一份,作为新的对象。
具体的示例也很简单,有需要再加上。
也有防止破坏单例的方法,对于使用反射破坏的,可以做一个标志位,该标志在单例对象创建后会置位,在构造器中检查这个标志,如果为ture,就抛出来一个异常,破坏对象的创建过程。对于使用对象序列化机制破坏单例的,可以在单例类中写一个private Object readResolve
方法,在这个方法中直接返回原来的单例对象。通过以上操作便可以防止对单例模式的破坏。
但必须生命的是,所有的方式对通过枚举实现单例的方法无效,枚举实现单例是很安全的一种方式。
最后
以上就是朴实战斗机为你收集整理的创建型模式之单例模式单例模式的全部内容,希望文章能够帮你解决创建型模式之单例模式单例模式所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复