如何创建一个完美的单例模式

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何创建一个完美的单例模式相关的知识,希望对你有一定的参考价值。

单例模式的目的是什么

单例类的目的是控制对象创建,约束对象的数量有且只有一个。单例模式只允许有一个入口来创建类的实例。

因为只有一个单例类的实例,任何单例类的实例都将之会产生一个类,就像静态域。当你需要控制资源的时候,如何数据库连接池、线程池或者使用sockets,单例模式是非常有用的。

下面我们来创建一个单例类。

创建单例类

为了实现单例类,最简单的方式就是将构造器私有化设置为private。有两种初始化方式

饿汉式

饿汉式初始化,单例类的实例在类加载的时候被创建,这是创建单例类最简单的方法。

通过将构造器声明为private,不允许其他类来创建单例类实例。取而代之的是,创建一个静态方法(一般命名为getInstance)来提供创建类实例的唯一入口。

 1  package com.net;
 2 /**
 3  * Created by admin on 2017/9/28.
 4  */
 5 public class SingletonClass {
 6     private static SingletonClass singletonClass = new SingletonClass();
 7     //private constructor
 8     private SingletonClass(){
 9 
10     }
11     public static SingletonClass getInstance(){
12         return singletonClass;
13     }
14 
15 }

这种方法有一个缺陷,就是中程序没有使用到它的时候,实例已经被创建了。当你创建数据库连接或者socket的时候,这可能成为一个相当打的问题,会导致内存泄漏问题。解决的方法是,在需要的时候

再创建实例,我们称为懒汉式初始化。

懒汉式

与饿汉式相反,你在getInstance()方法中初始化类实例。方法中判断实例是否已经创建,如果已经创建,则返回旧的实例,反之在JVM中创建新的实例并返回。

 1 package com.net;
 2 
 3 /**
 4  * Created by admin on 2017/9/28.
 5  */
 6 public class SingletonClass {
 7     private static SingletonClass singletonClass = null;
 8 
 9     private  SingletonClass(){
10 
11     }
12 
13     public  static  SingletonClass getInstance(){
14         if (singletonClass == null){
15             singletonClass = new SingletonClass();
16         }
17         return singletonClass;
18     }
19 
20 }

 

我们都知道这JAVA中,如果两个对象是相同的,那么他们的hashCode也是相同的。我们测试一下,如果上面的单例类都正确,那么他们应该会返回相同的哈希值。

 1 package com.net.test;
 2 
 3 import com.net.SingletonClass;
 4 
 5 /**
 6  * Created by admin on 2017/9/28.
 7  */
 8 public class Test {
 9     public static void main(String[] args) {
10         SingletonClass singletonClass1 = SingletonClass.getInstance();
11         SingletonClass singletonClass2 = SingletonClass.getInstance();
12         System.out.println("singletonClass1hashcode:"+singletonClass1.hashCode()); 
13 System.out.println("singletonClass2hashcode:"+singletonClass1.hashCode());
14 }
15 }

输出日志:

singletonClass1hashcode:21029277
singletonClass2hashcode:21029277

可以看到两个实例拥有相同的的hashcode。所以,这就意味着上面的代码创建了一个完美的单例类,是吗?回单是no

让单例类反射安全

在上面的单例类中,通过反射可以创建不止一个实例。Java Reflection 是一个在运行时检测或者修改类的运行时行为的过程。通过在运行时修改构造器的可见性并通过构造器创建实例可以产生新的单例实例。

如下代码:

 1 import java.lang.reflect.Constructor;
 2 import java.lang.reflect.InvocationTargetException;
 3 
 4 /**
 5  * Created by admin on 2017/9/28.
 6  */
 7 public class Test {
 8     public static void main(String[] args) {
 9         SingletonClass singletonClass1 = SingletonClass.getInstance();
10         SingletonClass singletonClass2 = null;
11 
12         try {
13             Class<SingletonClass> clazz = SingletonClass.class;
14             Constructor<SingletonClass> cons = clazz.getDeclaredConstructor();
15             cons.setAccessible(true);
16                 singletonClass2 = cons.newInstance();
17         } catch (Exception e) {
18             e.printStackTrace();
19         }
20         System.out.println("singletonClass1hashcode:"+singletonClass1.hashCode()); 
21     System.out.println("singletonClass2hashcode:"+singletonClass2.hashCode());
22 }
23 }

输入日志:

singletonClass1hashcode:21029277
singletonClass2hashcode:24324022

可以看到每一个实例都有不同的hashCode。显然这个单例类是不合格的。

解决方案:

为了防止反射导致的单例失败,当构造器已经初始化并且其他类再次初始化时,抛出一个运行时异常。下面是更新后的代码:

 1 package com.net;
 2 
 3 /**
 4  * Created by admin on 2017/9/28.
 5  */
 6 public class SingletonClass {
 7     private static SingletonClass singletonClass = null;
 8 
 9     private  SingletonClass(){
10         if(singletonClass != null){
11             throw new RuntimeException("Use getInstance method to get the single instance of this class");
12         }
13     }
14 
15     public  static  SingletonClass getInstance(){
16         if (singletonClass == null){
17             singletonClass = new SingletonClass();
18         }
19         return singletonClass;
20     }
21 
22 }

让单例类线程安全

如果两个线程几乎同时去尝试初始化单例类,将会发生什么?测试下面代码

 1 import com.net.SingletonClass;
 2 
 3 import java.lang.reflect.Constructor;
 4 import java.lang.reflect.InvocationTargetException;
 5 
 6 /**
 7  * Created by admin on 2017/9/28.
 8  */
 9 public class Test {
10     public static void main(String[] args) {
11         Thread thread1 = new Thread(new Runnable() {
12             @Override
13             public void run() {
14                 SingletonClass singletonClass1 = SingletonClass.getInstance();
15                 System.out.println("singletonClass1 hashcode:"+singletonClass1);
16             }
17         });
18         Thread thread2 = new Thread(new Runnable() {
19             @Override
20             public void run() {
21                 SingletonClass singletonClass2 = SingletonClass.getInstance();
22                 System.out.println("singletonClass2 hashcode:"+singletonClass2);
23             }
24         });
25         thread1.start();
26         thread2.start();
27 
28     }
29 }

如果你多次运行这些代码,有时候你会发现不同的线程出现了不同的实例。如下:

singletonClass2 hashcode:21029277
singletonClass1 hashcode:24324022

这说明了你的单例类并不是线程安全的。所有的线程同时调用getInstance()方法时,singletonClass==null条件对所有的线程返回值,所以两个不同的实例被创建出来。这就打破了单例的原则。

解决方案

同步getInstance()方法

 1 package com.net;
 2 
 3 /**
 4  * Created by admin on 2017/9/28.
 5  */
 6 public class SingletonClass {
 7     private static SingletonClass singletonClass = null;
 8 
 9     private  SingletonClass(){
10         if(singletonClass != null){
11             throw new RuntimeException("Use getInstance method to get the single instance of this class");
12         }
13     }
14 
15     public synchronized static  SingletonClass getInstance(){
16         if (singletonClass == null){
17             singletonClass = new SingletonClass();
18         }
19         return singletonClass;
20     }
21 
22 }

在外面同步getInstance()方法之后,第二个线程必须等到第一个线程执行完getInstance()方法之后才能执行,这就保证了线程安全。

但是,这个方法同样有一些缺点:

 锁的开销导致运行变慢

实例变量初始化之后的同步操作是不必要的双检查锁

使用双检查锁 方法创建实例可以克服上面的问题。

这种方法中,当实例为空时,中同步代码块中创建实例,这样只有当singletonClass为空的时候,同步代码块才会执行,避免了不必要的同步操作。

 1 package com.net;
 2 
 3 /**
 4  * Created by admin on 2017/9/28.
 5  */
 6 public class SingletonClass {
 7     private static SingletonClass singletonClass = null;
 8 
 9     private  SingletonClass(){
10         if(singletonClass != null){
11             throw new RuntimeException("Use getInstance method to get the single instance of this class");
12         }
13     }
14 
15     public  static  SingletonClass getInstance(){
16         
17         if (singletonClass == null){ //check for the first time
18             synchronized (SingletonClass.class){
19               if(singletonClass ==null){//check for the second time
20               锁 = new SingletonClass();
21               }
22             }
23         }
24         return singletonClass;
25     }
26 
27 }

使用volatile关键字

表面上看,这个方法看起来很完美,你只需要付出一次静态代码块的代价。但是除非你使用volatile关键字,否则单例仍然会被打破。

没有volatile修饰符,另一个线程可能在变量 singletonClass正在初始化尚未完成时引用它。但是通过volatile的保证happens-before关系,所有对于singletonClass变量的写操作都会在读操作之前发生。

 1 package com.net;
 2 
 3 /**
 4  * Created by admin on 2017/9/28.
 5  */
 6 public class SingletonClass {
 7     private static volatile SingletonClass singletonClass = null;
 8 
 9     private  SingletonClass(){
10         if(singletonClass != null){
11             throw new RuntimeException("Use getInstance method to get the single instance of this class");
12         }
13     }
14 
15     public  static  SingletonClass getInstance(){
16 
17         if (singletonClass == null){ //check for the first time
18             synchronized (SingletonClass.class){
19               if(singletonClass ==null){//check for the second time
20               singletonClass = new SingletonClass();
21               }
22             }
23         }
24         return singletonClass;
25     }
26 
27 }

现在上面的单例类是线程安全的。在多数线程应用环境中保证单例类的线程安全是必须的。

让单例类序列化安全

 在分布式的系统中,有些情况下你需要做单例类中实现Serralizable接口。这样你可以在文件系统中存储它的状态并且在稍后的某一事件点取出。

让我们测试一个这个单例类中序列化和反序列化之后是否仍然保持单例。 

 1 package com.net.test;
 2 
 3 import com.net.SingletonClass;
 4 
 5 import java.io.*;
 6 import java.lang.reflect.Constructor;
 7 import java.lang.reflect.InvocationTargetException;
 8 
 9 /**
10  * Created by admin on 2017/9/28.
11  */
12 public class Test {
13     public static void main(String[] args) {
14         try {
15             SingletonClass singletonClass1 = SingletonClass.getInstance();
16             ObjectOutput out = null;
17             out = new ObjectOutputStream(new FileOutputStream("filename.cc"));
18             out.writeObject(singletonClass1);
19             out.close();
20             //deserialize from file to object
21             ObjectInput in = new ObjectInputStream(new FileInputStream("filename.cc"));
22              SingletonClass singletonClass2 = (SingletonClass)in.readObject();
23              in.close();
24             System.out.println("singletonClass1hashcode:"+singletonClass1.hashCode());
25             System.out.println("singletonClass2hashcode:"+singletonClass2.hashCode());
26         } catch (IOException e) {
27             e.printStackTrace();
28         } catch (ClassNotFoundException e) {
29             e.printStackTrace();
30         }
31 
32     }
33 }

 

输出结果:

singletonClass1hashcode:23050916
singletonClass2hashcode:9286386

显然看到实例的hashcode是不同的,违反了单例原则。序列化单例类后,我们反序列化时,会创建一个新的类实例。为了预防另一个实例的产生,你需要提供readResolve()方法的实现。readResolve()

代替了从流中读取对象。这就确保了中序列化和反序列化的过程中没有人可以创建新的实例。

 1 package com.net;
 2 
 3 import java.io.Serializable;
 4 
 5 /**
 6  * Created by admin on 2017/9/28.
 7  */
 8 public class SingletonClass implements Serializable{
 9     private static volatile SingletonClass singletonClass = null;
10 
11     private  SingletonClass(){
12         if(singletonClass != null){
13             throw new RuntimeException("Use getInstance method to get the single instance of this class");
14         }
15     }
16 
17     public  static  SingletonClass getInstance(){
18 
19         if (singletonClass == null){ //check for the first time
20             synchronized (SingletonClass.class){
21               if(singletonClass ==null){//check for the second time
22               singletonClass = new SingletonClass();
23               }
24             }
25         }
26         return singletonClass;
27     }
28     //make singleton from serialize and deserialize operation
29     protected Object readResolve(){
30         return getInstance();
31     }
32 }

 

 


 

以上是关于如何创建一个完美的单例模式的主要内容,如果未能解决你的问题,请参考以下文章

我去,这才是完美的单例模式!

如何写一个简单的单例模式?

最完美的单例模式

最完美的单例模式

最完美的单例模式

最完美的单例模式