第一部分:单例模式的内容

  单例模式:类只能有一个实例。

  类的特点:1、私有构造器;2、内部构造实例对象;3、对外提供获取唯一实例的public方法。

  常见的单例模式实现有五种形式:

    1、饿汉式。

    2、懒汉式。

    3、双重检查锁式。

    4、静态内部类式。

    5、枚举式。

  以下分别介绍:

  一、饿汉式

    饿汉式单例特点:线程安全,不能延时加载,效率较高。



1 public class SingletonDemoE {
2
3 //内部构建唯一实例
4 private static SingletonDemoE instance = new SingletonDemoE();
5
6 //私有化构造器
7 private SingletonDemoE(){
8
9 }
10
11 //公共静态方法获取唯一实例化对象
12 public static SingletonDemoE getInstance(){
13 return instance;
14 }
15
16 }



  二、懒汉式

    懒汉式单例特点:线程安全(须synchronized做方法同步),可以延时加载,效率较低。



1 public class SingletonDemoL {
2
3 //声明实例对象
4 private static SingletonDemoL instance;
5
6 //私有化构造器
7 private SingletonDemoL(){
8
9 }
10
11 //公共静态方法获取唯一实例化对象,方法同步
12 public static synchronized SingletonDemoL getInstance(){
13 if(instance == null){
14 //第一次实例化时构建
15 instance = new SingletonDemoL();
16 }
17 return instance;
18 }
19
20 }




  三、双重检查锁式  

 何为双重加锁机制?

  在懒汉式实现单例模式的代码中,有使用synchronized关键字来同步获取实例,保证单例的唯一性,但是上面的代码在每一次执行时都要进行同步和判断,无疑会拖慢速度,使用双重加锁机制正好可以解决这个问题:



1 public class SLHanDanli {
2 private static volatile SLHanDanli dl = null;
3 private SLHanDanli(){}
4 public static SLHanDanli getInstance(){
5 if(dl == null){
6 synchronized (SLHanDanli.class) {
7 if(dl == null){
8 dl = new SLHanDanli();
9 }
10 }
11 }
12 return dl;
13 }
14 }



  看了上面的代码,有没有感觉很无语,双重加锁难道不是需要两个synchronized进行加锁的吗?

  ......

  其实不然,这里的双重指的的双重判断,而加锁单指那个synchronized,为什么要进行双重判断,其实很简单,第一重判断,如果单例已经存在,那么就不再需要进行同步操作,而是直接返回这个实例,如果没有创建,才会进入同步块,同步块的目的与之前相同,目的是为了防止有两个调用同时进行时,导致生成多个实例,有了同步块,每次只能有一个线程调用能访问同步块内容,当第一个抢到锁的调用获取了实例之后,这个实例就会被创建,之后的所有调用都不会进入同步块,直接在第一重判断就返回了单例。至于第二个判断,个人感觉有点查遗补漏的意味在内(期待高人高见)。

  不论如何,使用了双重加锁机制后,程序的执行速度有了显著提升,不必每次都同步加锁。

  其实我最在意的是volatile的使用,volatile关键字的含义是:被其所修饰的变量的值不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存来实现,从而确保多个线程能正确的处理该变量。该关键字可能会屏蔽掉虚拟机中的一些代码优化,所以其运行效率可能不是很高,所以,一般情况下,并不建议使用双重加锁机制,酌情使用才是正理!

  四、静态内部类式

    静态内部类式单例特点:线程安全(须synchronized做方法同步),可以延时加载,效率较高。



1 public class SingletonDemoJ {
2
3 //静态内部类
4 private static class SingletonClassInstance {
5 private static final SingletonDemoJ instance = new SingletonDemoJ();
6 }
7
8 //私有化构造器
9 private SingletonDemoJ(){
10
11 }
12
13 //公共静态方法获取唯一实例化对象,方法同步
14 public static synchronized SingletonDemoJ getInstance(){
15 return SingletonClassInstance.instance;
16 }
17
18 }




  五、枚举式

    枚举式单例特点:枚举是天然的单例,线程安全,不可延时加载,效率较高



1 public enum SingletonDemoM {
2 //枚举元素,本身就是单例模式
3 INSTANCE;
4
5 //实现自己的操作
6 public void singletonOperation(){
7
8 }
9 }




第二部分:单例模式的破解(扩展)

  单例模式的五种实现方式中,除枚举式是天然的单例不可破解之外,其他四种形式均可通过反射和反序列化的机制进行破解。

  以懒汉式单例为例,首先分别看一下如何通过反射和反序列化的机制破解单例。

  定义一个常规的懒汉式单例:



1 public class SingletonDemoAntiCrackL {
2 private static SingletonDemoAntiCrackL instance;
3
4 private SingletonDemoAntiCrackL(){
5
6 }
7
8 public static synchronized SingletonDemoAntiCrackL getInstance(){
9 if(instance == null){
10 instance = new SingletonDemoAntiCrackL();
11 }
12 return instance;
13 }
14 }



  正常我们创建多个单例的实例,都应该是同一个对象,如下测试Demo:



1 public class TestCrackDemo {
2 public static void main(String[] args) {
3 SingletonDemoAntiCrackL sL1 = SingletonDemoAntiCrackL.getInstance();
4 SingletonDemoAntiCrackL sL2 = SingletonDemoAntiCrackL.getInstance();
5 System.out.println("sL1 = " + sL1);
6 System.out.println("sL2 = " + sL2);
7 }
8 }



  运行返回:


sL1 = com.corey.singleton.SingletonDemoAntiCrackL@2a139a55
sL2 = com.corey.singleton.SingletonDemoAntiCrackL@2a139a55


  利用反射机制破解单例,创建多个不同的实例:



1 package com.corey.singleton;
2
3 import java.lang.reflect.Constructor;
4
5 public class TestCrackDemo {
6 public static void main(String[] args) {
7 SingletonDemoAntiCrackL sL1 = SingletonDemoAntiCrackL.getInstance();
8 SingletonDemoAntiCrackL sL2 = SingletonDemoAntiCrackL.getInstance();
9 System.out.println("sL1 = " + sL1);
10 System.out.println("sL2 = " + sL2);
11
12 //利用反射机制破解单例
13 try {
14 Class<SingletonDemoAntiCrackL> clazz = (Class<SingletonDemoAntiCrackL>)Class.forName("com.corey.singleton.SingletonDemoAntiCrackL");
15 Constructor<SingletonDemoAntiCrackL> c = clazz.getDeclaredConstructor(null);
16 c.setAccessible(true); //跳过权限检查,可以访问私有属性和方法
17 SingletonDemoAntiCrackL sL3 = c.newInstance(null); //构建实例
18 SingletonDemoAntiCrackL sL4 = c.newInstance(null);
19 System.out.println("sL3 = " + sL3);
20 System.out.println("sL4 = " + sL4);
21
22 } catch (Exception e) {
23 e.printStackTrace();
24 }
25 }
26 }



  运行返回:


sL1 = com.corey.singleton.SingletonDemoAntiCrackL@2a139a55
sL2 = com.corey.singleton.SingletonDemoAntiCrackL@2a139a55
sL3 = com.corey.singleton.SingletonDemoAntiCrackL@15db9742
sL4 = com.corey.singleton.SingletonDemoAntiCrackL@6d06d69c


  可见,通过反射构建的sL3和sL4两个对象,都是不同的实例,破解了单例模式只能有一个实例的要求。

  那么,修改单例的构造函数,可以应对反射机制的破解,代码如下:



1 public class SingletonDemoAntiCrackL {
2 private static SingletonDemoAntiCrackL instance;
3
4 private SingletonDemoAntiCrackL(){
5 //私有构造器,增加实例检查,若已创建实例,则抛出异常
6 if(instance != null){
7 throw new RuntimeException();
8 }
9 }
10
11 public static synchronized SingletonDemoAntiCrackL getInstance(){
12 if(instance == null){
13 instance = new SingletonDemoAntiCrackL();
14 }
15 return instance;
16 }
17 }



  此时,在运行TestCrackDemo时,会抛出java.lang.reflect.InvocationTargetException异常,避免了通过反射机制创建多个实例的问题。

  接下来,看下通过反序列化机制破解单例。

  当单例的类实现了Serializable接口时,就可以通过反序列化机制破解,如下单例:



1 public class SingletonDemoAntiCrackL implements Serializable{
2 private static SingletonDemoAntiCrackL instance;
3
4 private SingletonDemoAntiCrackL(){
5 //私有构造器,增加实例检查,若已创建实例,则抛出异常
6 if(instance != null){
7 throw new RuntimeException();
8 }
9 }
10
11 public static synchronized SingletonDemoAntiCrackL getInstance(){
12 if(instance == null){
13 instance = new SingletonDemoAntiCrackL();
14 }
15 return instance;
16 }
17 }



  反序列化破解测试Demo:



1 package com.corey.singleton;
2
3 import java.io.FileInputStream;
4 import java.io.FileNotFoundException;
5 import java.io.FileOutputStream;
6 import java.io.ObjectInputStream;
7 import java.io.ObjectOutputStream;
8 import java.lang.reflect.Constructor;
9
10 public class TestCrackDemo {
11 public static void main(String[] args) {
12 SingletonDemoAntiCrackL sL1 = SingletonDemoAntiCrackL.getInstance();
13 SingletonDemoAntiCrackL sL2 = SingletonDemoAntiCrackL.getInstance();
14 System.out.println("sL1 = " + sL1);
15 System.out.println("sL2 = " + sL2);
16
17 //通过反序列化机制破解单例
18 try {
19 //序列化,将对象存入文件
20 FileOutputStream fos = new FileOutputStream("f:/fos.txt");
21 ObjectOutputStream oos = new ObjectOutputStream(fos);
22 oos.writeObject(sL1);
23 oos.close();
24 fos.close();
25 //反序列化,从文件中读出对象
26 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("f:/fos.txt"));
27 SingletonDemoAntiCrackL sL5 = (SingletonDemoAntiCrackL)ois.readObject();
28 System.out.println("sL5 = " + sL5);
29 } catch (Exception e) {
30 e.printStackTrace();
31 }
32 }
33 }



  运行的结果:


sL1 = com.corey.singleton.SingletonDemoAntiCrackL@2a139a55
sL2 = com.corey.singleton.SingletonDemoAntiCrackL@2a139a55
sL5 = com.corey.singleton.SingletonDemoAntiCrackL@33909752


  可见,反序列化出来的实例对象,是不同的对象,即单例已被破解。

  解决版本,单例类中重写readResolve方法,可以应对反射机制的破解,代码如下:



1 package com.corey.singleton;
2
3 import java.io.ObjectStreamException;
4 import java.io.Serializable;
5
6 /**
7 * 单例模式,懒汉式
8 * 线程安全,能延时加载,效率相对较低
9 * @author Corey
10 *
11 */
12 public class SingletonDemoAntiCrackL implements Serializable{
13 private static SingletonDemoAntiCrackL instance;
14
15 private SingletonDemoAntiCrackL(){
16 //私有构造器,增加实例检查,若已创建实例,则抛出异常
17 if(instance != null){
18 throw new RuntimeException();
19 }
20 }
21
22 public static synchronized SingletonDemoAntiCrackL getInstance(){
23 if(instance == null){
24 instance = new SingletonDemoAntiCrackL();
25 }
26 return instance;
27 }
28
29 private Object readResolve() throws ObjectStreamException{
30 //反序列化时,直接返回对象
31 return instance;
32 }
33 }



  修改后,再次运行TestCrackDemo,可以看到反序列化后,构建的仍然是同一个对象。


sL1 = com.corey.singleton.SingletonDemoAntiCrackL@2a139a55
sL2 = com.corey.singleton.SingletonDemoAntiCrackL@2a139a55
sL5 = com.corey.singleton.SingletonDemoAntiCrackL@2a139a55



第三部分:单例模式各个实现方式的效率

  采用如下代码,测试:



1 package com.corey.singleton;
2
3 import java.util.concurrent.CountDownLatch;
4
5 /**
6 * 测试单例模式的效率
7 * @author Corey
8 *
9 */
10 public class TestEfficiencyDemo {
11 public static void main(String[] args) throws Exception {
12
13 int threadNum = 10;
14 long start = System.currentTimeMillis();
15
16 final CountDownLatch cdl = new CountDownLatch(threadNum);
17
18 //创建10个线程
19 for(int k=0; k<threadNum; k++){
20 new Thread(new Runnable() {
21
22 @Override
23 public void run() {
24 //每个线程构建100万个实例对象
25 for(int i=0; i<1000000; i++){
26 Object o = SingletonDemoE.getInstance();
27 }
28 //每个线程运行完毕,线程计数器减一
29 cdl.countDown();
30 }
31 }).start();
32 }
33
34 cdl.await();//main线程阻塞,直到现场计数器为0,才继续执行。
35
36 long end = System.currentTimeMillis();
37 System.out.println("饿汉式总耗时:" + (end - start));
38 }
39 }



  运行结果:


饿汉式总耗时:17


  以此类推,测试各个实现方式的单例的效率。注意,此处根据电脑性能以及电脑的运行情况不同,结果都是不一样的,甚至同一实现方式,多次运行的结果也不一样。

  我这里的测试结果如下:


饿汉式总耗时:17
懒汉式总耗时:171
静态内部类式总耗时:165
枚举式总耗时:11