设计模式-单例模式
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给其他所有其他对象提供这一实例
一、饿汉式
所谓饿汉式就是在类创建的时候就已经将该实例创建,并以后不会发生改变,所以是线程安全的
当类初始化的时候已自动完成实例化,但是也因此可能会出现内存空间浪费的情况
public class Hungry{
// 私有化构造参数,保证所有的外部成员无法实例化该对象,保证了内存空间中只有一个对象
private Hungry(){}
// 在类创建时就已经创建好该对象并且在类初始化的时候就已经实例化该对象
private final static Hungry HUNGRY = new Hungry();
// 提供一个方法供外部获得该对象
public static Hungry getInstance(){
return HUNGRY;
}
}
二、懒汉式
所谓懒汉式就是不会在类创建时就实例,而是在第一次被调用的时候实例化自己,即想用的时候再实例化
public class LazyMan{
// 私有化构造函数
private LazyMan(){
System.out.println(Thread.currentThread().getName() + "懒汉式");
}
// 创建对象,但不实例,等需要用到的时候再实例
private static LazyMan lazyMan;
public static LazyMan getInstance(){
// 当该对象为空时,实例该对象,此为懒汉式
if(lazyMan == null)
lazyMan = new LazyMan();
return lazyMan;
}
}
但我们可以发现,这种情况在单线程下是可以实现单例的,但在多线程下呢?我们用代码测试一下
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
LazyMan.getInstance();
}).start();
}
}
此时我们可以发现,并没有实现单例
所以,对此我们需要对其进行加锁,使其在多线程并发下也能实现单例,但加锁有可能也会被两个线程拿到,所以我们需要进行两次检测,即双重检测锁模式的懒汉式单例,DCL懒汉式
public class LazyMan{
private LazyMan(){}
// 因为该对象不是类加载时就加载,先创建好
private static LazyMan lazyMan = null;
public static LazyMan getInstance(){
// 双重检测锁模式的懒汉式单例 DCL懒汉式
if (lazyMan == null){
synchronized (LazyMan.class){
// 当该对象为空时再去实例一个对象,这就是懒汉式
if (lazyMan == null)
lazyMan = new LazyMan();
}
}
return lazyMan;
}
}
但即使是这种情况下,该代码还是存在一定问题的,因为
lazyMan = new LazyMan(); // 不是一个原子性操作
在这句代码执行时总共经历了:
- 分配内存空间
- 执行构造方法,初始化对象
- 将这个对象指向这个内存空间
而由于指令重排问题使得线程在执行时不是按照这个步骤去执行,如当线程A执行方式为132时,线程B认为此时lazyMan对象已指向地址空间,不为null,从而直接返回该对象
对此,我们必须保证该对象避免指令重排,那么就在该对象创建前加 volatile
private volatile static LazyMan lazyMan;
三、静态内部类
// 静态内部类
public class Holder {
// 私有化构造方法
private Holder(){}
public static Holder getInstance(){
return InnerClass.HOLDER;
}
private static class InnerClass{
// 在内部类中创建并实例化该对象
private static final Holder HOLDER = new Holder();
}
}
但上述的单例都是不安全的,通过反射可以破坏该单例
public static void main(String[] args) throws Exception {
// 先获取单例类实例
LazyMan instance = LazyMan.getInstance();
// 通过类的反射获取无参的构造器
Constructor<LazyMan> declaredConstructor = LazyMan.class.getDeclaredConstructor(null);
// 将该构造器的访问权限打开
declaredConstructor.setAccessible(true);
// 实例一个新的对象
LazyMan lazyMan2 = declaredConstructor.newInstance();
System.out.println(instance == lazyMan2);
}
通过这么一番操作我们会发现,结果为false,表明该单例已被破坏,那么怎么去解决呢
当我们去看newInstance()的源码时,我们发现
我们是不能对枚举类通过反射进行创建
// enum本身也是一个class类
public enum EnumSingle {
// 默认就是单例的
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1 == instance2);
}
}
当我们用同样的方式进行对枚举类的操作时,我们会发现结果为:
那么此时的单例就无法被破坏了