在设计模式中,单例模式(Singleton)是最长见得一种设计模式之一。什么是单例模式呢?就是在整个系统中,只有一个唯一存在的实例。这样的情况可以干什么用呢?比如可以统计网站的访问量,一些连接池(数据库连接池等)。

一个最简单的单例模式 – 饿汉模式

那么怎么能保证只有一个对象的存在呢?首先得有一个static的实例,这个方法保证了一个class只有一个实例。还得防止外界使用构造器来new一个实例。

//一个没有封装的单例模式
public class Singleton {
    public static final Singleton singleton = new Singleton();
    private Singleton(){}
}

外界就可以使用Singleton.singleton这样的方法来调用了。但是这样存在的问题就是分装不够好,添加一个方法,返回singleton的引用。如下

//最简单的封装单例改进版。饿汉模式
public class Singleton {
    public static final Singleton singleton = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){ return singleton; }
}

当代码写到这,我们终于可以松一口气了,原来单例模式也很简单呀,就这么几行的代码。对,其实这个是最简单的一种方式,能够应付大部分的场景的。

不过,其他class在引用Singleton而不使用的时候,虚拟机会自动加载这个类,并且实例化这个对象(这点知道Java虚拟机的类加载就会了解那么一些)。于是我们就有了下面的写法。

延迟实例化(懒汉模式) – 在调用时进行实例化

经常能看见其他的单例模式会教下面的代码,这样的人估计是从《设计模式 – 可复用面向对象软件的基础》那本书看来的。这本书使用的是C++语言写的,然后就将其转到了Java平台来。

首先他们会说我们的代码应该先这样,在调用的时候,发现为null,再进行实例化该类。

public class Singleton {
    public static final Singleton singleton = null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(singleton == null){ //如果singleton为空,表明未实例化
           singleton = new Singleton();
        }
        return singleton;
    }
}

然后他们还会说,这样在多线程的情况下会出现这样的情况:两个进程都进入到if (singleton == null),于是两个线程都对这个进行实例化,这样就出问题啦。

所以他们又说应该使用synchronize关键字,在实例化之前,进行加锁行为。于是又产生了一下的代码

public class Singleton {
    public static final Singleton singleton = null;
    private Singleton(){}
    public static Singleton getInstance(){
        if(singleton == null){ //如果singleton为空,表明未实例化
           synchronize (Singleton.class){
               if( singleton == null ) { // double check 进来判断后再实例化。
                   singleton = new Singleton(); 
               } 
        }
        return singleton;
    }
}

他们就会说,他们的这个代码使用了double check (双重检测),以防止这样的情况:当两个线程执行完第一个singleton == null 后等待锁, 其中一个线程获得锁并进入synchronize后,实例化了,然后退出释放锁,另外一个线程获得锁,进入又想实例化,会判断是否进行实例化了,如果存在,就不进行实例化了。

他们会说,这样的代码perfect,很完美,既解决了多线程带来的问题,又解决了延迟实例化的方式。

我觉得这样的代码只是将C++版的单例模式复制到Java平台,没有Java的特色。第一个就是一个Java特色的代码,它解决了多线程的问题,因为JLS(Java Language Specification)中规定了一个类(Singleton.class)只会被初始化一次,但是不能解决延迟实例化的情况。如需要延迟实例化,可以看下面的方法,使用内部类来实现。

使用内部类的单例模式 (懒汉模式)

刚才说了,第一个不能解决延迟实例化Singleton对象的问题。所以我们使用内部类来进行,看看代码。

//一个延迟实例化的内部类的单例模式
public final class Singleton {
    
    //一个内部类的容器,调用getInstance时,JVM加载这个类
    private static final class SingletonHolder {
        static final Singleton singleton =  new Singleton();
    }

    private Singleton() {}

    public static Singleton getInstance() {
        return SingletonHolder.singleton;
    }
 }

我们来看看这个代码。首先,其他类在引用这个Singleton的类时,只是新建了一个引用,并没有开辟一个的堆空间存放(对象所在的内存空间)。接着,当使用Singleton.getInstance()方法后,Java虚拟机(JVM)会加载SingletonHolder.class(JLS规定每个class对象只能被初始化一次),并实例化一个Singleton对象。

这样做就可以解决前面说的对线程多次实例化对象延迟实例化对象的问题了。

缺点:不过你会使用这样复杂的方式嘛?代码那么多。只是为了延迟一个对象的实例化,引入另外一个class。就为了延迟那么一次对象延迟的实例化,延缓Java的heap堆内存。为此付出的代价是引入一个class,需要在Java的另外一个内存空间(Java PermGen 永久代内存,这块内存是虚拟机加载class文件存放的位置)占用一个大块的空间。

还存在的一些问题

好了,在许多情况其实用第一种方法就差不多可以了。在特殊情况下,还是存在着一些问题。

用反射生成对象

如果使用Java的反射机制来生成对象的话,那么单例模式就会被破坏。

//使用反射破坏单例模式
Class c = Class.forName(Singleton.class.getName());  
Constructor constructor = c.getDeclaredConstructor();  
constructor.setAccessible(true);  
Singleton singleton = (Singleton)ct.newInstance();  

对于用反射破坏单例模式的,是不对其进行代码保护的,即由此造成的后果,由写反射的构建单例实例的人负责。所以我们就不用担心反射带来的问题了。

分布式上,解决单例模式

对于分布式上的单例模式,应该使用RMI(Remote Method Invocation 远程方法调用)来进行。或者使用web serivce,在单个服务器上存在单例,其他的机器使用SOAP协议进行访问。

不同的ClassLoader(类加载器)加载Singleton

在不同的ClassLoader加载Singleton,他们是不一样的。就像在不同的package中相同的类名,他们是不同的类。同样的,在不同的ClassLoader上加载的类,他们尽管代码一样,还是属于不同的类。

这样,需要自己写classloader,保证Singleton.class的加载唯一。

参考文章:http://www.oschina.net/question/9709_102019

单例模式的对象是否会被JVM回收?

对于这个问题,在还没实例化单例的时候,对象不存在,单实例化后,那么singleton的引用就存在的,只要Singleton.class存在虚拟机中。那么什么时候Singleton.class会被回收呢?对于这个问题,牵扯了许多的问题。因为Singleton对象存在,所以Singleton.class就也存在,这样形成了相互依赖,所以不会被JVM垃圾回收。

网上有个文章验证了他的想法 http://blog.csdn.net/zhengzhb/article/details/7331354

使用反序列化生成对象

如果你的Singleton序列化了,那么通过反序列化方式可以生成一个对象。通过增加readResolve方法来解决。如下

//最简单的封装单例改进版。饿汉模式。序列化及反序列化解决
public class Singleton implements java.io.Serializable {
    public static final Singleton singleton = new Singleton();
    private Singleton(){}
    public static Singleton getInstance(){ return singleton; }
    private Object readResolve() {  return singleton;   }  
}

使用clone()克隆对象

Object的clone()方法默认是抛出CloneNotSupportedException异常的,所以只要不覆盖该方法,调用的时候,也会抛出异常。

effective java作者提到用enum枚举来实现单例模式。

扩展阅读:使用enum来进行Java的单例模式:http://coolxing.iteye.com/blog/1446648

声明:未经允许禁止转载 东东东 陈煜东的博客 文章,谢谢。如经授权,转载请注明: 转载自东东东 陈煜东的博客

本文链接地址: Java Singleton 单例模式 – https://www.chenyudong.com/archives/java-singleton.html