Singleton

设计模式目录

单例模式是一种创建型设计模式, 让你能够保证一个类只有一个实例, 并提供一个访问该实例的全局节点。

解决方案

所有单例的实现都包含以下两个相同的步骤:

  • 将默认构造函数设为私有, 防止其他对象使用单例类的 new运算符。
  • 新建一个静态构建方法作为构造函数。 该函数会 “偷偷” 调用私有构造函数来创建对象,并将其保存在一个静态成员变量中。 此后所有对于该函数的调用都将返回这一缓存对象。

如果你的代码能够访问单例类, 那它就能调用单例类的静态方法。 无论何时调用该方法, 它总是会返回相同的对象。

单例模式结构

以下是来自核心 Java 程序库的一些示例:

识别方法: 单例可以通过返回相同缓存对象的静态构建方法来识别。

样例

1.懒汉模式(线程不安全)

package creational.singleton;

/**
 * 懒汉模式(线程不安全)
 */
public class Singleton_01 {
    private static Singleton_01 instance;

    private Singleton_01() {
    }

    public static Singleton_01 getInstance() {
        if (null != instance) return instance;
        instance = new Singleton_01();
        return instance;
    }
}

存在问题,如果多个访问者同时去获取对象实例,就会造成多个同样的实例并存,从而没有达到单例的要求

2.懒汉模式(线程安全)

package creational.singleton;

/**
 * 懒汉模式(线程安全)
 */
public class Singleton_02 {
    private static Singleton_02 instance;

    public Singleton_02() {
    }

    public static synchronized Singleton_02 getInstance() {
        if (null != instance) return instance;
        instance = new Singleton_02();
        return instance;
    }
}

此种模式虽然是安全的,但由于把锁加到方法上后,所有的访问都因需要锁占用导致资源的浪费。如果不是特殊情况下,不建议此种方式实现单例模式。

3.饿汉模式(线程安全)

package creational.singleton;

/**
 * 饿汉模式(线程安全)
 */
public class Singleton_03 {
    private static Singleton_03 instance = new Singleton_03();

    public Singleton_03() {
    }

    public static Singleton_03 getInstance(){
        return instance;
    }
}
  • 此种方式与我们最后一种实例化 Map基本一致,在程序启动的时候直接运行加载,后续有外部需要使用的时候获取即可。
  • 但此种方式并不是懒加载,也就是说无论你程序中是否用到这样的类都会在程序启动之初进行创建。
  • 那么这种方式导致的问题就像你下载个游戏软件,可能你游戏地图还没有打开呢,但是程序已经将这些地图全部实例化。到你手机.上最明显体验就一开游戏内存满了, 手机卡了,需要换了。

4.使用类的内部类(线程安全)

package creational.singleton;

/**
 * 使用类的内部类(线程安全)
 */
public class Singleton_04 {

    private static class SingletonHolder{
        private static Singleton_04 instance = new Singleton_04();
    }

    public Singleton_04() {
    }

    public static Singleton_04 getInstance(){
        return SingletonHolder.instance;
    }
}
  • 使用类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的方式耗费性能。
  • 这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载
  • 此种方式也是非常推荐的一种单例模式

5.双重锁校验(线程安全)

package creational.singleton;

/**
 * 双重锁校验(线程安全)
 */
public class Singleton_05 {
    private static Singleton_05 instance;

    public Singleton_05() {
    }

    public static Singleton_05 getInstance(){
        if(null != instance) return instance;
        synchronized (Singleton_05.class){
            if(null == instance){
                instance = new Singleton_05();
            }
        }
        return instance;
    }
}
  • 双重锁的方式是方法级锁的优化,减少了部分获取实例的耗时。
  • 同时满足了懒加载

6.CAS [AtomicReference] (线程安全)

package creational.singleton;

import java.util.concurrent.atomic.AtomicReference;

/**
 * CAS [AtomicReference] (线程安全)
 */
public class Singleton_06 {
    private static final AtomicReference<Singleton_06> INSTANCE = new AtomicReference<>();

    private static Singleton_06 instance;

    public Singleton_06() {
    }

    public static final Singleton_06 getInstance() {
        for (; ; ) {
            Singleton_06 instance = INSTANCE.get();
            if (null != instance) return instance;
            INSTANCE.compareAndSet(null, new Singleton_06());
            return INSTANCE.get();
        }
    }

}
  • 使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层硬件的实现,来保证线程安全。相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性。
  • 当然CAS也有一个缺点就是忙等,如果一直没有获取到将会处于死循环中。

7.Effective Java 作者推荐的枚举单例(线程安全)

package creational.singleton;

/**
 * Effective Java 作者推荐的枚举单例(线程安全)
 */
public enum Singleton_07 {

    INSTANCE;
    public void test(){
        System.out.println("hi~");
    }
}
  • Effective Java作者推荐使用枚举的方式解决单例模式,此种方式可能是平时最少用到的。
  • 这种方式解决了最主要的;线程安全、自由串行化、单一实例。

0.静态类使用

public class Singleton_00 {
	public static Map <String, String> cache = new ConcurrentHashMap<>();
}

单例模式优点

  • 你可以保证一个类只有一个实例。
  • 你获得了一个指向该实例的全局访问节点。
  • 仅在首次请求单例对象时对其进行初始化。

单例模式缺点

  • 违反了_单一职责原则_。 该模式同时解决了两个问题。
  • 单例模式可能掩盖不良设计, 比如程序各组件之间相互了解过多等。
  • 该模式在多线程环境下需要进行特殊处理, 避免多个线程多次创建单例对象。
  • 单例的客户端代码单元测试可能会比较困难, 因为许多测试框架以基于继承的方式创建模拟对象。 由于单例类的构造函数是私有的, 而且绝大部分语言无法重写静态方法, 所以你需要想出仔细考虑模拟单例的方法。 要么干脆不编写测试代码, 或者不使用单例模式。

同时解决了两个问题: 1.保证一个类只有一个实例; 2. 为该实例提供一个全局访问节点


参考资料:

小傅哥的《重学 Java 设计模式》

posted @ 2021-01-17 18:29  花染梦  阅读(325)  评论(0编辑  收藏  举报