如果要保证系统在一个类最多只能存在一个实例时,我们就需要单例模式。这种情况在应用中经常碰到,例如缓存池、数据库连接池、线程池、一些应用服务实例等等。在多线程环境中。为了保证实例的唯一性其实并不简单。
1、最简单的单例模式
为了限制该类的对象被随意的创建,需要保证该类构造方法是私有的,这样外部类就无法创建该类的对象;另外,为了方便给客户对象提供单例对象的使用,我们为提供一个全局访问点,如下:
package com.pattern.singleton;
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
}
public static Singleton getInstance() {
return instance;
}
}
Singleton类中只有一个构造方法,它是被private修饰的,客户对象无法创建该对象的实例。
我们为此单例实现了一个全局访问点 public static Singleton getInstance()方法。
注意:instance 变量是私有的,外界无法访问。此实现是线程安全的,当多个线程同时去访问该类getInstance()方法时,不会初始化多个不同的对象,因为JVM在加载此类的时候,对于static数据的初始化只能由一个线程执行且仅一次。
2、单例性能-延迟创建
如果出于性能方面的考虑,我们希望延迟实例化单例对象(static属性在加载类时就会被初始化),只有在第一次使用该类的实例时才去实例化。此时我们就可以使用延迟创建,我们可以把单例的实例化过程移至getInstance()方法中,而不是在加载时预先创建。当访问该方法时,首先判断该实例对象是不是已经被实例化过了,如果已被初始化,则直接返回这个对象的引用;否则,创建这个实例并初始化,如下:
package com.pattern.singleton;
public class UnThreadSafeSingleton {
private static UnThreadSafeSingleton instance = null;
public static UnThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new UnThreadSafeSingleton();
}
return instance;
}
}
注意: if(instance == null) 判断是否实例化完成了,该方法不是线程安全的。
2.1、线程安全
在高并发的环境中,getInstance()方法返回了多个指向不同的实例对象,原因如何呢?
Thread1 | Thread2 | |
1 if(instance==null) 2 3 instance = new UnThreadSafeSingelton(); 4 return instance; 5 6 | 1 2 if(instance==null) 3 4 instance = new UnThreadSafeSingelton(); 5 return instance; 6 |
如果这两个线程按照上述步骤执行,我们不难发现,在时刻1和2,由于还没创建单例对象,Thread1和Thread2都会进入创建单例的代码快分别创建实例。在时刻3 Thread1创建了一个实例对象,但是Thread2此时无法知道,于是继续创建一个新的实例对象,导致这两个线程持有的实例并非为同一个。
更为糟糕的是,在没有自动内存回收机制的语言平台(C++)会因为我们认为创建了一个单例对象,从而忽略了其他线程所产生的对象,不会手动去回收他们,从而引起内存泄露。
为了解决此类问题,我们给方法添加 synchronized关键字,如下:
package com.pattern.singleton;
public class UnThreadSafeSingleton {
private static UnThreadSafeSingleton instance = null;
public static synchronized UnThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new UnThreadSafeSingleton();
}
return instance;
}
}
这样,再多的线程访问都只会实例化一个单例对象。
3、Double-Check Locking
虽然在多线程环境下是线程安全了,但是在多线程高并发的情况下,给次方法加上synchronized关键字会是的性能大不如前。synchronized关键字对整个getInstance()方法同步是没有必要的:我们只要保证实例化这个对象的那段逻辑被一个线程执行就可以了,而返回引用的那段代码是没有必要同步的。如下:
package com.pattern.singleton;
public class DoubleCheckSingleton {
private volatile static DoubleCheckSingleton instance = null;
public static DoubleCheckSingleton getInstance() {
if (instance == null) { // check if it is created.
synchronized (DoubleCheckSingleton.class) { // synchronized creation block
if (instance == null) { // double check if it is created
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
注意: 在getInstance()方法里,首先判断次实例是否被创建了,如果还没有创建,首先使用synchronized同步实例化代码块。在同步代码块里,还需要再次检查是否已经创建了单例对象,因为:如果没有第二次检查,这是有两个线程 Thread A 和 Thrad B 同时进入该方法,他们都检测到instance 为null 不管那个线程先占据了同步锁,并创建了实例对象,都不会阻止另外一个线程进入实例代码块重新创建实例对象,这样,同样会生成两个实例对象,所以,我们在同步的代码块中,要进行第二次判断,判断该对象是否被创建。
属性instance是被volatile修饰的,因为volatile具有synchronized的可见性特点,也就是说线程能够自动发现volatile变量的最新值。这样,如果instance实例化成功,其他线程便能立刻发现。
4、Initialization on demand holder
package com.pattern.singleton;
public class LazyLoadedSingleton {
private LazyLoadedSingleton() {
}
private static class LazyHolder {
private static final LazyLoadedSingleton instance = new LazyLoadedSingleton();
}
public static LazyLoadedSingleton getInstance() {
return LazyHolder.instance;
}
}