【设计模式】单例模式

定义

单例模式是指一个类在任何情况下,都只有一个实例。它提供了一个全局的访问点。

举例

饿汉式单例模式

饿汉式单例模式提供了final修饰的静态变量,所以在类加载的过程中,在加载到变量的时候就进行了初始化的操作。而不管创建后是不是要使用这个类。给人一种比较饥饿的感觉,所以就叫它饿汉式的单例模式。而且,由于在线程访问它之前,就进行了初始化,所以它就不会出现线程安全的问题,因而它是绝对线程安全的一种单例模式。

饿汉式单例模式的优点是它没有加任何锁,它的执行效率比较高,用户体验也比懒汉式单例模式好。缺点是,当要创建成单例模式的类非常多,而有些类又不会被用到的时候,就会非常的浪费内存。

  1. 简单的饿汉式单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 饿汉式单例模式
*/
public class HungrySimpleSingleton {

private static final HungrySimpleSingleton instance = new HungrySimpleSingleton();

private HungrySimpleSingleton(){}

public static HungrySimpleSingleton getInstance(){
return instance;
}

}
  1. 包含静态代码块的饿汉式单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 饿汉式单例模式(带静态代码块的)
*/
public class HungryStaticSimpleSingleton {

private static HungryStaticSimpleSingleton instance = null;

static {
instance = new HungryStaticSimpleSingleton();
}

private HungryStaticSimpleSingleton(){}

public static HungryStaticSimpleSingleton getInstance(){
return instance;
}

}

饿汉式单例模式适用于单例对象比较少的时候,为了解决内存浪费的问题,就需要用到下面的懒汉式单例模式。

懒汉式单例模式

懒汉式单例模式的特点是当外部需要用到的时候才会初始化,否则就不会初始化。

  1. 简单的懒汉式单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 懒汉式单例模式
*/
public class LazySimpleSingleton {

private LazySimpleSingleton(){}

private static LazySimpleSingleton lazySimpleSingleton = null;

public synchronized static LazySimpleSingleton getInstance(){

if(lazySimpleSingleton == null){
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}

}

但是这种写法也会有弊端:它会出现线程安全的问题。例如现在试着创建两个线程,然后在两个线程中创建类的实例,两个线程同时启动,看看它们创建的实例是不是同一个。

写一个线程类,然后打印出当前线程的名字和单例的内存地址值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14

/**
* 在线程池中创建懒汉式实例,打印出当前线程和懒汉式实例
*/
public class ExectorThread implements Runnable {

@Override
public void run() {

LazySimpleSingleton lazySimpleSingleton = LazySimpleSingleton.getInstance();

System.out.println(Thread.currentThread().getName() + ":" + lazySimpleSingleton );
}
}

在客户端进行测试,创建两个线程同时启动。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 懒汉式客户端测试类
*/
public class LazySimpleSingltonTest {

public static void main(String[] args) {

Thread thread1 = new Thread(new ExectorThread());
Thread thread2 = new Thread(new ExectorThread());

thread1.start();
thread2.start();

System.out.println("end");
}
}

通过最后的结果会发现,在某些情况下,两个单例的地址值并不相同。这是因为,当两个线程同时走到getInstance方法的if判断这里时,因为两个单例实例这个时候都为null,所以两个线程的非空判断都会为true,这个时候继续往下走,就会同时创建两个不同的实例对象。这样就违背了单例模式任何时候都只能有一个实例的定义。这就意味着上面的懒汉式单例模式的写法会出现线程安全的问题。

  1. 线程安全的懒汉式单例模式

因此需要在获取实例的getInstance方法上加入关键字synchronized,这样获取实例的方法就变成了同步方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 懒汉式单例模式(线程安全)
*/
public class LazySimpleSingleton {

private LazySimpleSingleton(){}

private static LazySimpleSingleton lazySimpleSingleton = null;

public synchronized static LazySimpleSingleton getInstance(){

if(lazySimpleSingleton == null){
lazySimpleSingleton = new LazySimpleSingleton();
}
return lazySimpleSingleton;
}

}

通过对线程进行debug调试发现,当第一个线程进入同步方法中时,另一个方法的状态会由running变成monitor。只有等第一个线程走出这个同步方法时,第二个线程才可以继续执行,否则它会一直处于监听的状态。

这样就保证了实例只会被创建一次,就解决了线程安全的问题。但是如果在线程很多的情况下,cpu的分配压力会变大,大量的线程阻塞会影响程序的效率和性能。那么,为了解决线程安全和线程安全导致效率低下的问题,就出现了下面的解决方法-双重检查锁的单例模式。

  1. 双重检查锁的懒汉式单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 双重检查索懒汉式单例模式
*/
public class LazyDoubleCheckSimple {

private static LazyDoubleCheckSimple singleton = null;

private LazyDoubleCheckSimple(){}

public static LazyDoubleCheckSimple getInstance(){
if(singleton == null){
synchronized (LazyDoubleCheckSimple.class){
if(singleton == null){
singleton = new LazyDoubleCheckSimple();
}
}
}
return singleton;
}

}

去掉了在getInstance方法上syncronized修饰符,转而在非空的判断里面加上同步代码块。这样所有的线程就可以进入到非空判断的里面,而不是在外面阻塞住。而且因为在同步代码块内部又加上了一层非空判断,所以最后也能保证不会创建两个相同的实例。这样在保证线程安全的情况下,就相对提高了程序的性能。可是由于加上了锁,终归还是会存在性能问题,那么有没有更好的解决方案呢。

这时候,就出现了静态内部类的懒汉式单例模式。

  1. 静态内部类的懒汉式单例模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 静态内部类懒汉式单例模式
*/
public class LazyInnerClassSingleton {

private static LazyInnerClassSingleton singleton = null;

private LazyInnerClassSingleton(){}

public static LazyInnerClassSingleton getInstance(){
return LazyHolder.LAZY;
}

private static class LazyHolder {

private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();

}
}

这种方式是从类的初始化的角度看的,只有在调用getInstance方法时,才会初始化内部类,然后获取到实例对象。如果不调用,就不会初始化内部类。这就解决了饿汉式单例模式的内存浪费和因为同步而带来的性能底下的问题。

但是以上的单例模式写法是不是又是完美的呢?下面我们来试图用反射,尝试一下破坏单例。通过反射获取单例类的私有化构造方法,然后通过设施setAccesible的方法为true暴力访问。然后调用两次newInstance方法,看创建的两个对象是否相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 通过暴力访问,破坏单例
*/
public class LazyInnerSingletonTest {

public static void main(String[] args) {
try{
Class<?> clazz = LazyInnerClassSingleton.class;
Constructor<?> c = clazz.getDeclaredConstructor(null);

c.setAccessible(true);

Object o1 = c.newInstance();
Object o2 = c.newInstance();

System.out.println(o1 == o2);
} catch (Exception e){
e.printStackTrace();
}
}
}

发现两个对象是不相等的,说明这种单例已经被反射暴力的破坏了。

  1. 防止反射破坏的静态内部类单例模式

那么现在来优化一下代码。在构造方法的内部,对静态内部类的LAZY单例对象进行非空的校验,如果不为空,说明已经被创建了,就抛出异常。不允许它被创建两次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 静态内部类懒汉式
*/
public class LazyInnerClassSingleton {

private static LazyInnerClassSingleton singleton = null;

private LazyInnerClassSingleton(){}

public static LazyInnerClassSingleton getInstance(){
if(LazyHolder.LAZY != null){
throw new RuntimeException("不允许创建多个实例");
}
}

private static class LazyHolder {

private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();

}
}

再次运行测试代码,会发现一个运行时异常被抛出了,就是自己在非空校验中抛出的异常。

  1. 防止序列化破坏的单例模式

一个单例对象创建好了之后,有时候需要将对象序列化的写入到磁盘中,下次再使用就从磁盘中读取对象并反序列化,将其转换为内存对象。反序列化后的对象将会重新分配内存,就是重新创建对象。如果这个序列化的对象是单例的,那么就违背了单例模式只能有一个实例的初衷。那么这个单例对象就会被序列化破坏。比如之前的懒汉式单例模式。

1
2
3
4
5
6
7
8
9
10
11
12
import java.io.Serializable;

public class SeralizeSingleton implements Serializable {

public static final SeralizeSingleton singleton = new SeralizeSingleton();

private SeralizeSingleton(){}

public static SeralizeSingleton getInstance(){
return singleton;
}
}

现在来创建测试例子,看看序列化的单例模式如何被破坏了。

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
28
29
30
31
public class SerealizesingletonTest {

public static void main(String[] args) {

SeralizeSingleton s1 = null;
SeralizeSingleton s2 = SeralizeSingleton.getInstance();

FileOutputStream fos = null;

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

FileInputStream fis = new FileInputStream("SeralizeSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
Object o = ois.readObject();
s1 = (SeralizeSingleton) o;
ois.close();

System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);

}catch (Exception e){
e.printStackTrace();
}
}
}

看看最后的结果。

由于将单例对象反序列化后重新了对象,重新分配了地址值,所以两个对象是不相等的。那么如何防止序列破坏单例模式呢,其实只需要加上readResolve方法就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.Serializable;

public class SeralizeSingleton implements Serializable {

public static final SeralizeSingleton singleton = new SeralizeSingleton();

private SeralizeSingleton(){}

public static SeralizeSingleton getInstance(){
return singleton;
}

private Object readResolve(){
return singleton;
}

}

再看看运行的结果。

那么,为什么加上readResolve方法后,出现的两个对象就相同了呢?这里就需要打开源码,去看看ObjectInputStream的readObject方法了。在readObject方法里面可以找到一个重写的方法readObject0。

然后点开这个readObject0,找到checkResolve方法,点击里面的readOrdinaryObject方法。

会发现,在这里一个判断,hasReadResolveMethod(),如果有readResolve这个方法,就会返回true。接着,就会调用invokeReadResolve方法,传入obj(就是要读的对象),然后将值赋给rep,然后将rep的值赋给obj,然后返回。实际上,这个过程就是,判断是否有readResolve方法,如果有就返回obj它自己,如果没有,通过下面的代码可以看到,它就会创建一个新的实例。所以如果在类中写了readResolve方法,就能够防止序列化破坏单例模式。

通过jdk的源码我们可以看出,虽然增加readResolve()方法能够防止序列化破坏单例模式,但是实际上,单例还是被创建了两次,只是没有被返回而已。如果创建对象发生的次数过于频繁,那么内存的开销也会变大。难道就没有办法从根本上解决问题了吗?下面的注册式单例也许就可以解决这个问题。

注册式单例模式
  1. 枚举式单例模式

先看看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 枚举式单例模式
*/
public enum EnumSingleton {

INSTANCE;

private Object data;

public Object getData() {
return data;
}

public void setData(Object data) {
this.data = data;
}

public static EnumSingleton getInstance(){
return INSTANCE;
}

}

写测试类,测试反序列化是否会破坏单例。

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
28
29
30
31
public class EnumSingletonTest {

public static void main(String[] args) {
try {

EnumSingleton s1 = null;
EnumSingleton s2 = EnumSingleton.getInstance();
s2.setData(new Object());

FileOutputStream fos = null;

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

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

System.out.println(s1.getData());
System.out.println(s2.getData());
System.out.println(s1.getData() == s2.getData());

} catch (Exception e) {
e.printStackTrace();
}
}
}

看看最后的结果。

可以看到,反序列化并不会破坏枚举式单例。

  1. 容器式单例模式

容器式单例模式是创建一个拥有静态Map容器的类,然后提供一个根据类名获取类的实例的方法。如果容易里面找不到这个实例,就新创建。如果找的到就将实例返回。

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
28
29
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* 容器式单例
*/
public class ContainerSingleton {
private ContainerSingleton(){}

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

public static Object getBean(String className){
synchronized (ioc){
if(!ioc.containsKey(className)){
Object obj = null;
try {
//通过反射获取对象的实例,然后创建对象
obj = Class.forName(className).newInstance();
ioc.put(className,obj);
}catch (Exception e){
e.printStackTrace();
}
return obj;
} else {
return ioc.get(className);
}
}
}
}

这种容器式单例模式适用于实例比较多的情况,比较的方便管理,它是线程不安全的单例模式。



The End.
  • 本文作者: 湛天伦
  • 本文链接: https://zhantianlun.com/6.html
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!