设计模式轻松学【二】你想学的单例模式都在这

简介: 一个类仅有一个实例,由自己创建并对外提供一个实例获取的入口,外部类可以通过这个入口直接获取该实例对象。

概述

场景:很多时候整个应用只能够提供一个全局的对象,为了保证唯一性,这个全局的对象的引用不能再次被更改。比如在某个应用程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例类统一读取并实例化到全局仅有的唯一对象中,然后应用中的其他对象再通过这个单例对象获取这些配置信息。

如:Spring容器中的对象、Windows任务管理器、垃圾回收站、打印机打印

特点描述 特点值
常用程度 非常常用
适用层次 代码级、应用级
引入时机 程序编码,代码级
结构复杂度 简单
实现 封装对象产生的个数
体现原则

实现思路

  1. 将该类的构造方法定义为私有方法,这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例

    class Person{
        private Person() {
            //此类外部无法使用new关键字进行实例化
        }
    }
  2. 在该类内提供一个静态方法,当我们调用这个方法时,返回该类实例对象的引用。

    public static Person getInstance() {
        return person;
    }

饿汉式写法

/**
 * 饿汉式单例写法
 * 设计模式轻松学:君哥聊编程
 *
 */
public class Singleton1 {
    // 私有构造
    private Singleton1() {}

    private static Singleton1 single = new Singleton1();

    // 静态工厂方法
    public static Singleton1 getInstance() {
        return single;
    }
}

饿汉式单例在类加载初始化时就创建好一个静态的对象供外部使用,除非系统重启,这个对象不会改变,所以本身就是线程安全的。

缺点:Java反射机制支持访问private属性,所以可通过反射来破解构该造方法,产生多个实例

懒汉式写法

/**
 * 懒汉式写法
 * 设计模式轻松学:君哥聊编程
 *
 */
public class Singleton2 {

    // 私有构造
    private Singleton2() {}

    private static Singleton2 single = null;

    public static Singleton2 getInstance() {
        if(single == null){
            single = new Singleton2();
        }
        return single;
    }
}

懒汉式属于延迟加载范畴,好处是当第一次使用到时才会进行实例化,但缺点是在多线程环境下面会产生多个single对象,现在我们采用多线程来进行懒汉式单例的破解

多线程破解

/**
 * 懒汉式多线程测试
 * 
 * 设计模式轻松学:君哥聊编程
 *
 */
public class Test {

    public static void main(String[] args) {
        //开启两个线程,此时构造方法被访问了多次
        new SingletonThread().start();
        new SingletonThread().start();
    }
}

//编写一个线程类
class SingletonThread extends Thread{
    @Override
    public void run() {
        Singleton2 instance = Singleton2.getInstance();
        System.out.println("对象地址:" + instance);
    }
}

此时懒汉式在多线程模式下将不堪一击,产生了多个实例,该如何解决呢?接下来我们需要学习双检锁写法

双检锁写法

懒汉式多线程改进

package com.it235.singleton.eh;

/**
 * 懒汉式加锁
 * 设计模式轻松学:君哥聊编程
 *
 */
public class Singleton2 {

    // 私有构造
    private Singleton2() {
        System.out.println("构造方法");
    }

    private static Singleton2 single = null;
    
    public static Singleton2 getInstance() {
        
        // 等同于 synchronized public static Singleton3 getInstance()
        synchronized(Singleton2.class){
            if (single == null) {
                single = new Singleton2();
            }
        }
        return single;
    }
}

改进加锁后的懒汉式能够扛住多线程下的运行,但通过上述代码我们可以看到,每次访问都加上了锁。而其实除了第一次访问会发生多实例情况,后续都不会再次创建,所以极大的降低了效率,我们继续改进,双检锁写法产生

package com.it235.singleton.eh;
/**
 * 单例双检锁举写法
 * 
 * 设计模式轻松学:君哥聊编程
 *
 */
public class Singleton2 {

    // 私有构造
    private Singleton2() {
        System.out.println("构造方法");
    }

    private static Singleton2 single = null;
    
    public static Singleton2 getInstance() {
        // 等同于 synchronized public static Singleton3 getInstance()
        if (single == null) {
            synchronized(Singleton2.class){
                if (single == null) {
                    single = new Singleton2();
                }
            }
        }
        return single;
    }
}

此时解决了每次访问加锁问题,多线程也不在话下,但是我们仍然可以通过反射来访问private构造方法,破坏实例化规则,产生多个实例,所以枚举写法应运而生。

枚举写法

/**
 * 单例枚举写法
 * 
 * 设计模式轻松学:君哥聊编程
 *
 */
public enum Singleton2 {
    
    INSTANCE;
     
    public void something(){
        //做你想做的
    }
}

上面是推荐写法,但我们看完就懵圈了,完全不知道怎么回事,怎么用?

其实枚举写法,并不是要求你获取某个单例对象,而是通过枚举类直接去做你想做的事情,简单例子如下

// 单例
public enum SingletonEnum {

    Instance;
    
    private SingletonEnum() {
        System.out.println("枚举类初始化");
    }
    
    public String[] getProperties() {
        final String[] properties = new String[3];
        properties[0] = "属性1";
        properties[2] = "属性2";
        properties[3] = "属性3";
        return properties;
    }
}

//测试
public static void main(String[] args) {
     //第一次获取
        BeanContext b1 = BeanContext.Instance;
        b1.getProperties();
         //第二次获取
        BeanContext b2 = BeanContext.Instance;
        b2.getProperties();
}
枚举单例写法来源于Effective Java这本书

书中描述:单元素的枚举类型已经成为实现Singleton的最佳方法

思考:为什么枚举单例模式是最好的单例模式?

  1. 写法简单,简洁明了
  2. JDK定义枚举类的构造方法为private的。每个枚举实例都是static final类型的,也就表明只能被实例化一次。
  3. 创建枚举默认就是线程安全的,所以不必担心线程问题
  4. enum是来自于Enum类的,通过JDK我们可以看到Enum类的构造,可以看到枚举类提供了序列化机制

    public abstract class Enum<E extends Enum<E>> implements Comparable<E>, Serializable

    其能够阻止默认的反序列化,方法声明如下:

        /**
         * 阻止默认的反序列化操作
         * prevent default deserialization
         */
        private void readObject(ObjectInputStream in) throws IOException,
            ClassNotFoundException {
            throw new InvalidObjectException("can't deserialize enum");
        }
    
        private void readObjectNoData() throws ObjectStreamException {
            throw new InvalidObjectException("can't deserialize enum");
        }
  5. 反射类中的newInstance方法中,禁止对枚举进行实例化,详见Constructor类416行

    @CallerSensitive
    public T newInstance(Object ... initargs){
      ...省略
      if ((clazz.getModifiers() & Modifier.ENUM) != 0)
          throw new IllegalArgumentException("Cannot reflectively create enum objects");
      ...省略
    }

总结

不管采取何种方案,请时刻牢记单例的三大要点:

  • 线程安全
  • 延迟加载
  • 序列化与反序列化安全
目录
相关文章
|
7天前
|
设计模式 存储 SQL
PHP中的设计模式:单例模式的探索
在PHP开发中,单例模式是一种常用的设计模式,它确保一个类只有一个实例,并提供一个全局访问点。本文将通过一个简单的例子,逐步引导你理解如何在PHP中实现和利用单例模式,以及它在实际项目中的应用价值。
|
2月前
|
设计模式 缓存 安全
Java设计模式的单例模式应用场景
Java设计模式的单例模式应用场景
32 4
|
2月前
|
设计模式 SQL 安全
【设计模式】第二篇:单例模式的几种实现And反射对其的破坏
一个普通实例化,一个反射实例化 但是我们如果通过反射的方式进行实例化类,会有什么问题呢? public static void main(String[] args) throws Exception { Lazy1 lazy1 = getLazy1();
27 5
|
1天前
|
设计模式 存储 负载均衡
【五】设计模式~~~创建型模式~~~单例模式(Java)
文章详细介绍了单例模式(Singleton Pattern),这是一种确保一个类只有一个实例,并提供全局访问点的设计模式。文中通过Windows任务管理器的例子阐述了单例模式的动机,解释了如何通过私有构造函数、静态私有成员变量和公有静态方法实现单例模式。接着,通过负载均衡器的案例展示了单例模式的应用,并讨论了单例模式的优点、缺点以及适用场景。最后,文章还探讨了饿汉式和懒汉式单例的实现方式及其比较。
【五】设计模式~~~创建型模式~~~单例模式(Java)
|
14天前
|
设计模式 安全 程序员
C#设计模式之单例模式
C#设计模式之单例模式
29 3
|
8天前
|
设计模式 存储 数据库连接
Python设计模式:巧用元类创建单例模式!
Python设计模式:巧用元类创建单例模式!
17 0
|
12天前
|
设计模式 安全 测试技术
[设计模式]创建型模式-单例模式
[设计模式]创建型模式-单例模式
|
1月前
|
设计模式 安全 C++
C++一分钟之-C++中的设计模式:单例模式
【7月更文挑战第13天】单例模式确保类只有一个实例,提供全局访问。C++中的实现涉及线程安全和生命周期管理。基础实现使用静态成员,但在多线程环境下可能导致多个实例。为解决此问题,采用双重检查锁定和`std::mutex`保证安全。使用`std::unique_ptr`管理生命周期,防止析构异常和内存泄漏。理解和正确应用单例模式能提升软件的效率与可维护性。
26 2
|
1月前
|
设计模式 安全 Java
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
Java面试题:设计模式如单例模式、工厂模式、观察者模式等在多线程环境下线程安全问题,Java内存模型定义了线程如何与内存交互,包括原子性、可见性、有序性,并发框架提供了更高层次的并发任务处理能力
54 1
|
1月前
|
设计模式 安全 Java
技术成神之路:设计模式(一)单例模式
【7月更文挑战第3天】技术成神之路:设计模式(一)单例模式
27 1