java设计模式之单例模式(Singleton)
利用元旦小假期,参考了几篇单例模式介绍的文章,然后自己下午对java设计模式中的单例模式做了一下简单的总结,主要是代码介绍。
单例模式,在实际项目开发中运用广泛,比如数据库连接池,实际上,配置信息类、管理类、控制类、门面类、代理类通常被设计为单例类。像Java的Struts、spring框架,.Net的Spring.Net框架,以及PHP的Zend框架都大量使用单例模式。
那么,接下来,我将以下面5点来对单例模式作一下介绍:
1.单例模式的定义
2.单例模式的特点
3.为什么要使用单例模式?
4.单例模式的5种不同写法及其总结
5.拓展--如何防止Java反射机制对单例类的攻击?
1.单例模式的定义
单例模式(Singleton)是一种创建型模式,指某个类采用Singleton模式,则在这个类被创建后,只可能产生一个实例供外部访问,并且提供一个全局的访问点。
核心知识点:
(1)将采用单例模式的类的构造方法私有化(采用private修饰);
(2)在其内部产生该类的实例化对象,并将其封装成private static类型;
(3)定义一个静态方法返回该类的实例。
2.单例模式的特点
- 单例类只能有一个实例;
- 单例类必须自己创建自己的唯一实例;
- 单例类必须给所有其他对象提供这一实例。
3.为什么要使用单例模式?
根据单例模式的定义和特点,我们会对单例模式有了初步认识,那么由特点出发,单例模式在项目中的作用就显而易见了。
(1)控制资源的使用,通过线程同步来控制资源的并发访问;
(2)控制实例产生的数量,到达节约资源的目的;
(3)作为通信媒介使用,也就是数据共享,他可以在不建立直接关联的条件下,让多个不相关的两个线程或者进程实现通信。
比如:
数据库连接池的设计一般采用单例模式,数据库连接是一种数据资源。项目中使用数据库连接池,主要是节省打开或者关闭数据库所引起的效率损耗。当然,使用数据库连接池可以屏蔽不同数据数据库之间的差异,实现系统对数据库的低度耦合,也可以被多个系统同时使用,具有高科复用性,还能方便对数据库连接的管理等。
实际上,配置信息类、管理类、控制类、门面类、代理类通常被设计为单例类。像Java的Struts、spring框架,.Net的Spring.Net框架,以及PHP的Zend框架都大量使用单例模式。
4.单例模式的5种不同写法及其总结
单例模式的实现常用的有5种,分别是:
(1).饿汉式;
(2).懒汉式(、加同步锁的懒汉式、加双重校验锁的懒汉式、防止指令重排优化的懒汉式);
(3).登记式单例模式;
(4)静态内部类单例模式;
(5).枚举类型的单例模式。
接下来,我就以代码为主来对各种实现方式介绍一下。
项目工程结构:如图中的红框1中所示。
(1).饿汉式
代码清单【1】
1 package com.lxf.singleton; 2 3 /** 4 5 * 单例类--饿汉模式 线程安全 6 * @author Administrator 7 * 8 */ 9 public class Singleton 10 { 11 private static final Singleton INSTANCE = new Singleton(); 12 13 private static boolean flag = true; 14 private Singleton() 15 { 16 } 17 18 public static Singleton newInstance() 19 { 20 return INSTANCE; 21 } 22 23 }
从代码中,我们可以看到,该类的构造函数被定义为private,这样就保证了其他类不能实例化此类,然后该单例类提供了一个静态实例并返回给调用者(向外界提供了调用该类方法的实例)。饿汉模式在类加载的时候就对该实例进行创建,实例在整个程序周期都存在。
优点:只在类加载的时候创建一次,不会存在多个线程创建多个实例的情况,避免了多线程同步的问题,是线程安全的。
缺点:在整个程序周期中,即使这个单例没有被用到也会被加载,而且在类加载之后就被创建,内存就被浪费了。
使用场景:适合单例占用内存比较小,在初始化就被用到的情况。但是,如果单例占用的内存比较大,或者单例只是在某个场景下才会被使用到,使用该模式就不合适了,这时候就要考虑使用“懒汉模式”进行延迟加载。
(2).懒汉式(、加同步锁的懒汉式、加双重校验锁的懒汉式、防止指令重排优化的懒汉式)
2.1--懒汉式(、加同步锁的懒汉式
代码清单【2.1】
1 package com.lxf.singleton; 2 3 /** 4 * 懒汉式单例模式 线程不安全 5 * @author Administrator 6 * 7 */ 8 public class Singleton2 9 { 10 private static Singleton2 instance = null; 11 12 private Singleton2(){} 13 14 /* 15 * 1.未加同步锁 16 */ 17 /* 18 public static Singleton2 getInstance() 19 { 20 if(instance == null) 21 { 22 instance = new Singleton2(); 23 } 24 return instance; 25 } 26 */ 27 28 /* 29 * 2.加同步锁 线程安全 30 * 上面的懒汉模式并没有考虑多线程的安全问题,在多性格线程可能并发调用它的getInsatance()方法, 31 * 导致创建多个实例,因此需要加锁来解决线程同步问题。 32 */ 33 public static synchronized Singleton2 getInstance() 34 { 35 if(instance == null) 36 { 37 instance = new Singleton2(); 38 } 39 return instance; 40 } 41 46 }
懒汉式单例模式是在需要的时候才去创建,如果调用该接口获取实例的时候,发现该实例不存在,就会被创建;如果发现该实例已经存在,就会返回之前已经创建出来的实例。
但是懒汉模式的单例设计,是线程不安全的,没有考虑线程安全问题。如果你的程序是多线程的,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程的运行结果一样的,而且其他的变量的值也和预期一样的,就是线程安全的。显然,懒汉式单例模式并不是线程安全的,在多线程并发环境下,可能会创建出来多个实例。
使用场景:适合在项目中使用单例类数量较少,而且占用资源比较多的项目,可以考虑使用懒汉式单例模式。
2.2--加双重校验锁的懒汉式、防止指令重排优化的懒汉式
代码清单【2.2】
1 package com.lxf.singleton; 2 3 /** 4 * 双重校验锁 线程安全 5 * @author Administrator 6 * 7 */ 8 public class Singleton3 9 { 10 private static Singleton3 instance = null; 11 //禁止指令重排优化 12 //private static volatile Singleton3 instance = null; 13 private Singleton3(){} 14 15 public static Singleton3 getInstance() 16 { 17 if(null == instance) 18 { 19 synchronized (Singleton3.class) 20 { 21 if(null == instance) 22 { 23 //双重校验 24 instance = new Singleton3(); 25 } 26 27 } 28 } 29 return instance; 30 } 31 32 }
在加锁的懒汉模式中,看似解决了线程的并发安全问题,有实现了延迟加载,然而它存在着性能问题。synchronized修饰的同步方法比一般方法要慢很多,如果多次调用getInstance(),累积的性能损耗就比较大了。因此,我们这里就有了双重校验锁。在上面的双重校验锁代码中,由于单例对象只需要创建一次,如果后面再次调用getInstance()只需要直接返回单例对象。
因此,大部分情况下,调用getInstance()都不会执行到同步代码块中的代码,从而提高了性能。
不过,在这里要提到Java中的指令重排优化。指令重排优化:在不改变原语义的情况下,通过调整指令的执行顺序让程序运行的更快。
由于指令重拍优化的存在,导致初始化Singleton3和将对象地址付给instance字段的顺序是不确定的。比如:在某个线程创建单例对象时,在构造方法被调用之前,就为该对象分配了内存空间并将对象的字段设置为默认值。此时就可以将分配的内存地址复制给instance字段了,然后该对象可能还没有初始化。若紧接着另外一个线程来调用getInstance,得到的是状态不确定的对象,程序就会出错。
以上就是双重校验锁会失效的原因。不过在JDK1.5及其以后的版本中,增加了volatile关键字。volatile的关键字的一个语义就是禁止指令重排优化,这样就保证了instance变量被赋值的时候已经是初始化的,避免了上面提到的状态不确定的问题。
3.登记式单例模式
代码清单【3】
1 package com.lxf.singleton; 2 3 import java.util.HashMap; 4 import java.util.Map; 5 6 /** 7 * 登记式单例模式 线程安全 8 * @author Administrator 9 *就是将该类名进行登记,每次调用前查询,如果存在,则直接使用;不存在,则进行登记。 10 *这里使用Map<String,Class> 11 */ 12 public class Singleton4 13 { 14 private static Map<String, Singleton4> map = new HashMap<String, Singleton4>(); 15 16 /* 17 * 静态语句块,保证其中的内容在类加载的时候运行一次,并且只运行一次。 18 */ 19 static 20 { 21 Singleton4 singleton4 = new Singleton4(); 22 map.put(singleton4.getClass().getName(), singleton4); 23 } 24 25 //保护的默认构造子 26 protected Singleton4 (){} 27 //静态工厂方法,返回此类唯一的实例 28 public static Singleton4 getInstance(String name) 29 { 30 if(null == name) 31 { 32 name = Singleton4.class.getName(); 33 System.out.println("name == null --- > name == " + name); 34 } 35 if(null == map.get(name)) 36 { 37 try { 38 map.put(name, (Singleton4) Class.forName(name).newInstance()); 39 } catch (InstantiationException e) { 40 e.printStackTrace(); 41 } catch (IllegalAccessException e) { 42 e.printStackTrace(); 43 } catch (ClassNotFoundException e) { 44 e.printStackTrace(); 45 } 46 } 47 return map.get(name); 48 } 49 50 }
4.静态内部类单例模式;
代码清单【4】
1 package com.lxf.singleton; 2 /** 3 * 静态内部类单例模式 线程安全 4 * @author Administrator 5 */ 6 public class Singleton5 7 { 8 /* 9 * 内部类,用于实现延迟机制 10 * @author Administrator 11 */ 12 private static class SingletonHolder 13 { 14 private static Singleton5 instance = new Singleton5(); 15 } 16 //私有的构造方法,保证外部的类不能通过构造器来实例化 17 private Singleton5(){} 18 19 /* 20 *获取单例对象的实例 21 */ 22 public static Singleton5 getInstacne() 23 { 24 return SingletonHolder.instance; 25 } 26 27 }
这种方式同样利用了类加载机制来保证只创建一个insatcne实例。因此不存在多线程并发的问题。它是在内部类里面去创建对象实例,这样的话,只要应用中不使用内部类,JVM就不会去加载这个单例类,
也就不会加载单例对象,从而实现了延迟加载。
5.枚举类型的单例模式。
代码清单【5】
package com.lxf.singleton; /** * 我们要创建的单例类资源,比如:数据库连接,网络连接,线程池之类的。 * @author Administrator * */ class Resource { public void doMethod() { System.out.println("枚举类型的单例类资源"); } } /** * 枚举类型的单例模式 线程安全 * * 获取资源的方式,Singleton6.INSTANCE.getInstance();即可获得所要的实例。 * @author Administrator * */ public enum Singleton6 { INSTANCE; private Resource instance; Singleton6() { instance = new Resource(); } public Resource getInstance() { return instance; } }
上面代码中,首先,在枚举中我们明确了构造方法限制为私有,在我们访问枚举实例时会执行构造方法,同时每个枚举实例都是static final类型的,也就表明只能被实例化一次。在调用构造方法时,我们的单例被实例化。也就是说,因为enum中的实例被保证只会被实例化一次,所以我们的INSTANCE也就会被实例化一次。
在之前介绍的实现单例的方式中都有共同的缺点:
(1).需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例;
(2).可以使用反射强行调用私有构造器(如果要避免这个情况,可以修改构造器,让它在创建第二个人实例的时候抛异常。)这个会在第5点中进行介绍
而使用枚举出了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
6.单例模式测试类 SingletonMain.java
代码清单【6】
1 package com.lxf.singleton; 2 3 import org.junit.Test; 4 5 public class SingletonMain 6 { 7 /** 8 *1. 饿汉模式单例测试 9 */ 10 @Test 11 public void testSingletonTest() 12 { 13 System.out.println("-------饿汉模式单例测试--------------"); 14 Singleton singleton = Singleton.newInstance(); 15 //singleton.about(); 16 Singleton singleton2 = Singleton.newInstance(); 17 //singleton2.about(); 18 19 if(singleton == singleton2) 20 { 21 System.out.println("1.singleton and singleton2 are same Object"); 22 } 23 else 24 { 25 System.out.println("1.singleton and singleton2 aren't same Object"); 26 } 27 System.out.println("---------------------------------------------"); 28 } 29 30 /** 31 *2. 懒汉式单例模式测试 32 */ 33 @Test 34 public void testSingleton2Test() 35 { 36 System.out.println("-------懒汉式单例模式测试--------------"); 37 Singleton2 singleton = Singleton2.getInstance(); 38 Singleton2 singleton2 = Singleton2.getInstance(); 39 40 if(singleton == singleton2) 41 { 42 System.out.println("2.singleton and singleton2 are same Object"); 43 } 44 else 45 { 46 System.out.println("2.singleton and singleton2 aren't same Object"); 47 } 48 System.out.println("---------------------------------------------"); 49 } 50 51 /** 52 * 3.双重校验锁单例模式测试 53 */ 54 @Test 55 public void testSingleton3() 56 { 57 System.out.println("-------双重校验锁单例模式测试--------------"); 58 Singleton3 singleton = Singleton3.getInstance(); 59 Singleton3 singleton2 = Singleton3.getInstance(); 60 61 if(singleton == singleton2) 62 { 63 System.out.println("3.singleton and singleton2 are same Object"); 64 } 65 else 66 { 67 System.out.println("3.singleton and singleton2 aren't same Object"); 68 } 69 System.out.println("---------------------------------------------"); 70 } 71 72 /** 73 * 4.登记式单例模式测试 74 */ 75 @Test 76 public void testSingleton4() 77 { 78 System.out.println("-------双重校验锁单例模式测试--------------"); 79 Singleton4 singleton = Singleton4.getInstance(Singleton4.class.getName()); 80 Singleton4 singleton2 = Singleton4.getInstance(Singleton4.class.getName()); 81 if(singleton == singleton2) 82 { 83 System.out.println("4.singleton and singleton2 are same Object"); 84 } 85 else 86 { 87 System.out.println("4.singleton and singleton2 aren't same Object"); 88 } 89 System.out.println("---------------------------------------------"); 90 } 91 92 /** 93 *5. 静态内部类单例模式测试 94 */ 95 @Test 96 public void testSingleton5() 97 { 98 System.out.println("-------静态内部类单例模式测试--------------"); 99 Singleton5 singleton = Singleton5.getInstacne(); 100 Singleton5 singleton2 = Singleton5.getInstacne(); 101 if(singleton == singleton2) 102 { 103 System.out.println("5.singleton and singleton2 are same Object"); 104 } 105 else 106 { 107 System.out.println("5.singleton and singleton2 aren't same Object"); 108 } 109 System.out.println("---------------------------------------------"); 110 } 111 112 /** 113 *6. 静态内部类单例模式测试 114 */ 115 @Test 116 public void testSingleton6() 117 { 118 System.out.println("-------枚举类型的单例类资源测试--------------"); 119 Resource singleton = Singleton6.INSTANCE.getInstance(); 120 Resource singleton2 = Singleton6.INSTANCE.getInstance(); 121 if(singleton == singleton2) 122 { 123 System.out.println("6.singleton and singleton2 are same Object"); 124 } 125 else 126 { 127 System.out.println("6.singleton and singleton2 aren't same Object"); 128 } 129 System.out.println("---------------------------------------------"); 130 } 131 132 133 }
运行结果:
5.拓展--如何防止Java反射机制对单例类的攻击?
上面介绍的除了最后一种枚举类型单例模式外,其余的写法都是基于一个条件:确保不会被反射机制调用私有的构造器。
那么如何防止Java反射机制对单例类的攻击呢?请参考下一篇随笔:《如何防止反射机制对单例类的攻击?》
6.后期补充