单例模式

饿汉式

在类加载的时候直接将Singleton对象提前创建好,等需要用的时候调用getSingleton()方法获取即可。

提前创建,是一种空间换时间的方式,无法实现延迟加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 饿汉式
* 特点:
* 1. 空间换时间 -- 一上来就创建对象(慢),后面直接用就好;
* 2. 线程安全;
*/
class Singleton {
// 私有构造方法,其他类不能访问该构造方法,即无法通过构造器来实例化对象
private Singleton() {
}

// 私有当前类的实例对象,让外部对象无法直接对其进行访问
private static final Singleton s = new Singleton();

// 对外提供公共的访问方法
public static Singleton getS() {
return s;
}
}

JDK源码中的典型实现: java.lang.Runtime类 和 Unsafe类(只不过获取Unsafe实例时往往使用反射打破单例模式强行创建)。

JVM中的Runtime类就是饿汉式单例模型的典型实现,只能通过 Runtime.getRuntime()方法获取其实例对象。

懒汉式

等需要使用单例对象的时候才去实例化对象,是一种时间换空间的延迟加载方式,线程不安全,实际开发中一般不用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 懒汉式(延迟加载模式)
* 特点:
* 1. 时间换空间 -- 一上来只是声明(快),等需要的时候再创建对象
* 2. 线程不安全;在多线程访问时,懒汉式可能会创建多个对象
*/
class Singleton {
// 私有构造方法,其他类不能访问该构造方法
private Singleton() {
}

// 声明一个引用
private static Singleton s;

// 对外提供公共的访问方法
public static Singleton getS() {
if (s == null) {
// 在多线程时,线程1执行到这一步,刚刚创建完对象执行权就被别的线程抢占了,
// 另外一个线程执行到上一句的判断时依旧成立,就会再次进入并创建一个对象,这就不是单例了
s = new Singleton();
}
return s;
}
}

基于synchronized 线程安全型懒汉式

对于上述线程安全的问题,可以使用synchronized关键字加锁来保证线程安全,不过,加锁会导致串行化,会在一定程度上影响代码性能。

不过随着JDK对synchroniezd锁的优化,这种性能损耗也是越来越小,可以忽略不计。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
// 私有构造方法,其他类不能访问该构造方法
private Singleton() {
}

// 声明一个引用
private static Singleton s;

// 对外提供公共的访问方法
public static synchronized Singleton getSingleton() {
if (s == null) {
s = new Singleton();
}
return s;
}
}

DCL(Double Check Lock)

DCL(Double Check Lock),双重检查锁,比线程安全型的懒汉式运行效率更高。

对获取单例对象的请求有分流,只在获取不到单例对象的时候才会去创建对象;在单例对象存在时直接获取,方法的执行效率高;

DCL存在的问题:

由于JVM在编译和运行的时候都会对代码进行一定的优化,比如:指令重排;因此可能会导致NullPointerException – Singleton对象创建过程有指定重排。

当一个线程执行请求获取单例对象,进入同步代码块创建一个单例对象;另外一个线程也尝试获取单例对象,判断得知单例对象已经创建,直接返回创建的对象供其使用,但是在使用的过程中可能会出现空指针异常,因为指令重排可能会导致对象是创建出来了,但尚未完成初始化,所以另外一个线程获取的对象内部可能有值为null的属性;

JVM中对象的实例化过程简单地说:加载、链接、初始化、使用。

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
public class Singleton {

private static Singleton instance;

/**
* 私有构造方法,其他类不能访问该构造方法
*/
private Singleton() {
}

/**
* 获取单例对象
*/
public static Singleton getInstance() {
// 如果已经创建了对象,就直接返回已创建的对象,不需要获取锁这一环节了。
if (null == instance) {
synchronized (Singleton.class) {
// 再次判断对象是否为空是针对获取锁阻塞的场景,如果其他线程已经获取锁并创建了实例对象,等当期线程获取到锁时,对象已经被创建了。
if (null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}

禁止指令重排的DCL

volatile 关键字会禁止指令重排,保证创建对象的过程是有序的,即可保证使用的对象都是已经完成了初始化操作的完整对象;

JVM使用指令重排的目的就是优化性能,所以频繁使用volatile禁止指令重排会影响性能;

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
public class Singleton {
// Singleton对象的创建过程不会进行指令重排。
private static volatile Singleton instance;

/**
* 私有构造方法,其他类不能访问该构造方法
*/
private Singleton() {
}

/**
* 获取单例对象
*/
public static Singleton getInstance() {
if (null == instance) {
synchronized (Singleton.class) {
if (null == instance) {
instance = new Singleton();
}
}
}

return instance;
}
}

Holder

懒加载|线程安全|效率高,充分利用了JVM的类加载机制。

JVM中的类加载机制为懒加载,一个应用在启动的时候不会一上来就直接加载应用中所有引入的类,而是加载应用启动时有使用的类,剩余没有加载的类只有在用到的使用才会加载。所以从Java应用启动的时候,就会创建一个方法区,随着业务场景的调用,使用到的类越来越多,方法区中的已使用内存也会越来越大,直到该应用中所有用到的类都经历了加载,那么方法区中的已使用内存大小就会趋于稳定不变。

当然了,也有例外,方法区中存放的是类信息和字符串常量池(JDK1.6),针对这两块在程序运行时还有有可能会越来越大。

类信息:运行时不断地增加加载的类。

  1. 自定义类加载器,去自定目录下加载自定义类;
  2. 反射,通过反射不断地生成Class文件;

字符串常量池:不断地创建字符串对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* 懒加载|线程安全|效率高
*/
public class Singleton {
/**
* 私有构造方法,其他类不能访问该构造方法
*/
private Singleton() {
}

// 利用静态内部类的类变量
private static class InstanceHolder {
// static保证唯一性,充分利用类加载机制。类只会被加载一次,所以只会创建一个Singleton对象。
public static final Singleton INSTANCE = new Singleton();
}

public static Singleton getInstance() {
return InstanceHolder.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
public class Singleton {

/**
* 私有构造方法,其他类不能访问该构造方法
*/
private Singleton() {
}

/**
* 充分利用了枚举项的构造函数只会执行一次的特点来进行单例对象的实例化;
*/
private enum SingletionEnum {
INSTANCE;

private final Singleton singleton;

SingletionEnum() {
this.singleton = new Singleton();
}

public Singleton getSingleton() {
return singleton;
}
}

public Singleton getInstance() {
return SingletionEnum.INSTANCE.getSingleton();
}
}

打破单例

Singleton单例模式从其定义出发就是永远获取的是同一个对象,但是可以通过反射来打破单例;

单例实现的核心就是通过私有化构造器,让外界无法通过构造器来创建对象,而是在类的内部维护了一个对象的引用,来实现单例;而反射可以在系统运行时通过Class对象来获取对象的构造器,包括私有构造器,然后通过获取的构造器对象来创建对象;这样就打破了单例模式。

Demo:

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
/**
* 单例模式不是绝对的,可以通过反射来破解单例;
*/
class SingletonFactory {
// 静态变量|共享
private static Singleton singleton;

// 静态代码块只会在类加载的时候执行一次
static {
try {
Class<?> aClass = Class.forName(Singleton.class.getName());
// 获取私有构造器
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
// 设置对私有构造器的访问
declaredConstructor.setAccessible(true);
// 通过私有构造器来创建一个对象
singleton = (Singleton) declaredConstructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
}

public static Singleton create() {
return singleton;
}
}

上述代码获取的就是一个新的单例对象,和Singleton.getIntance()获取的不是同一个对象;但是利用上述方法获取的都是同一个对象;因为创建对象的过程是在静态代码块中,所以创建对象的过程只会在类加载的时候执行一次,所以通过create()方法获取的都是同一个对象;

也可以写的更过分,将单例粉碎了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class SingletonFactory {
public static Singleton create() {
Singleton singleton = null;
try {
Class<?> aClass = Class.forName(Singleton.class.getName());
// 获取私有构造器
Constructor<?> declaredConstructor = aClass.getDeclaredConstructor();
// 设置对私有构造器的访问
declaredConstructor.setAccessible(true);
// 通过私有构造器来创建一个对象
singleton = (Singleton) declaredConstructor.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return singleton;
}
}

上述代码每获取一次对象,都是通过反射重新创建一个新的对象,返回的结果没有一个是相同的,都是新生成的对象,单例已经不复存在了。