0%

单例模式

普通线程安全的单例模式

描述

即在一个JVM中,一个对象只有一个实例存在

问题

指令重排

在Java指令中创建对象和赋值操作是分开进行的,也就是说instance = new Singleton();语句是分两步执行的,但是JVM并不保证这两个操作的先后顺序;

理想顺序是:

​ a.分配对象的内存空间;

​ b.初始化对象;

​ c.设置instance指向刚分配的内存地址;

但经过JVM编译和CPU优化之后有可能是a>c>b;

即指令重排之后,当A线程执行同步代码,并执行到c后,此时B线程执行同步代码的第一次null检测,此时instance!=null,从而返回了一个没有初始化完成的instance对象;

线程安全优化的单例模式

描述

  1. 利用类加载的过程实现懒加载;

    单例模式使用内部类来维护单例的实现,JVM内部的机制能够保证当一个类被加载时,这个类的加载过程是线程互斥的;

    当第一次调用getInstance的时候,JVM能够保证instance只被创建一次,并且会保证把赋值给instance的内存初始化完毕,这样就不用担心1.1问题;同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题;

  2. 使用volatile修饰符

    该修饰符阻止了遍历访问前后的指令重排,保证了指令执行顺序;

    java volatile关键字

适用

解决指令重排的问题

问题

  1. 思想1不一定完美,若在构造函数中抛出异常,实例将永远得不到创建,会出错;
  2. 思想2无法防止利用反射来重复构建对象;

示例

  1. 思想1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 思想1
class Singleton {

//私有构造方法,防止被实例化
private Singleton() {}
// 使用一个内部类来维护单例
private static class SingletonFactory{
private static final Singleton instance = new Singleton();
}
// 获取实例
public static Singleton getInstance(){
return SingletonFactory.instance;
}
//如果该对象用于序列化,可以保证对象在序列化前后保持一致
public Object readResolve(){
return getInstance();
}
}
  1. 思想2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 思想2
class Singleton {
private volatile static Singleton instance = null;
//私有构造方法,防止被实例化
private Singleton() {}

public static Singleton getInstance() {
if (instance == null) {
synchronized(this) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

线程安全高级单例模式

描述

因为只需要在创建类的时候进行同步,所以只要将创建和getInstance()分开,单独为创建加synchronized关键字

适用

解决内部类或者volatile关键字遇到的问题1

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Singleton {
//私有静态实例,防止被引用,此处赋值null,目的是实现延迟加载;
private static Singleton instance = null;
//私有构造方法,防止被实例化
private Singleton() {}
// 初始化
private static synchronized void syncInit() {
if (instance == null) {
instance = new Singleton();
}
}
// 获取实例
public static Singleton getInstance(){
if (instance == null) {
syncInit();
}
return instance;
}
//如果该对象用于序列化,可以保证对象在序列化前后保持一致
public Object readResolve(){
return instance;
}
}

反射问题

利用反射打破单例

1
2
3
4
5
6
7
8
9
//获得构造器
Constructor con = Singleton.class.getDeclaredConstructor();
//设置为可访问
con.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = (Singleton)con.newInstance();
Singleton singleton2 = (Singleton)con.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2)); //==false

解决,利用枚举

​ 优点:利用枚举实现的单例模式不仅能够防止反射构造对象,还能保证线程安全;

​ 缺点:它并非使用懒加载,其实例对象是在枚举类被加载的时候进行初始化的;

1
2
3
public enum SingletonEnum {
INSTANCE;
}