# 单例模式需要满足:
- 私有的构造函数
- 懒加载
- 线程安全
- 通过静态方法来访问实例
- 无法通过反射来实例化对象
- 无法通过反序列化来实例化对象
1. 饿汉模式
package com.futao.springbootdemo.design.pattern.gof.a.singleton;
/**
* 单例模式1-饿汉模式,即在类加载的时候就实例化对象。
*
* @author futao
* Created on 2018-12-25.
*/
public class EagerSingleton {
/**
* 因为该字段是静态的,属于类,所以会在类加载的时候就初始化,
* 又因为类加载的时候是天然的线程安全的,所以不会有线程安全问题
*
* 伴随着类的加载而实例化一个对象,如果该单例最后并未被使用,则浪费了系统资源
*/
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 = (Class) Class.forName("com.futao.springbootdemo.design.pattern.gof.a.singleton.EagerSingleton");
//获取构造方法
Constructor 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 |