GeekIBLi

设计模式之美-单例模式

2021-08-07

单例模式

概念

单例模式:指一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点(getInstance方法)。
大概实现就是隐藏其构造方法,单例模式属于创建型模式。
一些实际的应用场景比如,DBpool, ServletContext,ServletConfig等

单例模式写法

饿汉式单例

在单例类首次加载时创建实例;

1
2
3
4
5
6
7
8
9
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();

private HungrySingleton(){}

public static HungrySingleton getInstance(){
return hungrySingleton;
}
}

优点

执行效率高,没有加任何锁

缺点

类加载的时候就初始化,在某些情况下,可能会造成内存浪费;如果出现类的数量很多的时候,会初始化很多类,占用大量内存;

局限性

Spring就不能使用,Spring启动的时候,会有大量的类加载。

饿汉式的第二种写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class HungryStaticSingleton {

private static final HungryStaticSingleton hungrySingleton ;

static {
hungrySingleton = new HungryStaticSingleton();
}

private HungryStaticSingleton(){}

public static HungryStaticSingleton getInstance(){
return hungrySingleton;
}
}

区别仅仅实在与类加载的顺序不同。👇

懒汉式单例

被外部类调用时才创建实例;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LazySingleton {

private static LazySingleton instance = null;

private LazySingleton() {
}

public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}

优点

节省了内存

缺点

不能保证真实单例,线程不安全

线程不安全的原因

  • 后面的线程覆盖掉前面线程创建的实例
  • 同时进入判断条件,按顺序返回,没有覆盖的时候,就返回实例

解决方法

1
2
3
4
5
6
public static synchorized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}

但是getInstance方法添加上锁之后,性能下降,如果有很多请求访问,除了获得锁的线程之外,其他线程都要等待。

如何优化?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LazyDclSingleton {

private static LazyDclSingleton instance = null;

private LazyDclSingleton() {
}
// 后面的线程覆盖掉前面线程创建的实例
public static LazyDclSingleton getInstance() {
if (instance == null) {
synchronized (LazyDclSingleton.class) {
instance = new LazyDclSingleton();
}
}
return instance;
}
}

双重检查锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LazyDclSingleton {

private static volatile LazyDclSingleton instance = null;

private LazyDclSingleton() {
}

public static LazyDclSingleton getInstance() {
// 检查是否要阻塞
if (instance == null) {
synchronized (LazyDclSingleton.class) {
// 检查是否要创建实例
if (instance == null) {
instance = new LazyDclSingleton();
}
}
}
return instance;
}
}

局限性

会出现指令重排序的问题,有可能返回一个不完整的实例
解决方案:private static volatile LazyDclSingleton instance = null;(volatile禁止指令重排序)

优点

性能高,能保证线程安全

缺点

代码可读性查,不够美观,代码不够优雅

静态内部类写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 静态内部类
* 静态内部类在使用时才进行构建
* classPath: ../LazyInnerClassSingleton.class
* ../LazyInnerClassSingleton$LazyHolder.class
*
* 优点:写法优雅,利用了java语言语法,性能也高,避免内存的浪费
* 缺点:能够被反射破坏
*/
public class LazyInnerClassSingleton {
private LazyInnerClassSingleton(){}
public static LazyInnerClassSingleton getInstance(){
return LazyHolder.instance;
}

private static class LazyHolder{
private static final LazyInnerClassSingleton instance = new LazyInnerClassSingleton();
}
}

优点

1、写法优雅,利用了java语言语法
2、性能也高
3、避免内存的浪费

缺点

1、能够被反射破坏单例

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 反射破坏单例模式
*/
public class ReflectTest {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
Object instance = declaredConstructor.newInstance();
System.err.println(instance);
}
}
// 打印结果 com.ibli.javaBase.pattern.singleton.LazyInnerClassSingleton@38af3868

解决办法: 在构造器中添加一个判断,如果实例已经创建,则直接抛出异常终止创建;

1
2
3
4
5
private LazyInnerClassSingleton(){
if (LazyHolder.instance != null){
throw new IllegalArgumentException();
}
}

注册式单例

将每一个实例都缓存到一个容器中,使用唯一标志获取实例

枚举写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public enum  EnumSingleton  {

INSTANCE;

private Object object;

public Object getObject() {
return object;
}

public void setObject(Object object) {
this.object = object;
}

public static EnumSingleton getInstance(){
return INSTANCE;
}
}

使用与测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class EnumSingletonTest {

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingleton enumSingleton = EnumSingleton.getInstance();
enumSingleton.setObject(new Object());

// 尝试使用反射破坏
Class<?> clazz = EnumSingleton.class;
Constructor<?> declaredConstructor = clazz.getDeclaredConstructor(String.class,int.class);
System.err.println(declaredConstructor);
declaredConstructor.setAccessible(true);
Object object = declaredConstructor.newInstance();
System.err.println(object);
}
}

测试结果

1
2
3
4
private com.ibli.javaBase.pattern.singleton.EnumSingleton(java.lang.String,int)
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.ibli.javaBase.pattern.singleton.EnumSingletonTest.main(EnumSingletonTest.java:17)

原因在JDK底层源码中已经做了限制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@CallerSensitive
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
// 不能创建枚举类型
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}

优点

写法优雅,使用方便

缺点

和饿汉式一样,在某些情况下会造成大量内存浪费

容器式单例写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class ContainerSingleton {

private ContainerSingleton() {
}

private static Map<String, Object> ioc = new ConcurrentHashMap<>();

public static Object getInstance(String className) {
Object instance = null;
if (!ioc.containsKey(className)) {
try {
instance = Class.forName(className).newInstance();
ioc.put(className, instance);
} catch (Exception e) {
e.printStackTrace();
}
return instance;
} else {
return ioc.get(className);
}
}
}

测试类:

1
2
3
4
5
6
7
public class ContainerSingletonTest {
public static void main(String[] args) {
Object o1 = ContainerSingleton.getInstance("com.ibli.javaBase.pattern.singleton.Pojo");
Object o2 = ContainerSingleton.getInstance("com.ibli.javaBase.pattern.singleton.Pojo");
System.err.println(o1 == o2); // true
}
}

容器式单例写法适合创建大量单例实例的场景,类似与Spring的IOC容器。
当然上面的写法也会存在一个线程安全问题

序列化破坏单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 序列化:把内存中对象的状态转换为字节码的形式,然后在把字节码以IO输出流写到磁盘上
* 反序列化: 将持久化的字节码内容,通过IO流的方式读取到内存中,然后在转换成Java对象
*/
public class SerializableSingleton implements Serializable {

private static final SerializableSingleton serializableSingleton = new SerializableSingleton();

private SerializableSingleton() {
}

public static SerializableSingleton getInstance() {
return serializableSingleton;
}
}

测试类:

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
public class SerializableSingletonTest {
public static void main(String[] args) {
SerializableSingleton s1;
SerializableSingleton s2 = SerializableSingleton.getInstance();

FileOutputStream fos;
try {
fos = new FileOutputStream("SerializableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();


FileInputStream fis = new FileInputStream("SerializableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SerializableSingleton) ois.readObject();
ois.close();

System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}

结果:

1
2
3
com.ibli.javaBase.pattern.singleton.SerializableSingleton@20ad9418
com.ibli.javaBase.pattern.singleton.SerializableSingleton@681a9515
false

解决方法:在SerializableSingleton中添加一个方法

1
2
3
4
// 桥接模式
private Object readResolve() {
return serializableSingleton;
}

结果:

1
2
3
com.ibli.javaBase.pattern.singleton.SerializableSingleton@681a9515
com.ibli.javaBase.pattern.singleton.SerializableSingleton@681a9515
true

原因:ois.readObject();方法底层有对readResolve方法的判断,如果不存在这个方法,会利用反射生成一个新的实例;

ThreadLocal单例

下面介绍一种比较少见的一种单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ThreadLocalSingleton {

private static final ThreadLocal<ThreadLocalSingleton> threadLocalSingleton =
new ThreadLocal<ThreadLocalSingleton>() {
@Override
protected ThreadLocalSingleton initialValue() {
return new ThreadLocalSingleton();
}
};

private ThreadLocalSingleton(){}

public static ThreadLocalSingleton getInstance(){
return threadLocalSingleton.get();
}
}
1
2
3
4
5
6
public class ThreadLocalExector implements Runnable{
@Override
public void run() {
System.err.println(ThreadLocalSingleton.getInstance());
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
public class ThreadLocalSingletonTest {

public static void main(String[] args) {
System.out.println(ThreadLocalSingleton.getInstance());
System.out.println(ThreadLocalSingleton.getInstance());

Thread thread1 = new Thread(new ThreadLocalExector());
Thread thread2 = new Thread(new ThreadLocalExector());
thread1.start();
thread2.start();;
}
}

结果:

1
2
3
4
5
com.ibli.javaBase.pattern.singleton.ThreadLocalSingleton@38af3868  1
com.ibli.javaBase.pattern.singleton.ThreadLocalSingleton@38af3868 2

com.ibli.javaBase.pattern.singleton.ThreadLocalSingleton@10c69a60 3
com.ibli.javaBase.pattern.singleton.ThreadLocalSingleton@2f1eeb2f 4

以上3和4的结果虽然不一样,但是其实也是实现了【单例】的效果。

山脚太拥挤 我们更高处见。