设计模式之单例模式

什么是单例模式

单例模式是一种创建设计模式,确保类只有一个实例,同时为此实例提供全局访问点,换句话说就是在JVM中,某个类只允许被创建一次(唯一实例),之后所有的操作都是基于同一个实例。
单例模式同时解决了两个问题(保证类只有一个实例、提供全局访问点),所以违反了单一职责原则。
单例模式

应用场景

单例模式主要用来确保某个类的实例只能有一个。

  1. 工厂类
  2. 配置类
  3. 日志类
  4. 资源管理类
  5. 工具类

各种Mgr和各种Factrory都可以使用单例模式。

单例模式优缺点

优点

  1. 保证了类只有一个实例,节省内存空间。
  2. 通过全局访问点获取对实例的访问。
  3. 避免重复创建销毁对象,减少GC,提供性能。

缺点

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

单例模式的实现方式

单例实现需要满足以下两点:

  1. 构造器私有化,防止外部实例化该对象。
  2. 提供一个静态方法获取实例,返回相同的实例 。

饿汉式(推荐)

/**
 * 饿汉式
 * 优点:类加载到内存后,就会实例化对象,JVM保证线程安全。
 * 缺点:没有达到懒加载的目的。
 * 总结:推荐使用,简单易用,至于懒加载,项目中你不使用它为什么要装载它。
 */
public class Singleton {
    /**
     * 初始化对象,也可以在static代码块初始化
     */
    private static final Singleton INSTANCE = new Singleton();

    /**
     * 私有化构造器,防止new
     */
    private Singleton() {}

    /**
     * 获取实例对象
     *
     * @return 实例对象
     */
    public static Singleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        //测试
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

懒汉式(一)

/**
 * 懒汉式(非线程安全)
 * 优点:达到了懒加载,按需加载。
 * 缺点:多线程下不安全。
 * 总结:不推荐,多线程下不安全。
 */
public class Singleton {

    private static Singleton INSTANCE = null;

    private Singleton() {}

    public static Singleton getInstance() {
        if (null == INSTANCE) {
            try {
                //模拟延迟,多线程下同时进入此代码块。
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                //通过hashCode打印,可以发现多线程下是不安全的
                System.out.println(Singleton.getInstance().hashCode());
            }).start();
        }
    }
}

懒汉式(二)

/**
 * 懒汉式(线程安全)
 * 优点:达到了懒加载,按需加载,解决了多线程下安全问题。
 * 缺点:通过synchronized解决,效率下降。
 * 总结:不推荐。
 */
public class Singleton {

    private static Singleton INSTANCE = null;

    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (null == INSTANCE) {
            try {
                //模拟延迟,多线程下同时进入此代码块。
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton();
        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                //通过hashCode打印,可以发现多线程下是安全的
                System.out.println(Singleton.getInstance().hashCode());
            }).start();
        }
    }
}

双重检查

/**
 * 双重检查
 * 优点:达到了懒加载,按需加载,使用双重检查能够减小锁机制带来的开销,解决了多线程下安全问题。
 * 缺点:实现略微复杂,使用volatile保证安全。
 * 总结:不推荐。
 */
public class Singleton {
    /**
     * 使用volatile通过内存屏障禁止指令重排序从而达到线程安全。
     */
    private static volatile Singleton INSTANCE = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (null == INSTANCE) {
            synchronized (Singleton.class) {
                if (null == INSTANCE) {
                    try {
                        //模拟延迟,多线程下同时进入此代码块。
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton();
                }
            }

        }
        return INSTANCE;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                //通过hashCode打印,可以发现多线程下是安全的
                System.out.println(Singleton.getInstance().hashCode());
            }).start();
        }
    }
}
为什么使用volatile?

new 对象并不是一个原子操作,new 对象时会有三个步骤:

  1. 内存申请。
  2. 调用构造器初始化对象。
  3. 将对象的引用赋值给变量。

其中1永远是第一步因为2,3都依赖于1,而2,3可能发生指令重排。
在多线程下,线程A进入,调用了INSTANCE = new Singleton(),假设率先执行的是步骤3,此时其他线程进来,发现INSTANCE不为NULL,就会直接返回,产生错误。

静态内部类

/**
 * 饿汉式
 * 优点:类加载到内存后,就会实例化对象,JVM保证线程安全。
 * 缺点:没有达到懒加载的目的。
 * 总结:推荐使用,简单易用,至于懒加载,项目中你不使用它为什么要装载它。
 */
public class Singleton {
    /**
     * 初始化对象
     */
    private static final Singleton INSTANCE = new Singleton();

    /**
     * 私有化构造器,防止new
     */
    private Singleton() {

    }

    /**
     * 获取实例对象
     *
     * @return 实例对象
     */
    public static Singleton getInstance() {
        return INSTANCE;
    }

    public static void main(String[] args) {
        //测试
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();
        System.out.println(s1 == s2);
    }
}

枚举

/**
 * 枚举方式
 * 优点:不仅保证了线程安全,还防止了序列化。
 * 缺点:没啥缺点。
 * 总结:最优。
 */
public class Singleton {

    private Singleton() {
    }

   private enum SingletonHelper{

        INSTANCE;

       SingletonHelper() {
           singleton = new Singleton();
       }
        private final Singleton singleton;

       private Singleton getInstance() {
           return singleton;
       }
   }

    public static Singleton getInstance() {
        return SingletonHelper.INSTANCE.singleton;
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                //通过hashCode打印,可以发现多线程下是安全的
                System.out.println(Singleton.getInstance().hashCode());
            }).start();
        }
    }
}

总结

以上几种单例模式实现方式中,除了枚举方式外,其他几种方式都可以通过序列化和反序列化绕过类的private构造方法从而创建出多个实例(实际开发中也不会有人去这么做,费力不讨好)。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值