JAVA设计模式之单例模式(单件模式)—Singleton Pattern

1.什么是单例模式?

《Head First 设计模式》中给出如下定义:确保一个类只有一个实例,并提供一个全局访问点。

关键词:唯一实例对象。

2.单例模式的实现方式:

2.1 懒汉式

 对于实例做懒加载处理,即在客户第一次使用时再做创建,所以第一次获取实例的效率会稍微低一些。

 1 /**
 2  * 懒汉式
 3  * @author Lsj
 4  */
 5 public class LazySingleton {
 6     
 7     private static LazySingleton instance;
 8     
 9     /**
10      * 获取单一实例对象—非同步方法
11      * @return
12      */
13     public static LazySingleton getInstance(){
14         if(instance == null){
15             try {
16                 TimeUnit.NANOSECONDS.sleep(1);//为了使模拟效果更直观,这里延时1ms,具体看时序图
17             } catch (InterruptedException e) {
18                 // TODO Auto-generated catch block
19                 e.printStackTrace();
20             }
21             instance = new LazySingleton();
22         }
23         return instance;
24     }
25     
26 }

这种创建方式可以延迟加载、但是在多线程环境下获取到的实例可能并非唯一的,具体见如下验证:

 1 import java.util.concurrent.CountDownLatch;
 2 import java.util.concurrent.TimeUnit;
 3 
 4 /**
 5  * 懒汉式
 6  * @author Lsj
 7  */
 8 public class LazySingleton {
 9     
10     private static LazySingleton instance;
11     
12     /**
13      * 获取单一实例对象—非同步方法
14      * @return
15      */
16     public static LazySingleton getInstance(){
17         if(instance == null){
18             try {
19                 TimeUnit.NANOSECONDS.sleep(1);//为了使模拟效果更直观,这里延时1ms,具体看时序图
20             } catch (InterruptedException e) {
21                 // TODO Auto-generated catch block
22                 e.printStackTrace();
23             }
24             instance = new LazySingleton();
25         }
26         return instance;
27     }
28     
29     /**
30      * 增加同步锁
31      * 避免多线程环境下并发产生多个实例可能的同时,会带来性能上的损耗。
32      * 事实上只有第一次创建时需要这么做,但后续依然通过加锁获取单例对象就有点因小失大了。
33      * @return
34      */
35     public synchronized static LazySingleton getInstanceSyn(){
36         if(instance == null){
37             try {
38                 TimeUnit.MILLISECONDS.sleep(1);//为了使模拟效果更直观,这里延时1ms,具体看时序图
39             } catch (InterruptedException e) {
40                 // TODO Auto-generated catch block
41                 e.printStackTrace();
42             }
43             instance = new LazySingleton();
44         }
45         return instance;
46     }
47     
48     public static void main(String[] args) throws InterruptedException {
49         //模拟下多线程环境下实例可能不唯一的情况
50         CountDownLatch startSignal = new CountDownLatch(1);
51         for(int i=0;i<2;i++){//模拟2个线程  
52             Thread t = new Thread(new MyThread(startSignal));
53             t.setName("thread " + i);
54             t.start();  
55         }  
56         Thread.sleep(1000);
57         startSignal.countDown();
58     }
59 
60 }
61 
62 class MyThread implements Runnable {
63     
64     private final CountDownLatch startSignal;
65 
66     public MyThread(CountDownLatch startSignal){
67         this.startSignal = startSignal;
68     }
69 
70     public void run() {
71         try {
72             System.out.println("current thread : " + Thread.currentThread().getName() + " is waiting.");
73             startSignal.await();
74         } catch (InterruptedException e) {
75             // TODO Auto-generated catch block
76             e.printStackTrace();
77         }
78         LazySingleton l = LazySingleton.getInstance();
79         System.out.println(l);
80     }
81     
82 }
View Code

从验证结果可以看出两个线程同时获取实例时,得到的并非同一个实例对象:

2.2 懒汉式(+同步锁)

1 public synchronized static LazySingleton getInstanceSyn(){
2     if(instance == null){
3          instance = new LazySingleton();
4     }
5     return instance;
6 }

在上述懒汉式的获取对象方法上做出一些改变,给获取实例的方法加上synchronized同步锁, 但是这种做法在多次调用获取实例方法的情况下会带来性能上的损耗。

事实上只有第一次创建实例时需要这么做,但后续依然通过加锁获取单例对象就有点因小失大了。

2.3 饿汉式

顾名思义、在类加载时就将唯一实例一并加载,后续只需要获取就可以了。

 1 /**
 2  * 饿汉式
 3  * @author Lsj
 4  */
 5 public class HungrySingleton {
 6 
 7     private static final String CLASS_NAME = "HungrySingleton";
 8     
 9     private static final HungrySingleton instance = new HungrySingleton();
10 
11     static{
12         System.out.println("类加载时创建:"+instance);//这里可以看到类加载后,优先加载上方的静态成员变量
13     }
14     
15     private HungrySingleton(){
16         
17     }
18     
19     public static HungrySingleton getInstance(){
20         return instance;
21     }
22 
23     public static void main(String[] args) throws ClassNotFoundException {
24         System.out.println(HungrySingleton.CLASS_NAME);//可以看到,这里仅仅是打印HungrySingleton的静态常量,但实例依然被初始化了。
25         System.out.println("==========分割线==========");
26         HungrySingleton instance1 = HungrySingleton.getInstance();
27         System.out.println(instance1);
28         HungrySingleton instance2 = HungrySingleton.getInstance();
29         System.out.println(instance2);
30     }
31     
32 }

运行结果:

从结果可以看到,这种获取单例的方式是线程安全的,JVM保障在多线程情况下一定先创建此实例并且只做一次实例化处理,但是这种情况没有做到懒加载,比如只是引用此类中的一个静态成员变量(常量),此实例在类加载时也一起被初始化了,如果后续应用中不使用这个对象,则会造成资源浪费,占用内存。

2.4 双重检查加锁

此方式可以看做是在懒汉式(+同步锁)方式上的进一步提升,从代码上可以看出主要是针对创建的过程加同步锁。

 1 /**
 2  * 通过双重检查的方式创建及获取单例对象
 3  * @author Lsj
 4  */
 5 public class DoubleCheckedLockingSingleton {
 6     
 7     private volatile static DoubleCheckedLockingSingleton instance;
 8     
 9     private DoubleCheckedLockingSingleton(){}
10     
11     public static DoubleCheckedLockingSingleton getInstance(){
12         if(instance == null){
13             synchronized (DoubleCheckedLockingSingleton.class) {
14                 if(instance == null){
15                     instance = new DoubleCheckedLockingSingleton();
16                 }
17             }
18         }
19         return instance;
20     }
21 
22 }

这种方式可以大大减少2.2方法中获取实例时不必要的同步操作,需要注意的是:静态成员变量中定义的volatile关键字,保证线程间变量的可见性以及防止指令重排序,同时需要的注意必须是jdk1.5及以上版本(volatile关键字在1.5做出了增强处理)。

--这里后续补充不加volatile关键字的危害。

2.5 静态内部类

此方案是基于类初始化的解决方案,JVM在类的初始化阶段,会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。

 1 /**
 2  * 以静态内部类的方式来创建获取单例对象
 3  * @author Lsj
 4  *
 5  */
 6 public class InnerSingleton {
 7 
 8     private InnerSingleton(){
 9     }
10     
11     public static InnerSingleton getInstance(){
12         return InnerSingleton.InnerClass.instance;
13     }
14     
15     static class InnerClass {
16         private static InnerSingleton instance = new InnerSingleton();
17     }
18     
19 }

这种方式受益于JVM在多线程环境下对于类初始化的同步控制,这里不再做太详细的说明。

2.6 通过枚举实现单例

《Effective Java》一书中作者Joshua Bloch提倡使用这种方式,这种方式依然是依靠JVM保障,而且可以防止反序列化的时候创建新的对象。

 1 /**
 2  * 通过枚举实现单例模式
 3  * @author Lsj
 4  *
 5  */
 6 public class EnumSingleton {
 7 
 8     public static EnumSingleton getInstance(){
 9         return Singleton.INSTANCE.getInstance();
10     }
11     
12     enum Singleton {
13         INSTANCE;
14         private EnumSingleton instance;
15         
16         private Singleton(){
17             instance = new EnumSingleton();
18         }
19         
20         private EnumSingleton getInstance(){
21             return instance;
22         }
23     }
24     
25 }

笔者没有这么使用过,暂不做过多描述、实际工作中也很少看到有这么用的,待后续有深入了解后再补充。

3. 总结:

几种单例模式的实现方式中,建议使用4,5,6这三种方式,实际根据使用场景作出选择,另外对于上述提到的几种方式1~5,需要防范通过反射或反序列化的手段创建对象从而使得实例不再唯一,笔者也会在后续会对此作出补充。

参考文献:

《Head Frist设计模式》

《Effective Java》

《Java并发编程的艺术》

原文地址:https://www.cnblogs.com/carryjack/p/7668169.html