1. “懒汉模式”(非线程安全)
由于其实例对象的创建是在调用 getInstance()
方法时完成的,所以”懒汉模式”又称作 延迟加载(当叫到它干活时才动,很懒,即懒汉);与其相对的立即加载 – “饿汉模式”将在第3节进行介绍。
下面来看看”懒汉模式”的Java代码实现:
/**
* 单例模式: 懒汉模式(线程不安全的)
*/
public class SingletonDemo1 {
private static SingletonDemo1 instance;
private SingletonDemo1() {}
public static SingletonDemo1 getInstance() {
if(instance == null) {
instance = new SingletonDemo1();
}
return instance;
}
}
测试其运行结果:
public class Demo {
public static void main(String[] args) {
new Thread() {
public void run() {
System.out.println(SingletonDemo1.getInstance().hashCode());
}
}.start();
new Thread() {
public void run() {
System.out.println(SingletonDemo1.getInstance().hashCode());
}
}.start();
new Thread() {
public void run() {
System.out.println(SingletonDemo1.getInstance().hashCode());
}
}.start();
}
}
在这里,我们开启了3个线程,每个线程的功能相同,都是调用 SingletonDemo1
中的 getInstance()
方法获取实例,并调用实例 hashCode()
方法打印其哈希值。
若取出来的对象实例都相同,则打印出来的哈希值结果应该相同。下面是测试多次后的典型结果:
图1
图2
正常情况下,其结果应该是图1所示结果,即获取的对象都相同;然而也有例外,在测试多次后,得到了图2所示的结果。
这表示了用这种单例模式获取实例的方法在单线程的情况下是没有问题的,但在多线程高并发的环境下,容易产生“多例”。
2. “懒汉模式”(线程安全)
(1)对方法体声明synchronized关键字
”懒汉模式”产生多例的原因是多个线程同时进入 getInstance()
方法,那对 getInstance()
方法声明 synchronized
关键字就可以防止此情况发生。
public class SingletonDemo2 {
private static SingletonDemo2 instance;
private SingletonDemo2() {}
public synchronized static SingletonDemo2 getInstance() {
if(instance == null) {
instance = new SingletonDemo2();
}
return instance;
}
}
用1中的方法开启3个线程获取实例并打印哈希值,测试多次后其结果如下:
由结果可以看出,此方法解决了多线程下的安全问题。不过当下一线程想获取实例对象,必须等上一对象释放锁后才能继续执行,若在高并发的环境下,效率是很低下的。
(2)采用同步代码块方式,对关键代码上锁
同步方法是对方法整体进行上锁,这对效率影响很大。可以将其改造成只对关键代码块,即创建对象的语句块进行上锁。
public class SingletonDemo7 {
private static SingletonDemo7 instance;
private SingletonDemo7() {}
public static SingletonDemo7 getInstance() {
synchronized(SingletonDemo7.class) {
if(instance == null) {
instance = new SingletonDemo7();
}
}
return instance;
}
}
用1中的方法开启3个线程获取实例并打印哈希值,测试多次后其结果如下:
由结果可以看出,此方法也解决了多线程下的安全问题。但由于 getInstance()
方法大部分代码块被锁住,其效率也比较低下。
3. DCL双重锁检验机制
这儿使用双重锁检验机制,可以实现多线程环境中延迟加载单例模式。
public class SingletonDemo4 {
private volatile static SingletonDemo4 instance;
private SingletonDemo4() {}
public static SingletonDemo4 getInstance() {
if(instance == null) {
synchronized(SingletonDemo4.class) {
if(instance == null) {
instance = new SingletonDemo4();
}
}
}
return instance;
}
}
在代码中,实例对象被定义为 volatile
变量,这保证了多线程环境下所有线程看到的都是实例变量实时值,即保证变量的可见性;并在判断对象为 null
后,在同步代码块中又进行一次判断,最后再创建实例对象。这种双重锁检验机制保证了多线程环境下单例模式的安全执行。
4. “饿汉模式”
前面说到“懒汉模式”是延迟加载,而“饿汉模式”是立即加载(饿汉者,非常着急,在 getInstance()
获取实例方法还没被调用时,对象就已经创建好了)。下面来看看饿汉的代码:
/**
* 单例模式3: 饿汉模式
*/
public class SingletonDemo3 {
private static SingletonDemo3 instance = new SingletonDemo3();
private SingletonDemo3() {}
public static SingletonDemo3 getInstance() {
return instance;
}
}
其运行结果如下:
5. 使用静态内部类
前面提到,DCL双重锁检验机制可以解决多线程单例模式的非线程安全问题。当然,使用其他方法也可以达到同样的目的,这里先介绍使用静态内部类的方式:
/**
* 单例模式: 静态内部类
*/
public class SingletonDemo5 {
private static class innerSingleton {
private static SingletonDemo5 instance = new SingletonDemo5();
}
private SingletonDemo5() {}
public static SingletonDemo5 getInstance() {
return innerSingleton.instance;
}
}
6. 使用静态代码块
静态代码块中的代码在使用类的时候就开始执行,所以可以使用这一特点设计单例模式。
public class SingletonDemo8 {
private static SingletonDemo8 instance;
static {
instance = new SingletonDemo8();
}
private SingletonDemo8() {}
public static SingletonDemo8 getInstance() {
return instance;
}
}
7. 使用枚举
枚举和静态代码块的特性相似,在使用枚举类时,构造方法将被自动调用,可以根据这一特性进行设计单例模式。
/**
* 单例模式6: 枚举
*/
public enum SingletonDemo6 {
instance;
private SingletonDemo6() {
//这里可以是一些初始化代码
}
//这里可以是任意方法
public void print() {
}
}
通过枚举设计单例模式,其调用对象方式与普通类不同,如下所示:
public class Demo {
public static void main(String[] args) {
new Thread() {
public void run() {
System.out.println(SingletonDemo6.instance.hashCode());
}
}.start();
new Thread() {
public void run() {
System.out.println(SingletonDemo6.instance.hashCode());
}
}.start();
new Thread() {
public void run() {
System.out.println(SingletonDemo6.instance.hashCode());
}
}.start();
}
}
其结果如下:
关于常见的单例模式设计方案可以总结为以上7种,在多线程环境下,我们应该选择合适的设计方案才能保证线程安全性。