概要
单例模式是一种创建型设计模式(Creational Design Pattern),也是GOF23种设计模式中的一种。 从定义来看,这是一种非常简单的设计模式,但是在实现时还是又很多的考量。 在Java中实现单例模式,在开发者中是一个很有争议的话题,我们在这儿学习单例模式的原则,以及不同的实现方式,还包括使用时一些最佳实践
介绍
- 单例模式严格限制类的实例化,并且确保在JVM中只有一个实例
- 单例类必须提供一个全局的访问入口,用来获取单例实例
- 单例模式通常用被用于实现日志(Logging),驱动(Driver),缓存(Caching),线程池(Thread Pool)
- 单例模式也被用在一些其它的设计模式中,比如抽象工厂(Abstract Factory),建造者模式(Builder),原型模式(Propotype),门面模式(Facade)等
- java的一些核心类也使用了单例设计模式,比如
java.lang.Runtime
,java.awt.Desktop
Java单例模式实现
我们有很多方法来实现单例模式,但是都要遵循下列的原则
- 私有的构造器,限制从外部其它类实例化
- 私有静态单例类变量,即唯一实例对象
- 公共静态方法用于返回唯一实例对象,这是外部获取单例对象的唯一获取点
在下面的章节中,我们将用不同的方法来实现单例模式,并且考虑不同的实现的关注点
- 饱汉初始化
- 静态代码块初始化
- 懒汉初始化
- 线程安全单例
- Bill Pugh实现
- 反射破坏单例
- 枚举实现
- 序列化和单例
1. 饱汉初始化
在饱汉初始化中,在类加载时初始化单例对象,这是最简单的一种单例实现方式。但是它有一个缺点,可能应用程序没有获取和使用单例对象,但都会创建它
下面是静态初始化单列类
package com.journaldev.singleton;
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
//private constructor to avoid client applications to use constructor
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance(){
return instance;
}
}
2. 静态代码块初始化
静态代码块初始化和饱汉初始化类似,区别在于它是在静态代码块中初始化单例对象,这为处理异常提供了可能
package com.journaldev.singleton;
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton(){}
//static block initialization for exception handling
static{
try{
instance = new StaticBlockSingleton();
}catch(Exception e){
throw new RuntimeException("Exception occured in creating singleton instance");
}
}
public static StaticBlockSingleton getInstance(){
return instance;
}
}
不管是饱汉初始化还是静态代码块初始化来创建单例对象,都是在使用单例对象前就创建了实例,这并不是一个很好的实践。所以在下面的章节中,我们将学习如何通过延迟初始化来创建单例类
3. 懒汉初始化
懒汉初始化是在全局访问方法中创建单例对象,下面是一段该方式的实现代码
package com.journaldev.singleton;
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton(){}
public static LazyInitializedSingleton getInstance(){
if(instance == null){
instance = new LazyInitializedSingleton();
}
return instance;
}
}
上面的实现范式在单线程环境下没啥问题,但是在多线程场景下会有问题。多线程同时访问时,将破坏单例模式,不同的线程可能获取到不同实例对象,在下面的章节中,我们可以用不同的方式来实现线程安全的单例类
4. 线程安全单例
把全局访问方法设置为线程安全的Synchronized是一种简单的解决方式,这样在同一个时刻只会有一个线程访问该方法。常见的实现方法代码如下
package com.journaldev.singleton;
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton(){}
public static synchronized ThreadSafeSingleton getInstance(){
if(instance == null){
instance = new ThreadSafeSingleton();
}
return instance;
}
}
上面的实现可以很好的实现线程安全,但因同步方法锁的开销,也降低了性能,其实我们只需要在最初的几个访问线程中保证线程隔离。为了避免每一次访问有这种额外的开销,可以使用双重锁(double checked locking )。 在此方法中,同步代码块中有一次额外的检查来保证只有一个单例对象被创建。
下面是双重锁单例的代码块
public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
if(instance == null){
synchronized (ThreadSafeSingleton.class) {
if(instance == null){
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
5. Bill Pugh 实现
在Java5之前,java内存模式存在很多的问题,在特定的场景下,多线程并发获取单例时上面的单例方式常常会失败。 所以Bill Pugh提出了一个不同的方法来创建单例,用一个内部静态帮助类来实现,代码类似如下
package com.journaldev.singleton;
public class BillPughSingleton {
private BillPughSingleton(){}
private static class SingletonHelper{
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance(){
return SingletonHelper.INSTANCE;
}
}
可以看到private inner static class 包含了一个单例实例,当单例类被加载,SingletonHelper
此时并没有被加载,并且只有在被调用 getInstance 方法时,该类才会被加载,这时单例实例会被初始化
这是一个被广泛使用的单例实现方法,它不依赖于同步。我也在不同的项目中使用这种方式,而且它也非常的容易被理解和实现
6. 使用反射破坏单例
Reflection can be used to destroy all the above singleton implementation approaches. Let’s see this with an example class.
反射可以用来破坏上面所有的单例实现模式,让我们看一个例子
package com.journaldev.singleton;
import java.lang.reflect.Constructor;
public class ReflectionSingletonTest {
public static void main(String[] args) {
EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton instanceTwo = null;
try {
Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
//Below code will destroy the singleton pattern
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
运行上面的测试方法,可以看到这些实例的hashcode不相同,这表明了单例模式被破坏了。反射非常强大,它被用在很多框架的实现中,比如Spring,Hibernate
7. 枚举单例
为了避免反射场景, Joshua Bloch建议使用枚举来实现单例,Java中保证枚举中的任意枚举值只被初始化一次。因为枚举的值可以被全局访问,所以是一个单例。但枚举这种方式也缺乏弹性,举个例子,他不能够被赖加载
package com.journaldev.singleton;
public enum EnumSingleton {
INSTANCE;
public static void doSomething(){
//do something
}
}
8. 序列化和单例
有时候在分布式系统中,我们需要在单例类上实现Serializable接口。我们可以保存状态到文件系统中,并且在后续的获取。这儿是一个小的实现了Serializable接口单例类
package com.journaldev.singleton;
import java.io.Serializable;
public class SerializedSingleton implements Serializable{
private static final long serialVersionUID = -7604766932017737115L;
private SerializedSingleton(){}
private static class SingletonHelper{
private static final SerializedSingleton instance = new SerializedSingleton();
}
public static SerializedSingleton getInstance(){
return SingletonHelper.instance;
}
}
这儿序列化的单例类有一个问题,无论什么时候反序列化,都会是一个全新的实例对象。让我们同一个简单的程序来看
package com.journaldev.singleton;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class SingletonSerializedTest {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
SerializedSingleton instanceOne = SerializedSingleton.getInstance();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
"filename.ser"));
out.writeObject(instanceOne);
out.close();
//deserailize from file to object
ObjectInput in = new ObjectInputStream(new FileInputStream(
"filename.ser"));
SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
in.close();
System.out.println("instanceOne hashCode="+instanceOne.hashCode());
System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
}
}
程序输出如下:
instanceOne hashCode=2011117821
instanceTwo hashCode=109647522
所以破坏了单例模式,我们可以通过实现readResolve()
方法来解决这个问题
protected Object readResolve() { return getInstance(); }
到这儿,你会发现不同实例的hashcode是一样的 我希望这篇文章能够帮助你很好的掌握单例模式的细节,如果你有一些想法可以留言让我们知道