利用元旦小假期,参考了几篇单例模式介绍的文章,而后本身下午对java设计模式中的单例模式作了一下简单的总结,主要是代码介绍。java
单例模式,在实际项目开发中运用普遍,好比数据库链接池,实际上,配置信息类、管理类、控制类、门面类、代理类一般被设计为单例类。像Java的Struts、spring框架,.Net的Spring.Net框架,以及PHP的Zend框架都大量使用单例模式。spring
那么,接下来,我将如下面5点来对单例模式做一下介绍:数据库
1.单例模式的定义设计模式
2.单例模式的特色安全
3.为何要使用单例模式?网络
4.单例模式的5种不一样写法及其总结多线程
5.拓展--如何防止Java反射机制对单例类的攻击?并发
单例模式(Singleton)是一种建立型模式,指某个类采用Singleton模式,则在这个类被建立后,只可能产生一个实例供外部访问,而且提供一个全局的访问点。框架
核心知识点:
(1)将采用单例模式的类的构造方法私有化(采用private修饰);
(2)在其内部产生该类的实例化对象,并将其封装成private static类型;
(3)定义一个静态方法返回该类的实例。函数
- 单例类只能有一个实例;
- 单例类必须本身建立本身的惟一实例;
- 单例类必须给全部其余对象提供这一实例。
根据单例模式的定义和特色,咱们会对单例模式有了初步认识,那么由特色出发,单例模式在项目中的做用就显而易见了。
(1)控制资源的使用,经过线程同步来控制资源的并发访问;
(2)控制实例产生的数量,到达节约资源的目的;
(3)做为通讯媒介使用,也就是数据共享,他能够在不创建直接关联的条件下,让多个不相关的两个线程或者进程实现通讯。
好比:
数据库链接池的设计通常采用单例模式,数据库链接是一种数据资源。项目中使用数据库链接池,主要是节省打开或者关闭数据库所引发的效率损耗。固然,使用数据库链接池能够屏蔽不一样数据数据库之间的差别,实现系统对数据库的低度耦合,也能够被多个系统同时使用,具备高科复用性,还能方便对数据库链接的管理等。
实际上,配置信息类、管理类、控制类、门面类、代理类一般被设计为单例类。像Java的Struts、spring框架,.Net的Spring.Net框架,以及PHP的Zend框架都大量使用单例模式。
单例模式的实现经常使用的有5种,分别是:
(1).饿汉式;
(2).懒汉式(、加同步锁的懒汉式、加双重校验锁的懒汉式、防止指令重排优化的懒汉式);
(3).登记式单例模式;
(4)静态内部类单例模式;
(5).枚举类型的单例模式。
接下来,我就以代码为主来对各类实现方式介绍一下。
项目工程结构:如图中的红框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.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】
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 }
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】
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】
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 }
运行结果:
上面介绍的除了最后一种枚举类型单例模式外,其他的写法都是基于一个条件:确保不会被反射机制调用私有的构造器。
那么如何防止Java反射机制对单例类的攻击呢?请参考下一篇随笔:《如何防止反射机制对单例类的攻击?》