前言
单例模式是比较经典的设计模式,本篇我们将结合实际案例说明单例模式的实现原理,以及存在的问题。
一、单例模式的基本定义
单例模式(Singleton Pattern)确保某一个类只有一个实例方法,而且自行实例化并向整个系统提供这个实例,这个类被称为单例类,它提供全局访问方法。单例模式的要点有三个:
- 某个类只能有一个实例
- 它必须自行创建这个实例
- 它必须自行向整个系统提供这个实例
二、懒汉式单例
代码实现如下:
public class LazyWaterCup {
// 定义一个静态引用
private static LazyWaterCup wc;
// 构造方法私有
private LazyWaterCup(){}
// 提供一个全局访问点
public static LazyWaterCup getInstance() {
if (wc == null){
wc = new LazyWaterCup();
}
return wc;
}
}
首先我们要声明一个类引用,其次构造方法必须是私有的,以保证外部对于此类无法直接通过new进行类的实例化,与此同时我们要提供一个对外的方法,在此方法中实现类的实例化,也需要进行一个判断,若类引用没有指向任何对象,就创建一个对象,让引用指向它。
测试结果如下:
public class Test {
public static void main(String[] args) {
LazyWaterCup lwc1 = LazyWaterCup.getInstance();
LazyWaterCup lwc2 = LazyWaterCup.getInstance();
LazyWaterCup lwc3 = LazyWaterCup.getInstance();
System.out.println("lwc1= " + lwc1);
System.out.println("lwc2= " + lwc2);
System.out.println("lwc3= " + lwc3);
}
}
如图三次调用都指向同一个对象,因为从第一个getInstance()被调用时,由于类引用指向->null所以会创建对象,后续再调用getInstance()方法就不会再创建对象而是将上一个对象地址返回。因此三次打印结果都是指向同一个对象。
三、饿汉式单例
代码如下:
public class HungryWaterCup {
// 定义一个静态引用
private final static HungryWaterCup wc = new HungryWaterCup();
// 构造方法私有
private HungryWaterCup(){}
// 提供一个全局访问点
public static HungryWaterCup getInstance() {
return wc;
}
}
饿汉式单例与懒汉式单例的区别在于它对于对象的创建比较急迫,在声明的时候就创建一个对象,使声明的引用指向它,它是在类的装载过程中就完成的。
测试结果如下:
public class Test {
public static void main(String[] args) {
HungryWaterCup hwc1 = HungryWaterCup.getInstance();
HungryWaterCup hwc2 = HungryWaterCup.getInstance();
HungryWaterCup hwc3 = HungryWaterCup.getInstance();
System.out.println("hwc1= " + hwc1);
System.out.println("hwc2= " + hwc2);
System.out.println("hwc3= " + hwc3);
}
}
四、懒汉式单例存在的线程安全问题
懒汉式单例是在getInstance()被调用的时候创建的,若此时有大量线程访问此方法,就会创建多个对象,我们可以测试一下:
在创建对象的时候先让线程睡眠1ms,方便模拟多线程环境。
public class Test {
public static void main(String[] args) {
for (int i = 0; i < 200; i++) {
Thread t1 = new Thread(() -> {
try {
LazyWaterCup lwc = LazyWaterCup.getInstance();
System.out.println("lwc->" + lwc);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "t1");
t1.start();
}
}
}
在测试类代码中,创建200个线程。运行结果如下图:
创建的对象很多不一样。解决线程安全问题,一般可以在可能出现并发安全的代码块加上对象锁,就是synchronized锁,我们上锁之后,再试试:
测试结果:
就可以保证创建的对象是同一个。
总结
今天对单例模式的分析就到这里了,后续会继续总结其他设计模式的原理与实现。