设计模式——单例模式

单例模式简介

单例模式(Singleton Pattern)是较为简单的设计模式,而它又是最常用、最重要的一种设计模式。如:在前端开发中可能用到一个所有页面都会共享的一个状态,此时就可以使用单例模式。单例模式是创建型模式中的一种,它是创建对象的最佳方式,该模式涉及一个单一的类,这个类自己负责创建对象,并保证在程序的执行过程中只有一个实例存在,而外界只需要通过这个类中提供的某个方法来访问该实例。

单例模式的特点

  1. 只有一个实例。
  2. 实例必须是自己创建的
  3. 必须提供一个入口让其他对象访问该实例

单例模式的实现核心

  1. 私有构造器,即不让外界new对象,将创建对象的权力交由给自己
  2. 自己创建对象,并提供一个出口让外界可以访问

单例模式的分类

懒汉单例模式(延迟加载)

直接上代码:

/**
 * @Description 懒汉式
 * @Autor Peng hk
 * @Date 2020/9/20
 **/
public class LazySingleton {

    private static LazySingleton lazySingleton;
    private LazySingleton(){
        // 方便查看该对象被创建了几次
        System.out.println(Thread.currentThread().getName() + ":LazySingleton执行了。。");
    }
	// 其他对象访问单例的入口
    public static LazySingleton getInstance(){
        if (lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

这是最基础的单例模式,也是最简单的单例,下面我们写一个测试类来测试下是否该对象只创建了一次:

public class Test{
    public static void main(String[] args) {
        LazySingleton lazySingleton = LazySingleton.getInstance();
        LazySingleton lazySingleton2 = LazySingleton.getInstance();
        LazySingleton lazySingleton3 = LazySingleton.getInstance();
    }
}

输出结果:
在这里插入图片描述

然而,在单线程的环境下,似乎没有什么问题,而实际情况下,往往是多线程环境,那么会出现什么问题呢?我们可以来测试下多线程访问该实例:

class Test{
    public static void main(String[] args) {
        // 开启十个线程,并发访问该单例
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                LazySingleton instance = LazySingleton.getInstance();
            }).start();
        }
    }
}

输出结果:

在这里插入图片描述

从结果可以看出,构造函数执行了很多次,显然对象也被创建了很多次,那么在多线程的环境下,显然它是以上实现的懒汉单例是线程不安全的。

我们可以用synchronized关键字来修饰getInstance方法,保证器只能有一个线程去访问,可以很容易的解决以上问题,但是这种方式显然效率很低,因为每一个线程都要等待,相当于依次去执行了。因此我们可以通过双重校验锁来解决线程安全问题,同时在性能上也有保证,见下。

双重校验锁(基于懒汉单例的优化)

双重校验锁(DCL)在保证线程安全的前提下,同时保证其性能。

/**
 * @Description 双检锁/双重校验锁(DCL,double-checked locking) 实现懒汉式单例的线程安全
 * @Autor Peng hk
 * @Date 2020/9/20
 **/
public class LazyDCLSingleton {

    private static LazyDCLSingleton lazyDCLSingleton;

    private LazyDCLSingleton(){
        System.out.println(Thread.currentThread().getName() + ":LazyDCLSingleton执行了。。");
    }
    /*
        JDK 版本:JDK1.5 起
        是否 Lazy 初始化:是
        是否多线程安全:是
        实现难度:较复杂
        描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
        getInstance() 的性能对应用程序很关键。
    */
    public static LazyDCLSingleton getInstance(){
        if (lazyDCLSingleton == null){
            synchronized (LazyDCLSingleton.class){
                if (lazyDCLSingleton == null){
                    lazyDCLSingleton = new LazyDCLSingleton();
                }
            }
        }
        return lazyDCLSingleton;
    }
}

测试类:

class Test{
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
               LazyDCLSingleton lazyDCLSingleton = LazyDCLSingleton.getInstance();
            }).start();
        }
    }
}

输出结果:
在这里插入图片描述
你看到结果可能以为万事大吉了,其实并不是,我们来分析下首先来分析下JVM创建对象的步骤(以代码中的lazyDCLSingleton = new LazyDCLSingleton();为例):

从字节码的角度分析,正常的创建对象大概有如下几个步骤:
1. 在堆中开辟空间
2. 初始化实例
3. 将对象地址赋值给变量lazyDCLSingleton 

正常情况下创建对象是这样的步骤,但是JIT(编译器)或者是CPU在创建对象的时候可能会让指令码重排序,即在创建对象的时候可能不是按照123的顺序执行,其中3和2有可能会颠倒执行,如132的顺序执行,在多线程的情况下,先执行3,而后来的线程访问时,拿到对象,对象却没有初始化,有可能会出现问题,因此要保证对象创建的原子性Java中的关键字volatile可以防止指令重排序,保证对象的创建是按正常的指令码顺序执行。正确的DCL懒汉模式应该在此处加上volatile关键字,保证创建对象的原子性:

private volatile static LazyDCLSingleton lazyDCLSingleton;

静态内部类单例模式

这种利用ClassLoader的特性,保证类只会被加载依次,因此它是线程安全的,代码如下:

package com.phk.singleton;

/**
 * @Description 静态内部类单例模式
 * @Autor Peng hk
 * @Date 2020/9/20
 *
 * 是否 Lazy 初始化:是
 *
 * 是否多线程安全:是
 *
 * 实现难度:一般
 *
 * 描述:这种方式能达到双检锁方式一样的功效,但实现更简单。
 * 对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
 **/
public class InnerClassSingleton {
    private static InnerClassSingleton innerClassSingleton;

    private InnerClassSingleton() {
        System.out.println(Thread.currentThread().getName() + ":InnerClassSingleton执行了。。");
    }

    public static InnerClassSingleton getInstance() {
        return InnerClass.innerClassSingleton;
    }


    public static class InnerClass {
        private static final InnerClassSingleton innerClassSingleton = new InnerClassSingleton();
    }
}

class Test4 {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                InnerClassSingleton.getInstance();
            }).start();
        }
    }
}

测试结果:
在这里插入图片描述

饿汉单例模式

/**
 * @Description 饿汉式单例模式
 * @Autor Peng hk
 * @Date 2020/9/20
 **/
public class HungrySingleton {

    private static HungrySingleton hungrySingleton = new HungrySingleton();

    // 构造方法私有化,让外界无法去直接创建对象
    private HungrySingleton(){
        System.out.println(Thread.currentThread().getName() + ":HungrySingleton构造方法。。");
    }

    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

饿汉单例模式是线程安全的,但是这种方式是在类加载的时候初始化对象,可能导致内存的浪费,不过它没有加锁执行效率很高。

枚举(不常用)

/**
 * JDK 版本:JDK1.5 起
 * 是否 Lazy 初始化:否
 * 是否多线程安全:是
 * 实现难度:易
 *
 * 
 */
public enum EnumSingleton {

    INSTANCE;

    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

class Test5{
    public static void main(String[] args) {
        EnumSingleton instance = EnumSingleton.INSTANCE;
        EnumSingleton instance2 = EnumSingleton.INSTANCE;
        System.out.println(instance == instance2);
    }
}

描述:这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化(以上的很多方式都能够被反射机制破坏,这种方式不会被反射破坏)。

  • 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,
  • 防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
  • 不能通过 reflection attack 来调用私有构造方法。

补充:破解单例模式

1.反编译懒汉单例

	@Test
    public void test1(){
        Class<LazySingleton> lazySingletonClass = LazySingleton.class;
        try {
            // 获取无参构造
            Constructor<LazySingleton> declaredConstructor = lazySingletonClass.getDeclaredConstructor();
            // 强制打破private
            declaredConstructor.setAccessible(true);
            // 对象
            LazySingleton lazySingleton = declaredConstructor.newInstance();
            LazySingleton lazySingleton1 = declaredConstructor.newInstance();
            LazySingleton lazySingleton2 = declaredConstructor.newInstance();
            System.out.println(lazySingleton);
            System.out.println(lazySingleton1);
            System.out.println(lazySingleton2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

2.反编译饿汉单例

 	@Test
    public void test3(){
        Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
        try {
            Constructor<HungrySingleton> declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
            declaredConstructor.setAccessible(true);
            HungrySingleton hungrySingleton = declaredConstructor.newInstance();
            HungrySingleton hungrySingleton1 = declaredConstructor.newInstance();
            HungrySingleton hungrySingleton2 = declaredConstructor.newInstance();
            System.out.println(hungrySingleton);
            System.out.println(hungrySingleton1);
            System.out.println(hungrySingleton2);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值