23种设计模式之单例模式

23种设计模式之单例模式(Singleton Pattern)

创建型设计模式

意图: 类负责创建自己的对象,同时确保只有单个对象被创建,并且该类提供一种访问其唯一对象的方式,可以直接访问,不需要实例化。

关键代码: 构造函数私有。提供唯一一个获取对象的方法。

主要解决:一个全局使用的类频繁的创建和销毁
何时使用:当你想控制实例数目,节省系统资源的时候。

优点:

  1. 在内存中只有一个实例,减少了内存的开销(避免频繁的创建和销毁对象)

缺点:

  1. 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
  2. 长时间不使用实例对象,系统会认为该对象是垃圾对象而被回收。可能会造成对象状态的丢失。

注意:

 - 单例只能有一个实例
 - 单例类必须自己创建自己的唯一实例
 - 单例类必须给所有其他对象提供这一实例

1. 单例模式的几种实现方式:

1.1 . 懒汉式

懒加载,多线程的情况下不安全。

优点: 对象使用时再进行加载,节省内存

缺点: 多线程情况下不安全

// 懒加载多线程不安全方式:
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

// 懒加载多线程安全方式:
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

1.2 . 饿汉式

类加载时就创建对象,多线程安全
缺点: 类加载时就初始化,浪费内存
优点: 没有加锁,执行效率会很高。

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}

1.3. 双重检查锁\双重校验锁(DCL)

懒加载,多线程安全。

/***
* 第一次  if (singleton == null) 是为了不进入 synchronized ,提高性能
*
*第二次  if (singleton == null) 是为了不生成多个实例。
*
*/


public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        	if (singleton == null) {  
            	singleton = new Singleton();  
        	}  
        }  
    }  
    return singleton;  
    }  
}
双重检查锁中的volatile存在的意义:
volatile 保证有序性和禁止指令重排
why?: 使用javap -c 生成字节码文件,通过观察可以发现,创建一个对象实例的步骤不是一个原子性的操作,可以分为三步
1. 分配对象内存
2. 调用构造器方法,执行初始化
3. 将对象引用赋值给变量

两个线程,线程A 和 线程B  同时执行
当线程执行到 new Singleton 时,发生了指令重排序,执行了 分配对象内存,将对象引用赋值给变量,还没有来及执行 构造器的方法进行初始化,此时线程B 进入代码,进行 singleton 对象的 非null 校验,此时由于线程A 的作用 singleton 对象已经不为 Null 但是,对象中的属性值还没有被赋值,此时 线程 虽然拿到一个非 null 的对象,但是属性值还没有赋值好,使用起来会 发生异常。。。

synchronized 提供了有序性的保证,为什么还需要添加 volatile 关键字呢?
synchronized保证的有序性是多个线程之间的有序性,即被加锁的内容要按照顺序被多个线程执行。但是其内部的同步代码还是会发生重排序。

2. 破坏懒汉式单例和饿汉式单例

无论是完美的懒汉式还是饿汉式,通过反射和序列化都能破坏单例模式。

/**
* 通过反射,强制访问类的私有构造器,去创建另一个对象
*/ 

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

/**
* 通过序列化和反序列化破坏单例模式
*/ 
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
}

3. 枚举的方式防止单例模式被破坏

以上介绍了通过反射和序列化/反序列化可以破坏掉单例模式,接下来介绍一种无法破坏掉的创建单例方式。(枚举(枚举在jdk1.5后引进的))

public enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        System.out.println("这是枚举类型的单例模式!");
    }
}

4. 枚举方式创建单例的三种优势:(单元素的枚举类型)

  1. 代码简洁
  2. 天然的线程安全与单一实例
  3. 能保证单例模式不被破坏

4.1 天然的线程安全与单一实例

/**
* 在程序 启动时,会调用Singleton 的空参构造器,实例好一个 Singleton 对象赋给Instance,之后再也不会实例化。
*/
public enum Singleton {
    INSTANCE;
    Singleton() { System.out.println("枚举创建对象了"); }
    public static void main(String[] args) { /* test(); */ }
    public void test() {
        Singleton t1 = Singleton.INSTANCE;
        Singleton t2 = Singleton.INSTANCE;
        System.out.print("t1和t2的地址是否相同:" + t1 == t2);
    }
}
//输出
// 枚举创建对象了
// t1和t2的地址是否相同:true

4.2 枚举是如何保证单例模式不被破坏的

通过反射获取构造器,调用  newInstance()方法是,会判断是否是枚举类型,枚举类型会直接抛出异常。
序列化读取对象时,writeObject()方法,每个枚举类型和枚举姓名都是唯一的,所以在序列化时,仅仅只是对枚举的类型和变量名输出到文件中,在读入文件反序列化成对象时,使用Enum类的valueOf(String name)方法根据变量的名字查找对应的枚举对象。(所以,在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。)
public class ObjectOutputStream
    extends OutputStream implements ObjectOutput, ObjectStreamConstants
{
	public final void writeObject(Object obj) throws IOException {
	        if (enableOverride) {
	            writeObjectOverride(obj);
	            return;
	        }
	        try {
	            writeObject0(obj, false);
	        } catch (IOException ex) {
	            if (depth == 0) {
	                writeFatalException(ex);
	            }
	            throw ex;
	        }
	    }

	    private void writeObject0(Object obj, boolean unshared)
            throws IOException
    {
        boolean oldMode = bout.setBlockDataMode(false);
        depth++;
        try {
            // handle previously written and non-replaceable objects
            int h;
            if (
                    ...
            } else if (obj instanceof Class) {
                writeClass((Class) obj, unshared);
                return;
            } else if (obj instanceof ObjectStreamClass) {
               ...
            }
            ...
            // remaining cases
            if (obj instanceof String) {
               ...
            } else if (obj instanceof Enum) {
                writeEnum((Enum<?>) obj, desc, unshared);
            } else if (obj instanceof Serializable) {
               ...
            }
        } finally {
            ...
        }
    }
    }

枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王叮咚

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值