Java的23种设计模式---(1)单例模式(包含饿汉式,懒汉式,双重检测锁式,静态内部类式,枚举式)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq1021979964/article/details/87535219

单例模式

单例模式的优点:

      1.由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决

       2.单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

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

主要:

  1. 饿汉式:线程安全,调用效率高。但是,不能延时加载。
  2. 懒汉式:线程安全,调用效率不高。但是,可以延时加载。

其他:

  1. 双重检测锁式:由于JVM底层内部模型原因,偶尔会出现问题,不建议使用。
  2. 静态内部类式:线程安全,调用效率高,但是可以延时加载。
  3. 枚举式:线程安全,调用效率高,不能延时加载,并且可以天然的防止反射和反序列化漏洞。

在选用时:

单例对象,占用资源少,不要延时加载时

  • 枚举式比饿汉式好。

单例对象,占用资源大,需要延时加载

  • 静态内部类式比懒汉式好。

测试五种单例模式

package com.kevin.创建型模式.单例模式;

/**
 * @author kevin
 * @version 1.0
 * @description     测试单例模式
 * @createDate 2019/1/18
 */
public class Test {

    public static void main(String[] args) {
        // 不管创建多少次对象,都是指向同一个对象
        SingletonDemo1 s1 = SingletonDemo1.getInstance();
        SingletonDemo1 s11 = SingletonDemo1.getInstance();
        System.out.println("饿汉式是否是同一个对象: " + (s1 == s11));

        SingletonDemo2 s2 = SingletonDemo2.getInstance();
        SingletonDemo2 s22 = SingletonDemo2.getInstance();
        System.out.println("懒汉式是否是同一个对象: " + (s2 == s22));

        SingletonDemo3 s3 = SingletonDemo3.getInstance();
        SingletonDemo3 s33 = SingletonDemo3.getInstance();
        System.out.println("双重检测锁式是否是同一个对象: " + (s3 == s33));

        SingletonDemo4 s4 = SingletonDemo4.getInstance();
        SingletonDemo4 s44 = SingletonDemo4.getInstance();
        System.out.println("静态内部类式是否是同一个对象: " + (s4 == s44));

        SingletonDemo5 s5 = SingletonDemo5.INSTANCE;
        SingletonDemo5 s55 = SingletonDemo5.INSTANCE;
        System.out.println("枚举式是否是同一个对象: " + (s5 == s55));

    }

}
扫描二维码关注公众号,回复: 5416613 查看本文章
  • 饿汉式模式(单例对象立即加载)

package com.kevin.创建型模式.单例模式;

/**
 * @author kevin
 * @version 1.0
 * @description     饿汉式单例模式
 *      线程安全,调用效率高。但是,不能延时加载。
 * @createDate 2019/2/17
 */
public class SingletonDemo1 {

    // 类初始化时,立即加载这个对象(没有延时加载的优势)。由于加载类时,天然的是线程安全的
    private static SingletonDemo1 instance = new SingletonDemo1();

    public SingletonDemo1() {
    }

    // 方法没有同步,调用效率高
    public static SingletonDemo1 getInstance(){
        return instance;
    }


}

注:饿汉式单例模式,static变量会在类装载时初始化,此时也不会涉及多个线程对象访问该对象的问题。虚拟机保证只会装载一次该类,肯定不会发生并发访问的问题。因此,可以省略synchronized关键字。

问题:如果只是加载本类,而不是调用getInstance(),甚至永远没有调用,则会造成资源浪费。

  • 懒汉式模式(单例对象延迟加载)

package com.kevin.创建型模式.单例模式;

/**
 * @author kevin
 * @version 1.0
 * @description     懒汉式单例模式
 *      线程安全,调用效率不高,但是可以延时加载
 * @createDate 2019/2/17
 */
public class SingletonDemo2 {

    // 类初始化时,不初始化这个对象(有延时加载的优势,到真正使用的时候再创建)。由于加载类时,天然的是线程安全的
    private static SingletonDemo2 instance;

    private SingletonDemo2(){

    }

    // 方法需要同步,效率低
    public static synchronized SingletonDemo2 getInstance() {
        if(instance == null) {
            instance = new SingletonDemo2();
        }
        return instance;
    }
}

注:lazy load。延迟加载,懒加载。真正使用的时候才会加载。

问题:资源利用高了。但是,每次调用getInstance()方法都需要同步,并发效率较低。

双重检测锁实现模式

package com.kevin.创建型模式.单例模式;

/**
 * @author kevin
 * @version 1.0
 * @description     双重检测锁模式
 *      由于JVM底层内部模型原因,偶尔会出现问题,不建议使用
 * @createDate 2019/2/17
 */
public class SingletonDemo3 {

    // 类初始化时,不初始化这个对象,赋值为null,到使用的时候创建对象
    private static SingletonDemo3 instance = null;

    private SingletonDemo3() {

    }

    // 调用时,会先判断使用已经同步了对象
    public static SingletonDemo3 getInstance() {
        if(instance == null) {
            SingletonDemo3 sc;
            synchronized (SingletonDemo3.class){
                sc = instance;
                if(sc == null) {
                    synchronized (SingletonDemo3.class) {
                        if (sc == null){
                            sc = new SingletonDemo3();
                        }
                    }
                    instance = sc;
                }
            }
        }
        return instance;
    }

}

注:这个模式将同步内容下方到if内部,提高了执行的效率不必每次获取对象时都进行同步,只有第一次才同步创建了以后就没必要了。

问题:由于编译器优化原因和JVM底层内部模型原因,偶尔会出现问题,不建议使用。

静态内部类实现模式(懒加载方式)

package com.kevin.创建型模式.单例模式;

/**
 * @author kevin
 * @version 1.0
 * @description     静态内部类模式
 *      线程安全,调用效率高,但是,可以延时加载
 * @createDate 2019/2/17
 */
public class SingletonDemo4 {

    private SingletonDemo4() {

    }

    // 将加载对象放在静态内部类中,所以不会立即加载对象
    private static class SingletonClassInstance{
        private static final SingletonDemo4 instance = new SingletonDemo4();
    }

    // 只有真正调用getInstance()时才会加载静态内部类,加载类时是线程安全的。
    // instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证线程安全
    public static SingletonDemo4 getInstance() {
        return SingletonClassInstance.instance;
    }

}

注:

1.外部类没有static属性,则不会想饿汉式那样立即加载对象。

2.只有真正调用getInstance(),才会加载静态内部类,加载类时是线程安全的。instance是static final类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性。

3.兼备了并发高效调用和延迟加载的优势。

枚举式模式

package com.kevin.创建型模式.单例模式;

/**
 * @author kevin
 * @version 1.0
 * @description     枚举单例模式
 *      线程安全,调用效率高,不能延时加载
 * @createDate 2019/2/17
 */
public enum SingletonDemo5 {

    // 枚举元素,本身就是单例对象,但是没有延时加载。定义一个枚举的元素,它就代表了Singleton的一个实例
    INSTANCE;

    // 单例可以有自己的操作
    public void singletonOperation(){

    }
}

注:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障,避免通过反射和反序列化的漏洞。

问题:无延迟加载。

1.反射可以破解上面几种(不包含枚举式)实现方式

(可以在构造方法中手动抛出异常控制)

package com.kevin.创建型模式.单例模式;

import java.lang.reflect.Constructor;

/**
 * @author kevin
 * @version 1.0
 * @description     测试反射破解单例模式
 * @createDate 2019/2/17
 */
public class Test2 {

    public static void main(String[] args) throws Exception {
        // 不管创建多少次对象,都是指向同一个对象
        SingletonDemo6 s1 = SingletonDemo6.getInstance();
        SingletonDemo6 s2 = SingletonDemo6.getInstance();
        System.out.println(s1);
        System.out.println(s2);
        System.out.println("---------------");

        // 通过反射破解单例模式
        Class<SingletonDemo6> clazz = (Class<SingletonDemo6>) Class.forName("com.kevin.创建型模式.单例模式.SingletonDemo6");
        Constructor<SingletonDemo6> c = clazz.getDeclaredConstructor(null); //获取构造器
        c.setAccessible(true);  // 设置为true可以访问private对象
        SingletonDemo6 s3 = c.newInstance();
        SingletonDemo6 s4 = c.newInstance();
        System.out.println(s3);
        System.out.println(s4);

    }
    
}
package com.kevin.创建型模式.单例模式;

import java.io.Serializable;

/**
 * @author kevin
 * @version 1.0
 * @description     懒汉式单例模式(测试如何防止反射漏洞)
 * @createDate 2019/2/17
 */
public class SingletonDemo6 implements Serializable {

    private static SingletonDemo6 instance;

    // 手动抛出异常,避免通过反射创建多个单例对象
    private SingletonDemo6() throws Exception {
        if(instance != null) {
            throw new Exception("只能创建一个对象");
        }
    }

    public static synchronized SingletonDemo6 getInstance() throws Exception {
        if(instance == null) {
            instance = new SingletonDemo6();
        }
        return instance;
    }

}

2.反序列化可以破解上面几种(不包含枚举式)实现方式。   

      可以通过定义readResolve()防止获得不同对象。

      反序列化时,如果对象所在类定义了readResolve(),(实际是一种回调),定义返回哪个对象。

package com.kevin.创建型模式.单例模式;

import java.io.*;

/**
 * @author kevin
 * @version 1.0
 * @description     测试反序列化破解单例模式
 * @createDate 2019/2/17
 */
public class Test2 {

    public static void main(String[] args) throws Exception {
        // 不管创建多少次对象,都是指向同一个对象
        SingletonDemo6 s1 = SingletonDemo6.getInstance();
        SingletonDemo6 s2 = SingletonDemo6.getInstance();
        System.out.println(s1);
        System.out.println(s2);
        System.out.println("---------------");

        // 通过反序列化破解单例模式
        FileOutputStream fos = new FileOutputStream("D:/a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(s1);
        oos.close();
        fos.close();

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D:/a.txt"));
        SingletonDemo6 s5 = (SingletonDemo6) ois.readObject();
        System.out.println(s5);

    }
    
}
package com.kevin.创建型模式.单例模式;

import java.io.ObjectStreamException;
import java.io.Serializable;

/**
 * @author kevin
 * @version 1.0
 * @description     懒汉式单例模式(测试如何防止反序列化漏洞)
 * @createDate 2019/2/17
 */
public class SingletonDemo6 implements Serializable {

    private static SingletonDemo6 instance;

    private SingletonDemo6(){
        
    }

    public static synchronized SingletonDemo6 getInstance() throws Exception {
        if(instance == null) {
            instance = new SingletonDemo6();
        }
        return instance;
    }

    // 反序列化时,如果对象所在类定义了readResolve(),(实际上是一种回调),定义返回哪个对象
    private Object readResolve() throws ObjectStreamException {
        return instance;
    }

}

效率测试

常见的五种单例模式在多线程环境下的效率测试,相对值:

 

测试5种单例模式的效率

package com.kevin.创建型模式.单例模式;

import java.util.concurrent.CountDownLatch;

/**
 * @author kevin
 * @version 1.0
 * @description     测试5种单例模式的效率
 * @createDate 2019/2/17
 */
public class Test3 {

    public static void main(String[] args) throws Exception {
        test1();
        test2();
        test3();
        test4();
        test5();

    }

    public static void test1() throws Exception {
        long start = System.currentTimeMillis();
        int threadNum = 10; // 线程数
        final CountDownLatch countDownLatch = new CountDownLatch(threadNum);

        for (int i = 0; i < threadNum ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        Object o = SingletonDemo1.getInstance();
                    }
                    countDownLatch.countDown(); // 每次线程执行完将计数器减一
                }
            }).start();
        }

        countDownLatch.await(); // main线程阻塞,直到计数器变为0,才会继续往下执行

        long end = System.currentTimeMillis();
        System.out.println("饿汉式总耗时: " + (end-start));
    }

    public static void test2() throws Exception {
        long start = System.currentTimeMillis();
        int threadNum = 10; // 线程数
        final CountDownLatch countDownLatch = new CountDownLatch(threadNum);

        for (int i = 0; i < threadNum ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        Object o = SingletonDemo2.getInstance();
                    }
                    countDownLatch.countDown(); // 每次线程执行完将计数器减一
                }
            }).start();
        }

        countDownLatch.await(); // main线程阻塞,直到计数器变为0,才会继续往下执行

        long end = System.currentTimeMillis();
        System.out.println("懒汉式总耗时: " + (end-start));
    }

    public static void test3() throws Exception {
        long start = System.currentTimeMillis();
        int threadNum = 10; // 线程数
        final CountDownLatch countDownLatch = new CountDownLatch(threadNum);

        for (int i = 0; i < threadNum ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        Object o = SingletonDemo3.getInstance();
                    }
                    countDownLatch.countDown(); // 每次线程执行完将计数器减一
                }
            }).start();
        }

        countDownLatch.await(); // main线程阻塞,直到计数器变为0,才会继续往下执行

        long end = System.currentTimeMillis();
        System.out.println("双重检测锁式总耗时: " + (end-start));
    }

    public static void test4() throws Exception {
        long start = System.currentTimeMillis();
        int threadNum = 10; // 线程数
        final CountDownLatch countDownLatch = new CountDownLatch(threadNum);

        for (int i = 0; i < threadNum ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        Object o = SingletonDemo4.getInstance();
                    }
                    countDownLatch.countDown(); // 每次线程执行完将计数器减一
                }
            }).start();
        }

        countDownLatch.await(); // main线程阻塞,直到计数器变为0,才会继续往下执行

        long end = System.currentTimeMillis();
        System.out.println("静态内部类式总耗时: " + (end-start));
    }

    public static void test5() throws Exception {
        long start = System.currentTimeMillis();
        int threadNum = 10; // 线程数
        final CountDownLatch countDownLatch = new CountDownLatch(threadNum);

        for (int i = 0; i < threadNum ; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 100000; i++) {
                        Object o = SingletonDemo5.INSTANCE;
                    }
                    countDownLatch.countDown(); // 每次线程执行完将计数器减一
                }
            }).start();
        }

        countDownLatch.await(); // main线程阻塞,直到计数器变为0,才会继续往下执行

        long end = System.currentTimeMillis();
        System.out.println("枚举式总耗时: " + (end-start));
    }

}

CountDownLatch

同步辅助类,在完成一组正在其它线程中执行的操作前,它允许一个或多个线程一直等待。

countDown()当前线程调用此方法,则计数减一(建议放在finally里执行)

await(),调用此方法会一直阻塞当前线程,知道计时器的值为0

猜你喜欢

转载自blog.csdn.net/qq1021979964/article/details/87535219