单例模式在Java编程实践中经常用到。单例模式用于保证每个类只有一个实例存在。本文将对Java单利模式进行一个系统深入的介绍。
单例模式是为了保证一个类只有一个实例存在。注意,我们所说的每个类只有一个实例存在是相对于一个JVM和一个ClassLoader而言的。由于不同的类装载器装载的类位于不同的命名空间内,所以在同一个JVM还是可以有有不同的类装载器装载的不同的实例。但这些实例是不同的。
测试:
先建立如下Singleton类:
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){};
public static Singleton getInstance(){return instance; };
}
可以看出,这个类是单例的。
实现单例模式,有4个基本要点:
1. 将构造函数的访问属性设置为private/package private。这样,客户代码就不可能调用构造函数自行构建实例了。这样做的另一个作用就是Singleton类不能被扩展了。见private Singleton(){};
2. 在类的内部实现初始化。见private static Singleton instance = new Singleton();
3. 提供一个类变量。见private static Singleton instance = new Singleton();
4. 提供一个全局访问点.见public static Singleton getInstance(){return instance; };
以上的代码中,在Singleton第一次被使用时,Singleton被加载并初始化。所以这种方式也叫eager initialization.但很在很多情况下,初始化instance可能是很耗费资源的一个操作,如果被初始化的instance没有被使用,那么初始化的动作就过于浪费了。为此,人们引入lazy initialization.lazy initialization的第一个版本如下:
1 public class FlawedLazySingleton {
2
3 private static FlawedLazySingleton instance = null;
4
5 private FlawedLazySingleton() {
6 };
7
8 public static FlawedLazySingleton getInstance() {
9 if(instance == null);{
10 instance = new FlawedLazySingleton();
11 }
12
13 return instance;
14 };
15
16 }
在客户想通过访问getInsance方法得到实例的时候,程序先判断instance是否已被初始化,如果没有初始化,就进行初始化,然后返回初始化之后的实例给客户。由于getInstance只有在客户需要实例时才会被调用,所以就省去了在没有需求的情况下的不必要的初始化工作--初始化只有在第一次使用时才被初始化。
FlawedLazySingleton在单线程环境下可以很好的工作。但在多线程环境下会出现问题。如果两个线程同时调用getInstance方法,并且在line 9和line 10的使用线程调度器进行线程切换,那么线程A和线程B都判断出instance==null为真,如是线程A就初始化instance,线程B也初始化instance,这样线程A和B得到的就是2个不同的instance。这也就违背了单例模式的初衷了。
为了使FlawedLazySingleton在多线程环境下也能很好的工作,我们可以将getInstance方法设置为synchornized的,代码如下:
1 package singleton;
2
3 public class SynchronizedLazySingleton {
4
5 private static SynchronizedLazySingleton instance = null;
6
7 private SynchronizedLazySingleton() {
8 };
9
10 public synchronized static SynchronizedLazySingleton getInstance() {
11 if(instance == null);{
12 instance = new SynchronizedLazySingleton();
13 }
14
15 return instance;
16 };
17
18 }
上述代码有一个问题,那就是如果许多线程需要同时访问getInstance方法,由于同一时刻只有一个线程能获得SynchronizedLazySingleton类上的锁,那么其他线程只能等待,这样该方法的性能将不好。这个问题在高并发性的环境会比较明显。
为了改进SynchronizedLazySingleton的并发访问的性能问题,人们设计出了Double Check Lasy(DCL) initialization策略。该策略是基于如下观察:
Singleton只是在一次初始化化是才有可能发生重复初始化。一旦初始化完毕,即使没有同步锁,getInstance方法也能很好的工作,所以我们完全没必要把在方法级别上加同步锁,而只需要在初始化实例的代码上加上同步锁就好了。
1 package singleton;
2
3 public class FlawedSynchronizedLazySingleton {
4
5 private static FlawedSynchronizedLazySingleton instance = null;
6
7 private FlawedSynchronizedLazySingleton() {
8 };
9
10 public static FlawedSynchronizedLazySingleton getInstance() {
11 if (instance == null) {
12 synchronized (FlawedSynchronizedLazySingleton.class) {
13 instance = new FlawedSynchronizedLazySingleton();
14 }
15 }
16
17 return instance;
18 };
19
20 }
注意,我们去掉了getInstance方法上的synchronized修饰符,并且在初始化instance代码的部分放入了synchronized代码块。
上述代码的问题在于在line 11和line 12中,还是可能发生两个线程都判断出instance==null为真,因而重复初始化实例的情况还是会发生。如何避免此问题?我们只需要在同步快内部再进行一次判断即可。
1 package singleton;
2
3 public class DoubleCheckLazySingleton {
4
5 private static volatile DoubleCheckLazySingleton instance = null;
6
7 private DoubleCheckLazySingleton() {
8 };
9
10 public static DoubleCheckLazySingleton getInstance() {
11 if (instance == null) {
12 synchronized (DoubleCheckLazySingleton.class) {
13 if (instance == null) {
14 instance = new DoubleCheckLazySingleton();
15 }
16 }
17 }
18
19 return instance;
20 };
21
22 }
由于sychronized块只是在线程判断出instance==null的时候才会执行,所以该方法只有在实例化还没完成时才有可能影响性能。一旦初始化完毕,则根本不会影响到。所以该方法即保证了初始化只发生一次,也避免了高并发访问时的性能问题。
注意我们在instance前面加上了volatile修饰符。这样做的目的是为了保证line 14,编译器不要对instance的写操作进行优化。否则编译器可能会再为实例分配完内存但没有完全初始化过程就将实例的引用写入instance。如果去掉volatile修饰符,则线程A正在执行初始化的时候,线程B执行到line 11发现instance不为空,然后就得到了一个指向尚为完全初始化的对象实例。
接下来我们用不同的ClassLoader来装载这两个类:
package classload;
import java.net.URL;
import java.net.URLClassLoader;
public class ClassLoaderTest {
static Singleton s1 = null;
public static void main(String[] args) throws Exception {
URL[] urls = new URL[] { new URL("file:/C:/classload/classes/") };
final URLClassLoader loader = URLClassLoader.newInstance(urls);
Singleton s2 = Singleton.getInstance();
Thread t = new Thread(new Runnable() {
public void run() {
try {
Thread.currentThread().setContextClassLoader(loader);
s1 = Singleton.getInstance();
} catch (Exception ex) {
}
}
});
System.out.println(s1 == s2);
}
}
在上述代码中,主线程通过默认的SystemClassLoader装载了Singleton并产生了一个实例s2,在程序中我们启动了另一个线程,在这个线程中,我们将URLClassLoader设为当前的ClassLoader,这样在语句s1 = Singleton.getInstance()时所用到的Singleton类是由URLClassLoader加载的。
通过观察程序输出,我们验证了在同一JVM中的单例在不同的ClassLoader中是不一样的。
下面我们先来看看类的单例模式的一个雏形:
对象属性的单例模式是指对象属性只会有一个值,也就是说,属性的值(引用)一但被初始化就不会再改变了。