23种设计模式(一)单例模式


一、单例模式

概念

  • 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

  • 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

优点

  • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 2、避免对资源的多重占用(比如写文件操作)。

缺点

  • 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

实现单例模式最关键的步骤就是 构造器私有


1.1 饿汉式

/**
 * 单例模式是最常见的一种设计模式
 * 它有以下特点
 * 1、单例类只能有一个实例
 * 2、单例类必须自己创建自己的唯一实例
 * 3、单例类必须给所有其他对线提供这一实例
 *
 * 饿汉式单例模式
 * 饿汉式的特点就在于
 * 在类创建的同时就创建好了一个静态的对象供系统使用
 * 以后不会再改变,所以天生就是线程安全的
 * 但是这会造成内存的浪费
 * @author kimariyb
 */
public class HungryMan {

    /**
     * 静态、私有的声明一个常量对象
     * 并且将构造器私有
     * 这样只能通过一个静态的方法来创建这个对象
     * 这个对象是一个常量对象
     * 这样就达到来单例的目的
     */
    private static final HungryMan HUNGRY_MAN = new HungryMan();

    private HungryMan() {

    }

    public static HungryMan getInstance() {
        return HUNGRY_MAN;
    }
}

饿汉式单例无疑是最简单的,最容易实现的一种模式了


1.2 懒汉式

1.2.1 线程不安全的

? 线程不安全的懒汉式是最基本的实现方式,它最大的问题就在于不支持多线程安全。它在多线程下是不能工作的。

/**
 * 懒汉式单例
 * 懒汉式的特点就在于
 * 它不会造成内存的浪费
 * 但是它会带来多线程安全的问题
 * @author kimariyb
 */
public class LazyManUnsafe {

    private static LazyManUnsafe LazyManUnsafe;

    private LazyManUnsafe() {

    }

    public static LazyManUnsafe getInstance() {
        if (LazyManUnsafe == null) {
            LazyManUnsafe = new LazyManUnsafe();
        }
        return LazyManUnsafe;
    }

}

1.2.2 线程安全的

? 在原来的代码基础上加上关键字synchronized才能保证单例,这使得多线程安全得到保障。但是会影响效率。

/**
 * 这是一种线程安全的懒汉式
 * 它主要的原理就在于
 * 加上了锁 synchronized
 * 能在多线程中工作,但是这降低了效率
 * @author kimariyb
 */
public class LazyManSafe {
    private static LazyManSafe lazyManSafe;
    private LazyManSafe() {

    }

    public static synchronized LazyManSafe getInstance() {
        if (lazyManSafe == null) {
            lazyManSafe = new LazyManSafe();
        }
        return lazyManSafe;
    }
}

1.3 双重检测

? 这是一种能在多线程下运行并且高性能的单例模式

/**
 * 双重检测(dcl)
 * dcl 模式下最重要的莫过于 volatile 关键字
 * 它会解决指令重排的问题
 * @author kimariyb
 */
public class DoubleCheck {
    private volatile static DoubleCheck doubleCheck;

    private DoubleCheck() {

    }

    public static DoubleCheck getInstance() {
        if (doubleCheck == null) {
            synchronized (DoubleCheck.class) {
                if (doubleCheck == null) {
                    //这不是原子性操作
                    //1、分配内存空间
                    //2、执行构造方法,初始化对象
                    //3、把这个对象指向这个空间
                    doubleCheck = new DoubleCheck();
                }
            }
        }
        return doubleCheck;
    }
}

所谓的原子性操作指的是所处的层(layer)的更高层不能发现其内部实现与结构。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。


1.4 静态内部类

? 这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。

/**
 * 静态内部类
 * 在内部类中声明对象
 * @author kimariyb
 */
public class StaticHolder {

    private StaticHolder() {

    }

    public static class HolderClass {
        private static final StaticHolder STATIC_HOLDER = new StaticHolder();
    }

    public static StaticHolder getInstance() {
        return HolderClass.STATIC_HOLDER;
    }
}

1.5 枚举

? 在说明枚举如何实现之前,我们应该思考,为什么枚举能够实现单例模式,这样做的优点在哪里。

1.5.1 使用反射破坏单例

? 在 Java 中反射是一个很强大的功能,毫不夸张的讲,反射是万能的。

public static void main(String[] args) throws Exception {
  	//使用反射破坏单例
  	//使用反射得到构造器
  	Constructor declaredConstructor = 	
      Reflection.class.getDeclaredConstructor(null);
  	//构造器设置为可访问得
  	declaredConstructor.setAccessible(true);
  	Reflection reflection1 = declaredConstructor.newInstance();
  	Reflection reflection2 = declaredConstructor.newInstance();
  	//对比哈希值
  	System.out.println(reflection1.hashCode());
  	System.out.println(reflection2.hashCode());
}

? 输出结果如下:

根据哈希值,我们很清楚的看见单例模式被破坏了!

1.5.2 使用枚举避免被反射破坏

? 使用枚举来避免反射被破坏的原理,我们可以在 Java 源码中窥见一二。我们找到newInstance()方法的源码,可以看见一行 "Cannot reflectively create enum objects"

/**
 * 枚举类
 * 枚举本身也是一个 class
 *
 * @author kimariyb
 */
public enum EnumSingle {
    /**
     * 代替原来的对象
     */
    INSTANCE;

    public EnumSingle getInstance() {
        return INSTANCE;
    }
}