单例模式 (Singleton Pattern )是指确保一个类在任何情况下有且仅有一个实例,并提供一个全局访问点。这就要求我们不能使用常规的构造器,而是提供一种机制来保证一个类只有一个实例。单例模式是创建型模式。J2EE 标准中的 ServletContext 、ServletContextConfig 等、Spring 框架应用中的ApplicationContext、数据库的连接池等也都是单例形式。单例模式在现实生活中应用也非常广泛,例如,公司 CEO、部门经理等。
饿汉式单例:在单例类首次加载时就创建实例。
饿汉式单例模式在类加载的时候就会立即初始化,并且创建单例对象。它是线程安全的,在线程还没出现以前就实例化了,不会存在访问安全问题。
下面,我们来看一下饿汉式单例模式的标准代码:
public class HungrySingleton {
private static final HungrySingleton hungrySingleton = new HungrySingleton();
/**
* 将构造方法隐藏起来,也就是构造方法私有化。这样,在外部就无法通过构造方法进行实例化
*/
private HungrySingleton() {
}
/**
* 提供一个全局访问点
*/
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
还有另外一种写法,利用静态代码块机制:
/**
*类加载优先于类实例化对象
* 类加载:程序从上到下,加载静态的初始化语句,初始化块和构造方法
* 类创建对象:程序从上到下,加载非静态的初始化语句,初始化块和构造方法
*/
public class HungrySingleton {
private static final HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton() {
}
public HungrySingleton getInstance() {
return hungrySingleton;
}
}
适用场景:饿汉式单例模式适用于单例对象较少的情况。
优缺点:这样写可以保证绝对线程安全,而且在单例类首次加载时就创建实例,执行效率比较高,同时没有任何锁,性能较高。但是它的缺点也很明显,就是所有对象在类加载的时候就实例化,这样一来,如果系统中有大批量的单例对象存在,那系统初始化就会导致大量的内存浪费。也就是说不管对象用与不用都占着空间,浪费了内存。
那有没有更优的写法呢?下面我们来继续分析。
懒汉式单例:被外部类调用时才创建实例。
2.1. 懒汉式单例模式的简单实现
为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是:单例对象在被使用时才会初始化,下面是懒汉式单例模式的简单实现:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
但这样写又带来了一个新的问题,如果在多线程环境下,就会出现线程安全问题。我们通过实验来验证一下,写一个线程类ThreadExecut:
public class ThreadExecut implements Runnable{
@Override
public void run() {
LazySingleton instance = LazySingleton.getInstance();
System.out.println("线程" + Thread.currentThread().getName() + "创建的对象" + ":" + instance);
}
}
客户端测试代码:
public class LazySingletonTest {
public static void main(String[] args) {
ThreadExecut threadExecut = new ThreadExecut();
Thread t1 = new Thread(threadExecut);
Thread t2 = new Thread(threadExecut);
t1.start();
t2.start();
System.out.println("-----------------------------------");
}
}
客户端测试代码,运行结果如下图所示:
从结果中可以看到,上面的代码有一定概率出现两种不同结果,这意味着上面的单例存在线程安全隐患。由于存在线程安全问题,所以会产生两种不同的结果,如下:
1. 两个线程获取到的是同一个实例,但获取到的同一个实例,又分为两种情况:
①两个线程按正常顺序先后执行;
②两个线程同时进入了LazySingleton中的if判断条件,由于抢占CPU资源问题,跑在前面的线程创建了对象之后,在执行打印语句之前,后面的另一个线程也创建了对象(instance = new LazySingleton()),将前者创建的对象覆盖,这样的话,控制台最终输出的是后者创建的对象。即使我们从执行的结果中看到两个线程获取到的对象相同,但线程安全隐患依旧存在,因为LazySingleton被实例化了两次;
2. 两个线程获取到的是不同的实例:两个线程同时进入了LazySingleton中的if判断条件,由于抢占CPU资源问题,跑在前面的线程创建了实例对象,并执行了输出语句之后,后面的另一个线程才继续执行,这个时候不仅会覆盖第一个线程创建的对象,执行的输出语句也会打印出不同的地址值,从结果中可以看到两个线程创建的对象不同;
下面,我们通过调试运行再具体看一下。这里,我们使用线程模式进行调试,通过手动控制线程的执行顺序来跟踪内存的变化。先在ThreadExecut类中打上断点,使用鼠标右键单击断点,切换为Thread模式,如下图所示:
然后给LazySingleton打上断点,同样标记为Thread模式
切回客户端测试代码,同样也打上断点,同时也改为Thread模式,如下图所示
以"Debug"模式运行之后,会看到"Debug"控制台可以自由切换Thread的运行状态,如下图所示
RUNNING:表示线程已经抢到CPU资源,开始运行
通过不断切换线程,并观测其内存状态,我们发现线程安全问题确实存在。在线程环境下,有时LazySingleton被实例化了两次,而我们得到的运行结果却可能是相同的两个对象,实际上是被后面执行的线程创建的对象覆盖了,我们看到的相同的对象,有可能是一个假象,线程安全隐患依旧存在。
2.2. 使用synchronized关键字,将获取实例的方法变成同步方法
那么,我们如何来优化代码,使得懒汉式单例模式在线程环境下安全呢?来看下面的代码,给 getlnstance()加上 synchronized 关键字,使这个方法变成线程同步方法:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
/**
* 加synchronized关键字,使getInstance()变成同步方法
*/
public synchronized static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
我们再来调试:当其中一个线程执行并调用 getlnstance()方法时,另一个线程再调用 getInstance()方法,线程的状态由 RUNNING 变成了 MONITOR,出现阻塞。直到第一个线程执行完getlnstance()方法,第二个线程才恢复到 RUNNING 状态继续调用 getlnstance()方法,如下图所示
上图展现了 synchronized 监视锁的运行状态,线程安全的问题得到了解决。但是,用synchronized 加锁时,在线程数量比较多的情况下,如果CPU分配压力上升,则会导致大批线程阻塞从而导致程序性能大幅下降。那还有没有其它的方式,不会导致大批线程阻塞?我们接着往下分析
2.3. 使用synchronized关键字,修饰代码块
既然使用synchronized关键字,将获取实例的方法变成同步方法时,在线程数量比较多的情况下,如果CPU分配压力上升,会导致大批线程阻塞从而导致程序性能大幅下降。那么,我们可以尝试缩小加锁的范围,使其在if判断里面生效:使用同步代码块,将创建实例部分放到同步代码块中。实现如下:
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
}
我们继续使用线程模式进行调试运行(2.1中有线程模式调试的详细步骤),发现运行结果和 2.1 中的产生的结果相同,也是会产生两种不同的结果。说明采用这种方式,也存在线程安全问题。那该怎么办呢?到底有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案是肯定的,我们来看双重检查锁的单例模式。
2.4. DCL(Double Check Lock)单例模式
public class LazyDoubleCheckSingleton {
private volatile static LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (LazyDoubleCheckSingleton.class) {
if (instance == null) {
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}