设计模式——单例模式

  HeadFirst中对单例模式的定义:单例模式确保一个类只有一个实例,并只提供一个全局访问点。

  单例模式的应用:任务管理器、回收站、项目的配置文件、日志文件等等

  单例模式的特点:单例模式只有一个实例,减少了系统的开销,当一个对象的产生需要很多资源时,就可以通过在启动时来创建一个实例永久的驻存。

  可以在全局设置访问点,优化资源的访问。

 

  单例模式的常见实现方式:

  饿汉式:线程安全,效率高,不能延时加载。
  懒汉式:线程安全,效率低,可以延时加载。
  双重检测锁:线程安全,效率高,可以延时加载,但是只有在java1.5之后才支持,并且由于JVM底层模型的原因容易出问题。
  静态内部类:线程安全,效率高,可以延时加载。
  枚举:线程安全,效率高,不能延时加载,可以天然的防止反射和反序列化漏洞。

  一.饿汉式

    1.静态初始化是天然的线程安全的。
    2.效率比较高
    3.一开始就创建,没有延时加载,如果一直没有用到这个单例对象的话就浪费了资源。

   

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.wxisme.singleton;
/**
 * 饿汉式单例模式
 * @author wxisme
 *
 */
 
public class SingletonOne {
    //静态初始化时就new出单例对象
    private static final SingletonOne instance = new SingletonOne();
    //私有构造器
    private SingletonOne() {
         
    }
    //返回单例对象
    public static SingletonOne getInstance() {
        return instance;
    }
}

 

 二.懒汉式

    
   1.线程安全,但是效率较低。
   2.延时加载,不会造成资源的浪费。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
package com.wxisme.singleton;
 
import java.io.Serializable;
 
/**
 * 懒汉式实现单例模式
 * @author wxisme
 *
 */
 
public class SingletonTow implements Serializable {
    //在获取的时候创建此处不能加final
    private static SingletonTow instance;
     
    private SingletonTow() {
        //防止反射破解
        /*
        if(instance != null) {
            throw new RuntimeException();
        }
        */
         
    }
    //必须要手动加锁,达到线程安全的目的。
    public synchronized static SingletonTow getInstance() {
        if(instance == null) {
            instance = new SingletonTow();
        }
        return instance;
    }
    //防止反序列化破解
    /*
    private Object readResolve() {
        return instance;
    }
    */
    //防止反射破解
    /*
    private static Class getClass(String classname)
            throws ClassNotFoundException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();  
 
        if(classLoader == null)  
            classLoader = SingletonTow.class.getClassLoader();  
 
        return (classLoader.loadClass(classname));  
         
}  
*/
 
}

三.双重检验锁

    1.对懒汉式进行改进,只需要在第一次调用getInstance()方法的时候枷锁,提高了效率。

    2.由于JVM底层模型问题,这种方式偶尔会出问题。在JDK1.5之后才能支持。

    volatile  用来确保线程安全。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.wxisme.singleton;
/**
 * 双重检验锁实现单例模式
 * @author wxisme
 *
 */
 
public class SingletonThree {
    //volatile指令关键字确保实例是线程安全的
    private volatile static SingletonThree instance;
     
    private SingletonThree() {
         
    }
    //双重检验锁实现 线程安全&延时加载
    public static SingletonThree getInstance() {
        if(instance == null) {
            synchronized (SingletonThree.class) {
                if(instance == null) {
                    instance = new SingletonThree();
                }
            }
        }
        return instance;
    }
     
}

 

  四.静态内部类

      1.静态初始化,天然的线程安全,效率高。
      2.实现延时加载
      3.但是能用反射机制和序列化破解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.wxisme.singleton;
/**
 * 静态内部类实现单例模式
 * @author wxisme
 *
 */
 
public class SingletonFour {
    //静态内部类
    private static class Inner {
        private static final SingletonFour instance = new SingletonFour();
    }
     
    private SingletonFour() {}
     
    //只有显示调用getInstance方法时才会加载内部类
    public static final SingletonFour getInstance() {
        return Inner.instance;
    }
 
}

 五.枚举

    1.天然的线程安全,效率高
    2.代码简洁。
    3.防止反射和反序列化破解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.wxisme.singleton;
/**
 * 枚举实现单例模式
 * @author wxisme
 *
 */
 
public enum SingletonFive {
    INSTANCE;
     
    public void getInstance() {
         
    }
}

 存在的问题:以上方法中除了枚举的方式之外,都可以通过反射和反序列化的方式来破解。

 来破解一下。

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package com.wxisme.singleton;
 
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.lang.reflect.Constructor;
 
/**
 * 反射和反序列化破解单例模式(除枚举之外的都可以破解)
 * @author wxisme
 *
 */
@SuppressWarnings("all")
public class SingletonBreak {
     
    /*
     * 通过反射破解,以懒汉式为例
     * 应对策略:在私有构造器中手动抛出异常
     */
    public static void test1() throws Exception {
        SingletonTow st1 = SingletonTow.getInstance();
        SingletonTow st2 = SingletonTow.getInstance();
        System.out.println(st1==st2);
         
        //反射破解
         
        Class<SingletonTow> clazz = (Class<SingletonTow>) Class.forName("com.wxisme.singleton.SingletonTow");
        //获取构造器
        Constructor<SingletonTow> c = clazz.getDeclaredConstructor(null);
        c.setAccessible(true);//跳过权限的检查,可以访问私有构造器
        SingletonTow st3 = c.newInstance();
        SingletonTow st4 = c.newInstance();
        System.out.println(st3==st4);
    }
     
    /*
     * 反序列化破解 以懒汉式为例  (被反序列化的类必须实现Serializable接口)
     * 应对策略:在类中定义一个readResolve()方法,当反序列化时直接返回已经存在的对象
     */
    public static void test2() throws IOException, ClassNotFoundException {
        SingletonTow st1 = SingletonTow.getInstance();
        SingletonTow st2 = SingletonTow.getInstance();
        System.out.println(st1==st2);
         
        //序列化st1对象
        FileOutputStream fos = new FileOutputStream("e:/a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(st1);
        oos.close();
        fos.close();
         
        //反序列化创建对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("e:/a.txt"));
        SingletonTow st3 = (SingletonTow) ois.readObject();
        ois.close();
         
        System.out.println(st1==st3);
         
         
         
    }
    public static void main(String[] args) throws Exception {
        test1();
        System.out.println("---------------");
        test2();
         
         
    }
}

   不过这种破解可以防止,但是比较繁琐。

   防止反射破解的方法:

  在私有构造器中手动抛出异常

1
2
3
4
5
6
7
8
9
private SingletonTow() {
    //防止反射破解
         
    if(instance != null) {
        throw new RuntimeException();
    }
         
         
    }

  添加一个getClass()方法

 

1
2
3
4
5
6
7
8
private static Class getClass(String classname)
            throws ClassNotFoundException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();  
 
        if(classLoader == null)  
            classLoader = SingletonTow.class.getClassLoader();  
 
        return (classLoader.loadClass(classname));

    防止反序列化破解的方法:

    添加一个readRsolve()方法

1
2
3
private Object readResolve() {
        return instance;
    }

   

 总结:通过以上可以得出结论:如果需要延时加载,静态内部类好于懒汉式,不需要延时加载则枚举好于饿汉式。双重检验锁是对饿汉式的优化但是不推荐使用。如果没有特别的安全要求静态内部类式是最好的,如果需要还可以防止破解。懒汉式也不错。

 PS.在多线程环境下测试每种方式的执行效率。(感谢高淇老师的视频:))

      必须在除main线程执行其他所有的线程执行完之后才能计时,用到了CountDownLatch类来控制。

     Demo:

    

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.wxisme.singleton;
 
import java.util.concurrent.CountDownLatch;
 
/**
 * 在多线程环境下测试单例模式的效率
 * @author wxisme
 *
 */
public class TestEfficiency {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        int thread = 0;
        //内部类方法生命周期和全局变量不一致,需要加final
        final CountDownLatch count = new CountDownLatch(thread);
        for(int i=0; i<100; i++) {
            new Thread(new Runnable() {
 
                @Override
                public void run() {
                    for(int j=0; j<10000; j++) {
                        Object o = SingletonOne.getInstance();
                    }
                    count.countDown();//一个线程执行完计数器减一。
                }
                 
            }).start();
             
        }
        try {
            count.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }//阻塞main线程,直到所有的线程执行完,线程计数器减为零。
        long end = System.currentTimeMillis();
         
    }
 
}

 

 

 

     

 

 

 

 

 

 


 

posted @   Pickle  阅读(602)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架
点击右上角即可分享
微信分享提示