单例的写法

来源:互联网 发布:通过网线共享网络 编辑:程序博客网 时间:2024/05/16 12:56

一、关于单例的几个写法

    虽然单例是一个很普遍的设计模式,但是可见很多人并没有掌握它的正确写法,所以这里也有必要来进一步讨论下单例的写法。首先单例必须要有一个private访问级别的构造函数,只有这样,才能确保单例不会在系统中的其他代码内被实例化。其次instance成员变量必须要是static的。

1、饿汉模式创建单例

public class Singleton {       private static Singleton instance = new Singleton();       private Singleton (){     }
public static Singleton getInstance() { return instance; } }
    这种单例的实现方式非常简单也很可靠,instance成员变量的实例是在虚拟机加载类的时候就会被创建。唯一的不足的是无法对Singleton实例做延时实例化,一旦Singleton的构造过程很耗时,这将导致创建该单例的调用方遇到性能问题。所以该方式的单例适用于构造单例不耗时的场景。

2、懒汉模式

 public class Singleton {        private static Singleton instance;        private Singleton (){}      public static synchronized Singleton getInstance() {        if (instance == null) {            instance = new Singleton();        }        return instance; 
} }

    这种写法可以避免在类加载的时候就初始化实例,但是getInstance()方法必须是同步的,否则在多线程环境下,实例就可能会不止一个的被创建。虽然懒汉模式实现了延迟加载功能,但和第一种方法相比,它引入了同步关键字,因此在多线程环境中,以及频繁调用的时候,性能损耗就会比较明显。即使在单线程环境下,synchronized关键字也会带来一定的性能损耗,我们不妨用简单的代码对上述单例做个测试,以下数据是10次的平均值。

for (int i = 0; i < 1000000; i++) {Singleton s = Singleton.getInstance();}

     100万次的调用该接口,在只有一个线程的情况下,使用了synchronized关键字耗时106ms,没有加该关键字72ms,性能接近1.5倍的差值如果10个线程同时运行,synchronized的影响就很明显了,在有synchronized的情况下耗时4436ms,不使用的情况下只要192ms,相差有23倍。这还只是一个接口的调用,如果有代码逻辑等耗时函数,那synchronized对性能的影响就更大。所以该写法并不利于性能,还需要优化。

3、使用双重检查锁的懒汉模式

public class Singleton {        private volatile static Singleton instance;        private Singleton (){}         public static Singleton getSingleton() {        if (instance == null) {            synchronized (Singleton.class) {            if (instance == null) {                instance = new Singleton();            }          }       }       return instance;       }   }

    该写法使用了双重检查锁,即在instance为空的时候,还需要加同步锁进行保护。注意这里有一个volatile的关键字。如果不加volatile关键字,双重检查就可能会失效,导致可能不止一个实例被创建。为什么需要加volatile,编译器会对代码做优化,CPU的高速缓存和内存速度相差也很大等等导致了指令重排序。例如:编译器可能会进行指令重排序;处理器可能会对语句所对应的机器指令进行重排序之后再执行,甚至并发地去执行;内存系统(由高速缓存控制单元组成)可能会对变量所对应的内存单元的写操作指令进行重排序;编译器、处理器以及内存系统可能会让两条语句的机器指令交错;编译器、处理器以及内存系统可能会导致代表两个变量的内存单元在连续的check调用之后的某个时刻才更新。

    在Java 1.5以后,volatile才开始真正的有效,它有两个主要的意义:

    1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  2)保证时序,禁止进行指令重排序。
    
    但是volatile并不能保证原子性,即不能起到同步关键字的作用。

    所以这里需要加入volatile关键词来保证单例的创建只有一份,对于指令重排等知识大家也可以参考网络上的其他文章进一步了解。

4、静态内部类

public class Singleton {private Singleton() {}public static  Singleton getInstance() {return SingletonHolder.instance;}private static class SingletonHolder {private static  Singleton instance = new Singleton();}}

     该写法使用了内部类的方式实现单例,即可以做到延迟加载,也不必使用同步关键字,因为静态内部里的实例化过程由虚拟机保证了线程的安全性,是一种比较好的实现方式。

5、枚举方式

 public enum Singleton {       INSTANCE;       public void getSingleton() {return INSTANCE;     }   } 

     用枚举的方式实现单例在代码中很少见,因为只有Java 1.5以后才开始支持。这种写法比较简洁,还提供了序列化的机制,又能绝对防止多次的实例化,即使在序列化和反射攻击的时候,仍然能正常工作。

    上面列举了5种单例的写法,3、4、5写法在性能上都有不错的表现,枚举方式不常见但是安全性高。写法4、5的主要缺点就是该单例很难灵活来做到自身的销毁和重建。而写法3双重检查锁的模式,可以比较方便的释放和重建自身,例如在模块退出的时候,如果这个单例占用了大量的资源,可以使用 instance = null 来释放资源,后面要使用的时候可以重新创建。在单例占据大量资源,而该模块又不是频繁调用的模块,不需要常驻内存的情况下,建议使用第3种写法。

二、保证系统中单例的唯一性

    虽然前面的写法已经能够保证在普通情况下,系统中只有一个单例的实例,但是我们还是可以通过反序列化、反射的手段来生成多个单例实例。

1、防止反射机制破坏单例


    反射机制是通过调用私有的构造函数来创建实例的,这样就可以绕过单例私有构造函数的屏障。为了抵御这种创建实例的方法,可以修改构造函数,在被要求创建第二个实例的时候抛出异常。这样就不能通过反射创建实例了,这也是很多工具类防止实例化的做法,在构造函数中抛异常。

2、防止反序列化破坏单例


   如果该单例能够被序列化,那么在反序列化的时候就会创建一个新的实例。为了防止序列化破坏单例的实现,可以在单例中增加一个如下的方法。

private Object readResolve(){    return instance;}

    反序列化的过程中会检测是否有一个私有的,返回类型是Object的readResolve,如果存在就会使用该方法来执行反序列化。从而防止序列化破坏单例的实现。 

    
    上面谈了多种单例的写法和防止多个实例的手段,在我们写代码的过程中也尽量多考虑一下单例的写法,保证单例的正确性和性能上的优越性。当然,单例也不能随意的使用,毕竟单例泛滥容易导致内存的难以管理甚至泄露,过多的占用内存,特别是那些难得使用到的模块,如果大量的使用单例,又不注意销毁的话,就会浪费内存资源和产生内存碎片。

0 0