一、什么是单例?
单例模式指的是保证一个类只有一个实例,并且提供一个全局可以访问的入口。举个例子:就像分身术,虽然分身有很多,但是每一个分身都对应同一个真身。
二、为什么需要单例?
第一、为了节省内存、节省计算。在很多时候我们只需要一个单例就够了,如果出现了更多实例,反而属于浪费。举个例子(Example A),
/**
*Example A
*/
public class ExpensiveSource{
public ExpensiveSource(){
field1 = //查询数据库
field2 = //大量的计算
field3 = //其它耗时操作
}
}
我们拿一个初始化比较耗时的类来说,在实例话这个类的时候,要从数据库进行很多的查询,然后还要做很多的计算,所以在第一次构造的时候,我们需要花费很多时间来实例化这个对象。但是假设我们数据库例的数据是不变的,并且我们把这个对象保存到了内存当中,那么以后我们就可以使用同一个实例了。如果我们每次都重新生成新的实例化,那实在是没有必要。
第二、保证结果的正确,比如我们需要一个全局的计数器,用来统计人数,如果有多个实例的话,反而造成混乱。另外,为了方便管理,很多工具类我们只需要一个实例,那么我们通过提供一个统一入口,比如getInstance()方法,就可以获取到一个单例,这是很方便的。太多的实例不但没有帮助,反而让我们眼花缭乱。
三、单例模式的适用场景
1、无状态的工具类,如日志工具,我们处理用它记录日志之外,并不需要在它的实例上做任何存储状态,这时候我们只需要一个实例就可以了。
2、全局信息类,全局计数、环境变量等,比如我们在一个类上记录网站的访问次数,我们不希望有写记录在对象A上,有些记录在对象B上,这时候我们就可以使用单例对象来记录,在需要记录的时候拿出来用就可以了。对于全局的环境变量类也是如此。
四、常见的写法
-
饿汉式
1 /** 2 * 饿汉式 3 */ 4 public class Singleton{ 5 //用static修饰实例 6 private static Singleton singleton = new Singleton(); 7 //构造函数用private修饰 8 private Singleton(){ 9 10 } 11 12 public static Singleton getInstance(){ 13 return singleton; 14 } 15 }
1 /**
2 * 静态代码库式
3 */
4 public class Singleton{
5
6 private static Singleton singleton;
7
8 static{
9 singleton = new Singleton();
10 }
11
12 private Singleton(){
13
14 }
15
16 public static Singleton getInstance(){
17 return singleton;
18 }
19 }
在类装载时就完成了实例化,避免了线程同步的问题。缺点是在类装载是就完成了实例化,而没有达到懒加载的效果,如果这个从始至终实例没被使用,就会造成内存的浪费。这个和饿汉式的加载过程类似,只不过把实例化放在了静态代码块中进行。也是在类加载时,就执行了静态代码块中的代码,完成了实例的初始化,所以缺点也是如果类不被使用,就会造成内存的浪费。
2、懒汉式
1 /** 2 * 懒汉式 3 */ 4 public class Singleton{ 5 6 private static Singleton singleton; 7 8 private Singleton(){ 9 10 } 11 12 public static Singleton getInstance(){ 13 if(singleton == null){ //1 14 singleton = new Singleton(); //2 15 } 16 return singleton; 17 } 18 }
这中写法在调用getInstance()方法时才去实例化我们的对象,起到了懒加载的效果,但是只能在单线程下使用,如果在多线程下使用,一个线程进入了1位置,还没来得及执行2处代码,另外一个线程也进入了1处代码,然后实例化了对象,而第一个已经做了为空的判断,所以也会执行2处代码,这时就会出现多次创建实例。所以在多线程环境下不能使用这种方式。多线程环境下这样写是错误的。那么线程安全的懒汉式该怎么写呢?我们在getInstance()方法上加synchronized关键字,以此来解决刚才的线程安全问题。不过这种方法的缺点就是效率低下,每个线程在执行getInstance()方法获取实例时都要进行同步。多个线程不能同时访问,这在大多时候是没有必要的。那么为了提高效率,缩小同步范围,就把synchronized关键字从方法上移除了,然后再把synchronized关键字刚在方法内部,采用代码块的方式保证线程安全。
1 /** 2 * 懒汉式 3 */ 4 public class Singleton{ 5 6 private static Singleton singleton; 7 8 private Singleton(){ 9 10 } 11 12 public static Singleton getInstance(){ 13 if(singleton == null){ //1 14 synchronized(Singleton.class){ //2 15 singleton = new Singleton(); //3 16 } 17 } 18 return singleton; 19 } 20 }
不过这种写法也是有问题的,假如一个线程执行了1处代码,但还没往下执行,这时候另外一个线程也执行了1处代码,然后获取锁了锁,执行完3处代码并释放锁后,第一线程就直接获取锁,并执行3处代码,这也会出现多次创建实例。所以为了解决这个问题,就有了另一种写法,双重检查式。
3、双重检查式
1 /** 2 * 双重检查式 3 */ 4 public class Singleton{ 5 6 private static volatile Singleton singleton; 7 8 private Singleton(){ 9 10 } 11 12 public static Singleton getInstance(){ 13 if(singleton == null){ 14 synchronized(Singleton.class){ 15 if(singleton == null){ 16 singleton = new Singleton(); 17 } 18 } 19 } 20 return singleton; 21 } 22 }
我们重点看一下getInstance()方法,我们进行了两次singleton==null判断,就可以保证线程安全了,这样实例化代码只会被调用一次,后边只需要调用第一个if就可以了,然后会跳过整个if块,直接return实例化对象,这种写法的优点时不仅线程安全,而且延迟加载,效率也会更高。那么去掉第一个if判断语句块行不行呢,答案肯定是不行,如果去掉第一个if块,所有的线程都会串行执行,效率会很低,所以两个check都要保留。
那为什么要加volatile关键字呢,主要是因为singleton = new Singleton()这句话,这并非是一个原子操作,事实上在JVM中,这一句至少做了三件事,
第1步:给singleton分配内存空间,
第2步:调用Singleton的构造函数等来初始化singleton
第3步:将singleton对象执行分配的内存空间(执行完这步singleton就不是null了)
这里要留意一下这三步的顺序,因为存在着重排序的优化,也就是第2步和第3步的顺序是不能保证的,最终的顺序可能是1-2-3,也可能是1-3-2,如果是1-3-2的话,那么在第3步执行完之后,singleton就不是null了,假设此时线程2进入了getInstance()方法,因为这时singleton已经不是null了,所以他就会通过第一层检查,直接返回,但这时对象并没有完全被初始化,所以在使用这个对象的时候就会报错。所以使用volatile这个关键字的主要意义就是防止刚才的重排序的发生,避免了拿到未完全初始化的对象。
4、静态内部类的写法
1 /** 2 * 静态内部类方式 3 */ 4 public class Singleton{ 5 6 private Singleton(){} 7 8 private class static SingletonInstance{ 9 private static final Singleton singleton = new Singleton(); 10 } 11 12 public static Singleton getInstance(){ 13 return SingletonInstance.singleton; 14 } 15 }
它跟饿汉式采用的机制类似,都采用了类装载的机制,来保证我们初始化实例的只有一个线程,所以这里,JVM帮助我们保证了线程的安全性。不过呢,饿汉式有一个特点呢就是只要singleton这个类被加载了,就会实例化这个单例对象,而静态内部类这种方式在类装载时并不会立刻实例化这个对象,而是在需要实例时,也就是在调用getInstance()方法时才会去实例化这个对象。
这里做个小总结,静态内部类的方式和双重检查式的有点是一样的,都是避免了线程不安全的问题,并且实现了延迟加载。效率高, 可以看出,两种方式都是不错的写法,但是他们不能防止反序列化生成多个实例,所以更好的方式就是枚举类的方法。
5、枚举类
1 /** 2 * 枚举方式 3 */ 4 public enum Singleton{ 5 6 INSTANCE; 7 8 public void whateverMethod(){ 9 } 10 }
借助JDK1.5中添加的枚举类来实现单例模式。这不仅能避免多线程同步的问题,还能防止反序列化和反射来创建新的对象,来破坏单例的情况出现。
Joshua Bloch说过使用枚举实现单例的方式虽然还没有被广泛采用,但是单元素的枚举类型已经成为了实现Singleton的最佳方法。为什么他比较推崇枚举的这种方式呢,那就要回到枚举这种方式的优点上来说了,枚举写法的优点有这么几个,首先是枚举类写法简单,不需要我们自己去考虑懒加载、线程安全等问题,同时代码比较短小精悍,比其他任何方式都更简洁,第二个优点是线程安全有保障,通过反编译一个枚举类我们可以发现,枚举中的各个枚举项,是通过static代码块来定义和初始化的,他们会在类加载时完成初始化,而Java类的加载由JVM来保证线程的安全,所以呢创建一个Enum类型的枚举是线程的安全的。前面集中方式是可能存在问题的,那就是存在被反序列化破坏,反序列化生成的新的对象从而产生多个实例。java是对枚举的序列化做了规定,在序列化时,仅仅是将枚举对象的name属性输出到结果中,在反序列化时,就是通过java.lang.Enum的valueOf方法来根据名字查找对象,而不是新建一个新的对象,所以这就防止了反序列化导致单例破坏问题的出现。对于反射破坏单例问题,枚举类同样有防御措施,反射在通过newInstance创建对象时,会检查这个类是否是枚举类,如果是的话就抛出illegalArguementException("cannot reflectively create Enum objects")这样的异常,反射创建失败。可以看出枚举方式能防止反序列化和反射破坏单例,这一点上有很大的优势,安全问题不容小事,一旦生成了多个实例单例模式就彻底没用了,