单例模式

简介: 单例模式

# 单例模式需要满足:



  1. 私有的构造函数
  2. 懒加载
  3. 线程安全
  4. 通过静态方法来访问实例
  5. 无法通过反射来实例化对象
  6. 无法通过反序列化来实例化对象


1. 饿汉模式

package com.futao.springbootdemo.design.pattern.gof.a.singleton;
/**
 * 单例模式1-饿汉模式,即在类加载的时候就实例化对象。
 *
 * @author futao
 * Created on 2018-12-25.
 */
public class EagerSingleton {
    /**
     * 因为该字段是静态的,属于类,所以会在类加载的时候就初始化,
     * 又因为类加载的时候是天然的线程安全的,所以不会有线程安全问题
     * <p>
     * 伴随着类的加载而实例化一个对象,如果该单例最后并未被使用,则浪费了系统资源
     */
    private static final EagerSingleton instance = new EagerSingleton();
    /**
     * 私有构造方法,防止用户随意new对象
     */
    private EagerSingleton() {
    }
    /**
     * 获取单例的静态方法,对于需要频繁访问的对象使用这种方式比较好
     * 为什么不设置成final的,因为静态方法没必要设置成final的
     * 
     * @return 单例
     */
    public static EagerSingleton getInstance() {
        return instance;
    }
}

2. 懒汉模式

package com.futao.springbootdemo.design.pattern.gof.a.singleton;
import java.io.Serializable;
/**
 * 单例模式2-懒汉模式
 * 只有在用到的时候才实例化对象
 *
 * @author futao
 * Created on 2018-12-25.
 */
public class LazySingleton {
    private static LazySingleton instance;
    /**
     * 私有构造方法
     */
    private LazySingleton() {}
    /**
     * 会有线程安全问题,所以需要加上同步锁synchronized
     * 因为这种方式,同时只能被一个线程访问,其他线程都会被阻塞,所以多线程环境下获取对象的速度非常慢
     *
     * @return 单例对象
     */
    public static synchronized LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }
}

3. 枚举模式

package com.futao.springbootdemo.design.pattern.gof.a.singleton;
/**
 * 单例模式3-枚举式
 * 避免了反射与反序列化的漏洞
 * 但是没有懒加载的效果
 *
 * @author futao
 * Created on 2018-12-25.
 */
public enum SingletonEnum {
    /**
     * 这个枚举元素本身就是单例的
     */
    INSTANCE;
    private int field;
    /**
     * 枚举也可以有普通成员方法
     *
     * @param words
     */
    public void say(String words) {
        System.out.println(words);
    }
    public int getField() {
        return field;
    }
    public void setField(int field) {
        this.field = field;
    }}

4. 静态内部类模式(静态内部类实现的单例无法防止反射)

package com.futao.springbootdemo.design.pattern.gof.a.singleton.byself;
/**
 * 单例模式4-静态内部类
 * 线程安全,调用效率高,并且实现了延时加载
 *
 * @author futao
 * Created on 2019-04-03.
 */
public class StaticInnerClassSingleton {
    private StaticInnerClassSingleton() {
    }
    /**
     * 静态内部类并不会在类一开始加载的时候就加载
     * 要等到真正调用的时候才会加载
     * 又因为类加载是天然的线程安全的,所以不会有线程安全问题
     */
    private static class StaticInnerClass {
        private static final StaticInnerClassSingleton instance = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance() {
        return StaticInnerClass.instance;
    }
}
=================================================
测试内部静态类的加载时机
=================================================
package com.futao.springbootdemo.design.pattern.gof.a.singleton;
import org.apache.commons.lang3.StringUtils;
/**
 * 测试内部静态类的加载时机
 *
 * @author futao
 * Created on 2019-04-02.
 */
public class InnerStaticClassLoaderOrder {
    static {
        System.out.println("外部类被加载");
    }
    public void outerMethod() {
        System.out.println("调用外部类方法");
    }
    /**
     * 内部静态类
     */
    private static class InnerStaticClass {
        static int a;
        int b;
        static {
            System.out.println("内部类被加载了");
        }
        public void innerMethod() {
            System.out.println("调用静态类内部方法");
        }
    }
    /**
     * 内部类
     */
    private class InnerClass {
        //        static int a;//普通内部类不允许有静态成员
        int b;
        public void innerClassMethod() {
            System.out.println("println");
        }
    }
    public static void main(String[] args) {
        InnerStaticClassLoaderOrder i = new InnerStaticClassLoaderOrder();
        i.outerMethod();
        System.out.println(StringUtils.repeat("==", 30));
        //静态内部类通过new 外部类类名.内部类类名()的方式实例化对象
        InnerStaticClassLoaderOrder.InnerStaticClass innerStaticClass = new InnerStaticClassLoaderOrder.InnerStaticClass();
        innerStaticClass.innerMethod();
        /*
        输出为:外部类被加载
                调用外部类方法
                ============================================================
                内部类被加载了
                调用静态类内部方法
                说明内部静态类不会随着外部类的加载而加载,而是等到被实际调用的时候才加载
                         */
        InnerClass innerClass = i.new InnerClass();//普通内部类只能通过外部类对象.new 内部类()来实例化对象
        innerClass.innerClassMethod();
    }
}

# 测试


@Test
    public void test75() {
        System.out.println(EagerSingleton.getInstance());
        System.out.println(EagerSingleton.getInstance());
        System.out.println(EagerSingleton.getInstance());
        System.out.println(StringUtils.repeat("==", 30));
        System.out.println(LazySingleton.getInstance());
        System.out.println(LazySingleton.getInstance());
        System.out.println(LazySingleton.getInstance());
        System.out.println(StringUtils.repeat("==", 30));
        System.out.println(SingletonEnum.INSTANCE == SingletonEnum.INSTANCE);
        System.out.println(StringUtils.repeat("==", 30));
        System.out.println(StaticInnerClassSingleton.getInstance());
        System.out.println(StaticInnerClassSingleton.getInstance());
        System.out.println(StaticInnerClassSingleton.getInstance());
    }


image.png

# 如何防止反射来实例化对象


  • 首先看看如何通过反射创建一个对象

@SuppressWarnings("unchecked")
    @Test
    public void test76() throws Exception {
        //通过静态方法访问单例对象
        System.out.println(EagerSingleton.getInstance());
        System.out.println(EagerSingleton.getInstance());
        Class<EagerSingleton> eagerSingleton = (Class<EagerSingleton>) Class.forName("com.futao.springbootdemo.design.pattern.gof.a.singleton.EagerSingleton");
        //获取构造方法
        Constructor<EagerSingleton> constructor = eagerSingleton.getDeclaredConstructor();
        //因为构造方法是私有的,所以需要跳过java安全检查
        constructor.setAccessible(true);
        //通过反射创建新的对象
        EagerSingleton singleton = constructor.newInstance();
        System.out.println(singleton);
    }


image.png


  • 这样就破坏了对象的单例。但是从中可以看出,反射是通过调用构造方法来实例化对象的,所以考虑在构造方法进行拦截。

/**
     * 私有构造方法,防止用户随意new对象
     */
    private EagerSingleton() {
        if (instance != null) {
            //如果单例对象已经被创建,则不允许再调用构造方法创建对象
            throw new RuntimeException("不允许通过反射创建对象!");
        }
    }

# 如何防止反序列化来实例化对象


  • 如何通过反序列化来创建一个对象

首先如果要序列化与反序列化需要 implements Serializable

  • 序列化对象

//序列化对象
    @Test
    public void test77() throws Exception {
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("./a.txt"));
        objectOutputStream.writeObject(EagerSingleton.getInstance());
    }
  • 反序列化对象

@Test
    public void test77() throws Exception {
        //通过静态方法访问单例对象
        System.out.println(EagerSingleton.getInstance());
        System.out.println(EagerSingleton.getInstance());
        //反序列化对象
        ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("./a.txt"));
        EagerSingleton eagerSingleton = (EagerSingleton) objectInputStream.readObject();
        System.out.println(eagerSingleton);
    }


image.png

  • 反序列化破坏单例的解决方案
    在单例类中添加方法readResolve()

/**
     * 防止反序列化创建对象
     * 在jdk中ObjectInputStream的类中有readUnshared()方法,
     * 如果被反序列化的对象的类存在readResolve这个方法,
     * 他会调用这个方法来返回一个“array”
     * 然后浅拷贝一份,作为返回值,并且无视掉反序列化的值,即使那个字节码已经被解析。
     *
     * @return
     */
    private Object readResolve() {
        return instance;
    }


  • 再次执行上面的测试


image.png

# 各种单例模式效率测试


@Test
    public void test74() throws InterruptedException {
        int threadCount = 10;
        long start = System.currentTimeMillis();
        CountDownLatch countDownLatch = new CountDownLatch(threadCount);
        //开启10个线程
        for (int i = 0; i < threadCount; i++) {
            new Thread(() -> {
                //10个线程并发获取单例对象1000W次
                for (int j = 0; j < 10000000; j++) {
                    Object o = EagerSingleton.getInstance();
                }
                //一个线程执行完成之后计数器-1
                countDownLatch.countDown();
            }).start();
        }
        //阻塞主线程进行等待,内部会一直检查计数器的值是否为0
        countDownLatch.await();
        long end = System.currentTimeMillis();
        System.out.println(end - start);
    }


  • 饿汉模式


image.png

懒加载模式


image.png


枚举模式

image.png

静态内部类模式


image.png


单例模式名称 测试线程数 单个线程访问对象次数 耗时
饿汉模式 10 1000W 165ms
懒汉模式 10 1000W 5750ms
枚举式 10 1000W 120ms
静态内部类 10 1000W 115ms


相关文章
|
7月前
|
设计模式 安全 测试技术
【C++】—— 单例模式详解
【C++】—— 单例模式详解
|
17天前
|
设计模式 安全 C#
C# 单例模式的多种实现
C# 单例模式的多种实现
|
7月前
|
设计模式
单例模式
单例模式
37 0
|
6月前
|
设计模式 安全 Java
单例模式分享
单例模式分享
23 0
|
设计模式 安全 编译器
2023-6-12-第三式单例模式
2023-6-12-第三式单例模式
74 0
|
7月前
|
C++
【C++ 单例模式】
【C++ 单例模式】
|
7月前
|
设计模式 安全
【单例模式】—— 每天一点小知识
【单例模式】—— 每天一点小知识
|
设计模式 前端开发
关于单例模式我所知道的
关于单例模式我所知道的
59 0
关于单例模式我所知道的
|
安全 Java
单例模式很简单
《基础系列》
117 0
单例模式很简单
|
设计模式 安全 Java
回顾一下单例模式
回顾一下单例模式