设计模式----单例模式

8 篇文章 0 订阅
5 篇文章 0 订阅
本文详细介绍了设计模式中的单例模式,包括其定义、应用场景和类型。重点讲解了单例模式的多种实现方式,如饿汉式、懒汉式及其线程安全优化,以及使用volatile关键字、登记式/静态内部类和枚举实现单例。同时,文中还提到了如何通过反射和序列化破坏单例模式的实例。
摘要由CSDN通过智能技术生成

设计模式之单例模式


一. 简介

1. 什么是单例模式?

单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。

2. 单例模式的应用场景?
  • 网页中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 要求生产唯一序列号。
  • 创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等
3. 单例模式的类型?
  • 懒汉式:在真正需要使用对象时才去创建该单例类对象
  • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用

二. 单例模式的几种写法

1. 饿汉式

饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。

// 饿汉式
class Singleton {
    //1.私有化构造器函数
    private Singleton() {}
    
    //2.创建本类对象并指向本类引用
    private final static  Singleton instance = new Singleton();

    //3.提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        return instance;
    }
}

优点:

  • 这种写法比较简单,就是在类装载的时候就完成了实例化。避免了线程同步问题。
  • 在类加载的同时已经创建好一个静态对象,调用时反应速度快。
  • 线程安全

缺点:

  • 来类装载的时候就完成了实例化,没有达到Lazy Loading的效果。如果从始至终未使用过这个实例,则会造成实例的浪费。
2. 懒汉式

懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。,否则则先执行实例化操作。


public class Singleton {
    
    //1.私有化构造器函数
    private Singleton(){}
    
    //2.先不创建对象
    private static Singleton instance ;
    //3.提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        //4.如果instance==null 的时候再去创建,否则直接返回
        if (instance == null) {
            instance = new Singleton();
        }
        return instance ;
    }
  }  

缺点:

  • 线程不安全,多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及 往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。

在这里插入图片描述

3. 懒汉式(线程安全+性能优化)

要想懒汉式线程安全,最容易想到的方法就是加锁。


public class Singleton {
    //1.私有化构造器函数
    private Singleton() {}

    //2.先不创建对象
    private static Singleton instance;

    //3.提供一个公有的静态方法,返回实例对象
    public static synchronized Singleton getInstance() {
        //4.如果instance==null 的时候再去创建,否则直接返回
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

这样就规避了两个线程同时创建Singleton对象的风险,但是引来另外一个问题:每次去获取对象都需要先获取锁,并发性能非常地差,极端情况下,可能会出现卡顿现象。

性能优化:


public class Singleton {
    //1.私有化构造器函数
    private Singleton() {}

    //2.先不创建对象
    private static Singleton instance;

    //3.提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        //4.线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
        if (instance == null) {
            //5.线程A或线程B获得该锁进行初始化
            synchronized (Singleton.class) {
                if (instance == null) {
                    //6.其中一个线程进入该分支,另外一个线程则不会进入该分支
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

上面这段代码已经近似完美了,但是还存在最后一个问题:指令重排

4. 使用volatile防止指令重排

指令重排序是指:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能

创建一个对象,在JVM中会经过三步:

  1. 为singleton分配内存空间
  2. 初始化singleton对象
  3. 将singleton指向分配好的内存空间

在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。

在这里插入图片描述

使用volatile关键字可以防止指令重排序,​其原理较为复杂,这篇博客不打算展开,可以这样理解:使用volatile关键字修饰的变量,可以保证其指令执行的顺序与程序指明的顺序一致,不会发生顺序变换,这样在多线程环境下就不会发生NPE异常了。

volatile还有第二个作用:使用volatile关键字修饰的变量,可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。

最终的代码如下所示:

public class Singleton {
    //1.私有化构造器函数
    private Singleton() {}

    //2.使用volatile关键字修饰的变量
    private static volatile Singleton instance;

    //3.提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        //4.线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
        if (instance == null) {
            //5.线程A或线程B获得该锁进行初始化
            synchronized (Singleton.class) {
                if (instance == null) {
                    //6.其中一个线程进入该分支,另外一个线程则不会进入该分支
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

5. 登记式/静态内部类

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

public class Singleton {

    // 1. 私有化构造函数
    private Singleton (){}

    // 2. 使用SingletonHolder类装载Singleton类
    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }
    //3.提供一个公有的静态方法,返回装载Singleton的类
    public static final Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。

想象一下,如果实例化,instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第3 种方式就显得很合理。

6. 枚举

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。 这种方式是 Effective Java 作者 Josh Bloch
提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

示例如下:

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}
7. 粉碎懒汉式单例与饿汉式单例

无论是完美的懒汉式还是饿汉式,终究敌不过反射和序列化,它们俩都可以把单例对象破坏掉(产生多个对象)。

  1. 利用反射破坏单例模式
public static void main(String[] args) {
    // 获取类的显式构造器
    Constructor<Singleton> construct = Singleton.class.getDeclaredConstructor();
    // 可访问私有构造器
    construct.setAccessible(true); 
    // 利用反射构造新对象
    Singleton instance1= construct.newInstance(); 
    // 通过正常方式获取单例对象
    Singleton instance2= Singleton.getInstance(); 
    System.out.println(instance1== instance2); // false
}

利用反射,强制访问类的私有构造器,去创建另一个对象

  1. 利用序列化与反序列化破坏单例模式

public static void main(String[] args) {
    // 创建输出流
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Singleton.file"));
    // 将单例对象写到文件中
    oos.writeObject(Singleton.getInstance());
    // 从文件中读取单例对象
    File file = new File("Singleton.file");
    ObjectInputStream ois =  new ObjectInputStream(new FileInputStream(file));
    Singleton newInstance = (Singleton) ois.readObject();
    // 判断是否是同一个对象
    System.out.println(newInstance == Singleton.getInstance()); // false
}

两个对象地址不相等的原因是:readObject() 方法读入对象时,它必定会返回一个新的对象实例,必然指向新的内存地址

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值