Java设计模式之单例模式(Singleton)
稍微有点Java编程经验的人,对于设计模式和单例模式都不会很陌生。因为在很多人面试的时候,就会被问道你知道哪些模式啊?写个单例模式我看看?另一方面,单例确实用到的地方还不少,比如线程池,数据库连接池,HttpApplication都需要被设计为单例,也就是在全局只能有一个实例,如果它们存在多个实例,那这些实例创建的对象(thread、datasource等)管理起来就会出现混乱。
现在呢,我们就来说说单例模式的具体实现方式有哪些。
相信读者听得比较多的就是懒汉模式、饿汉模式。甚至你用一些拼音输入法打出全拼都能显示出它们的汉字。
懒汉模式
public class Singleton1 {
//懒汉模式 具备延迟加载特性
//初始化类但不创建类实例,只有在使用时才真正创建对象
//但该方法在多线程环境则有问题
private static Singleton1 singleton1;
private Singleton1(){}
public static Singleton1 getInstance(){
if(singleton1 == null){
singleton1 = new Singleton1();
}
return singleton1;
}
}
代码中首先创建一个私有的静态变量,类型就是我们这个单例类类型,接着创建了一个私有的构造器,只有创建了这样一个私有构造器,才能保证该类的实例只能在该类内部进行创建。接下来就是一个获取实例的方法,很简单,就是判断实例是否为空,如果为空,则创建这样一个实例,否则就说明实例已经被创建过了,则直接返回之前创建过的实例即可。这样这样一个全局唯一实例就创建完了。顺着这个类看,看不出一点问题,除了感觉创建私有构造方法和在自己类中声明自己类类型的变量让你感觉有点别扭。但是如果我们把创建类的环境放到多线程中去考虑,是不是就不是那么简单了。比如,两个线程同时走到getInstance方法判断singleton1是否为空的地方,thread1判断是null,就去创建这个实例,此时thread2刚好在thread1 new Singleton1()时,对singleton1进行判断,此时它仍是空,也去执行singleton1 = new Singleton1() 这段代码,那就相当于得到而两个不同的实例。
发现了这个问题,解决起来也很简单,对吧, 也就是在getInstance方法上加上synchronized 关键字就行了。也就是下面这种形式
懒汉模式(改进版1)
public class Singleton2 {
private static Singleton2 singleton2;
private Singleton2(){}
public static synchronized Singleton2 getInstance(){
if(singleton2 == null){
singleton2 = new Singleton2();
}
return singleton2;
}
}
这段代码满足能创建一个全局唯一实例,不存在线程安全问题,是可以用在实际项目中去了。但是我们细想一下,我们实例化对象的过程只会执行一次,也就是singleton2=new Singleton2() 代码实际只会执行一次,因为之后的方法调用就不会进入if代码块中,但是因为我们在方法上加了synchronized ,所以每次方法调用,该方法都会被加锁,都知道j加了synchronizd关键字的方法性能上就没有那么高。再往深处想, 如果该方法会被频繁调用,那么性能问题就会很突出了。所以有了下面的改进版代码。
懒汉模式(改进版2)
public class Singleton3 {
private static Singleton3 singleton3;
private Singleton3(){}
public static Singleton3 getInstance(){
if(singleton3 == null){
synchronized (Singleton3.class) {
if(singleton3==null){
singleton3 = new Singleton3();
}
}
}
return singleton3;
}
}
这段代码与上面代码的不同之处:声明的singleton3变量之前加了个volatile关键字,在getInstance方法中,使用了同步代码块,volatile机制,我们不展开,只是说明被volatile修饰的变量,在多线程环境下,如果一旦值被修改,修改后的值在其他线程中会立马得到刷新。同步代码块与方法使用synchronized修饰的区别,相信读者都明白,就是让被上锁的代码少一些。使用同步点块有一个好处,就是只有创建的代码才会被上锁。如果实例已经创建,则直接返回实例,与上面每次调用getInstance方法都要加锁相比,已经好了很多。上面这种创建实例的方法也是比较有名的“双重检查锁”模式,这种方式本身是没有任何问题的,近乎于完美,但是限于Java本身版本的问题,Java版本小于1.5的话,这种双重检查锁机制会失效。所以如果jdk是1.5之前的版本,不要使用这种方式。
饿汉模式
public class Singleton4 {
private static Singleton4 singleton4 = new Singleton4();
private Singleton4(){}
public static Singleton4 getInstance(){
return singleton4;
}
}
这段代码也比较简单,与基础版懒汉模式的区别是,饿汉模式在声明类类型变量时,就完成了对象的实例化,在getInstance方法中,直接返回实例对象,这样是这种写法被称为饿汉模式的原因,因为它急切希望得到实例。这种写法可以生成全局唯一实例,由于classloader机制(static修饰的变量),也不存在线程安全问题。该写法中,只要初始化类,就会实例化singleton4。不管这个实例有没有被用到,这也就是该写法的一点瑕疵,不具备延迟加载的特性(只有在使用时,才实例化)。
饿汉模式(变种,但并不是改进)
public class Singleton5 {
private static Singleton5 singleton5 = null;
static{
singleton5 = new Singleton5();
}
private Singleton5(){}
public static Singleton5 getInstnce(){
return singleton5;
}
}
这种写法与上面的写法存在同样的问题,在类初始化时,对象就被创建出来了,而不管它被创建出来有没有用。
除了上面的写法,我通过查资料,看到还有一些其他的单例模式代码的实现,我们也来分析一下。
静态内部类
public class Singleton7 {
private static class SingletonHelper{
private static final Singleton7 singleton7 = new Singleton7();
}
private Singleton7(){}
public static Singleton7 getInstance(){
return SingletonHelper.singleton7;
}
}
如代码所示,在Singleton7类内部又创建了一个静态内部类,静态内部类中声明了一个static final 修饰的Singleton7 变量,并进行了实例化。在getInstance方法中,则直接获取该内部类中成员变量singleton7。这种写法也依赖于JVM机制,确保无线程安全问题,可以创建全局唯一实例,由于未使用synchronized关键字,性能也较好,是一种不错的实现方法。
枚举实现
public enum Singleton6 {
singleton6
//......其他方法
}
该写法简单明了,且不存在线程安全的问题,另外,它也可避免使用反序列化重新构建新的对象。由于jdk1.5才引入枚举类型,所以对于jdk版本小于1.5的就不适用了。
以上就是关于单例模式实现方式的总结。另外本人从《Head First设计模式》中了解到说垃圾回收器会把单例对象“吃”掉,书中解释道,该bug存在于jdk1.2之前的版本,在1.2之后就已经被修正了,所以不需要担心这个问题。