# 单例模式需要满足:
- 私有的构造函数
- 懒加载
- 线程安全
- 通过静态方法来访问实例
- 无法通过反射来实例化对象
- 无法通过反序列化来实例化对象
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()); }
# 如何防止反射来实例化对象
- 首先看看如何通过反射创建一个对象
@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); }
- 这样就破坏了对象的单例。但是从中可以看出,反射是通过调用构造方法来实例化对象的,所以考虑在构造方法进行拦截。
/** * 私有构造方法,防止用户随意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); }
- 反序列化破坏单例的解决方案
在单例类中添加方法readResolve()
/** * 防止反序列化创建对象 * 在jdk中ObjectInputStream的类中有readUnshared()方法, * 如果被反序列化的对象的类存在readResolve这个方法, * 他会调用这个方法来返回一个“array” * 然后浅拷贝一份,作为返回值,并且无视掉反序列化的值,即使那个字节码已经被解析。 * * @return */ private Object readResolve() { return instance; }
- 再次执行上面的测试
# 各种单例模式效率测试
@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); }
- 饿汉模式
懒加载模式
枚举模式
静态内部类模式
单例模式名称 | 测试线程数 | 单个线程访问对象次数 | 耗时 |
饿汉模式 | 10 | 1000W | 165ms |
懒汉模式 | 10 | 1000W | 5750ms |
枚举式 | 10 | 1000W | 120ms |
静态内部类 | 10 | 1000W | 115ms |