单例模式的正确书写方式

【推荐】2019 Java 开发者跳槽指南.pdf(吐血整理) >>> hot3.png

       单例模式在平常的生产工作中使用比较多,比如我们经常使用的Spring框架,其基本上所有的上下文环境和切面的实例都是单例的,这是单例模式应用的一种方式,即对应用程序的上下文环境进行控制;第二种方式是对一些公共资源的控制,比如我们设计一个篮球类的游戏,这里一场游戏中篮球只有一个,而争夺篮球的人则有很多个,因而我们必须对篮球类进行控制,使其自始至终只能有一个实例。

       为了保证一个类是单例的,那么我们这里就必须对实例的创建进行控制。如果一个类对客户端程序员提供了公有的构造方法,那么其还是可以创建多个实例的,因而,这里我们必须显示的为类创建一个构造器,并且将其声明为私有的。既然将构造器声明为私有的,那么怎么获取这个类的实例呢?这里我们就可以使用工厂方法,因为工厂方法属于类的一部分,我们可以在类内部声明该类的一个实例(因为是在类内部,其私有的构造方法也就可以访问,也就可以实例化),利用工厂方法返回该实例,这样我们就达到了只为该类创建一个实例的目的。基于以上两点,比较常见的单例模式的设计思路如下:

public class Singer {
  private static Singer INSTANCE = new Singer();

  private Singer() {}

  public static Singer getInstance() {
    return INSTANCE;
  }
}

或者是

public class Singer {
  private static Singer INSTANCE = null;

  private Singer() {}

  public static Singer getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Singer();
    }
    return INSTANCE;
  }
}

       第一种将该实例保存在一个静态域中,在加载Singer类时就已经初始化了该实例;第二种方式则是先声明一个静态引用,在工厂方法中对该引用进行初始化。对于客户端而言,其只需要按照如下即可获得该类的实例:

Singer singer = Singer.getInstance();

       对于上面两种初始化实例的方式,区别主要在于初始化的时机不一样,对于一些比较大或者初始化非常消耗系统资源的对象,应该使用第二种方式也即延迟初始化的方式,因为系统可能一直都不会使用该实例,因而无需在加载的时候就对其初始化消耗资源。

       这两种单例模式虽然都可以在形式上保证类只有一个实例,但这也不是绝对的。对象是否只有一个实例与对象的创建方式有关,因而我们应该思考对象创建的所有渠道以保证无论从哪种方式获取该对象都只能得到该对象的一个实例。在java中,创建对象的方式有四种:

  1. 通过new关键字进行创建;
  2. 使用ObjectInputStream和ObjectOutputStream创建;
  3. 通过反射调用对象的构造器创建;
  4. 使用对象的clone方法创建。

       前面我们介绍的两种创建单例模式的方式都只保证了通过第一种实例化的方式是无法创建的。这里我们看如下代码:

public class App {
  @Test
  public void testSingletonByStream() throws Exception {
    Singer singer = Singer.getInstance();

    File file = new File("classpath:singer.txt");
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream(file));
    out.writeObject(singer);
    out.close();

    ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
    Singer subSinger = (Singer) in.readObject();
    in.close();

    System.out.println(singer == subSinger);
  }
}

       当然,使用对象输入输出流对对象进行读写的时候必须要使Singer类实现Serializable接口,以保证其能序列化。这里运行这段程序,其输出结果为:

false

       这就说明通过输入输出流写入和读取的对象不是同一个对象,虽然两个对象的属性都是相同的。这也就造成了单例类有了两个实例的情形。那么如何避免这种方式获取的实例呢?在使用ObjectInputStream从文件中读取数据并将其恢复为一个对象的时候会调用该对象类型的readResolve方法,通过该方法创建该对象的实例,该方法是jvm自动帮我们加入的,那么如果我们重写该方法,并让该方法返回已经有的实例,对象输入输出流就不能创建实例了,具体代码如下:

public class Singer implements Serializable {
  private static Singer INSTANCE = null;

  private Singer() {}

  public static Singer getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Singer();
    }
    
    return INSTANCE;
  }
  
  public Object readResolve() {
    return INSTANCE;
  }
}

       重新运行前面的testSinletonByStream中的代码,会发现返回结果为

true

       从上面的分析可以看出,只要我们重写了readResolve方法,就可以避免客户端程序员通过对象输入输出流创建单例类的另一个实例。

       对于使用反射方式实例化一个类的方式,这里需要说明的是,反射实例化实际上也是调用了该类的构造方法,虽然我们将该类的构造方式声明为private修饰的,但是反射可以通过setAccessible方法来改变对象的方法属性的访问权限,比如如下代码:

public class App {
  @Test
  public void testSingletonByReflect() throws Exception {
    Singer singer = Singer.getInstance();

    Constructor<?>[] constructors = Singer.class.getDeclaredConstructors();
    for (Constructor<?> constructor : constructors) {
      if (constructor.getName().equals("chapter2.work8.Singer")) {
        constructor.setAccessible(true);
        Singer subSinger = (Singer) constructor.newInstance();
        System.out.println(singer == subSinger);
        break;
      }
    }
  }
}

       首先我们获取该类的所有构造方法,利用循环找到我们创建的构造方法,将该构造方法的访问权限提高之后通过该构造方法实例化了一个Singer对象,输出结果为

false

       通过输出结果我们可以看出通过反射确实可以另外创建该类的一个实例,这也违反了单例模式的初衷。那么如何避免这种方式创建对象呢?从上面的代码可以看出,利用反射创建该对象的实例实际上还是使用该类的构造方法,因而只要我们在该类的构造方法中进行判断,如果通过构造方法已经创建过一个对象,那么再次创建时我们就抛出一个异常以告知客户端程序员再次实例化是不允许的。具体代码如下:

import java.io.Serializable;

public class Singer implements Serializable {
  private static Singer INSTANCE = null;
  private static int count = 0;

  private Singer() {
    if (count >= 1) {
      throw new AssertionError("cannot construct other instance");
    }

    count++;
  }

  public static Singer getInstance() {
    if (INSTANCE == null) {
      INSTANCE = new Singer();
    }

    return INSTANCE;
  }

  public Object readResolve() {
    return INSTANCE;
  }
}

       可以看到,我们在构造方法中对通过构造方法创建实例的数目进行了统计,当已经实例化的数目不少于一个的时候将抛出异常。继续运行前面的testSingletonByReflect方法,可以发现这次程序抛出了我们所期望的异常。

       关于使用clone方法进行创建,这里我们需要简要说明一下。在Object类中,clone方法是受保护类型的,也就是说如果我们不重写该方法将其修改为public类型,客户端程序员是看不到该方法的,并且如果想正确的使用clone方法,子类必须实现Cloneable接口,否则调用clone方法时将抛出CloneNotSupportedException异常,无论是通过直接调用还是通过反射调用。因此,这里如果想让客户端程序员不通过clone方法产生该类型实例,我们只需要不对clone方法进行重写即可,并且在《Effective Java》中也建议不要使用clone方法来对一个对象进行克隆,其有非常多的缺点,如果确实想使用克隆的功能,正确的做法应该是使用克隆构造器或者是克隆工厂,在克隆构造器或者克隆工厂的方法声明中传入该类型的一个实例,从而对其进行深度克隆。

       上面所演示的单例类的创建方式其实还不是最终的书写方式,因为该方式并没有考虑多线程的问题,比如在上面所示的工厂方法中,如果两个线程都是首次加载该类,并且调用工厂方法获取实例的时候都是运行到 if (null == INSTANCE) 处,此时因为INSTANCE并没有实例化,其为null,这条判断对于两个线程来说都将返回true,然后两个线程都会创建各自的实例,也就产生了多个实例的情形。解决办法比较简单,这里只需要对必要的方法进行加锁即可,注意这里也必须对构造器和readResolve方法加锁,因为也有可能是多个线程同时使用反射或者对象输入输出流创建对象。具体的代码如下:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Singer {
    private static Lock lock = new ReentrantLock();
    private static volatile Singer INSTANCE = null;
    private static int count = 0;

    private Singer() {
        lock.lock();
        try {
            if (count >= 1) {
                throw new AssertionError("cannot construct other instance");
            }

            count++;
        } finally {
            lock.unlock();
        }
    }

    public static Singer getInstance() {
        if (null == INSTANCE) {
            lock.lock();
            try {
                if (null == INSTANCE) {
                    INSTANCE = new Singer();
                }
            } finally {
                lock.unlock();
            }
        }
        return INSTANCE;
    }
}

       对于这种单例模式的实现方式,首先由于Singer没有实现序列化接口Serializable,因而不能通过对象输入输出流创建对象,同理,其没有实现Cloneable接口,因而不能通过克隆的方式创建对象。对于通过反射创建对象的情况,由于反射最终会调用该类的构造函数,这里通过在构造函数中进行判断,避免了通过反射创建对象。因此获取对象的方式只有通过工厂方法,并且由于延迟初始化的方式只有在第一次初始化的时候才会有多线程的问题,而且锁对象是一件代价比较高昂的动作,因而这里通过判断,如果INSTANCE为空,说明是第一次加载,就将对象锁住,在锁里面再次判断,以防止两个线程都通过了外层的判断,在锁里面初始化之后,两个线程都将获得同一实例,并且后续再通过该工厂方法获取对象的时候,由于对象已经实例化,因而不会再产品锁住对象的动作,从而提高效率。这里在INSTANCE前使用volatile的目的是保证每个线程看到的实例是同一个实例。另外需要说明一点的是,如果使用急切初始化(如第一段代码)的方式实例化,则不需要使用同步,因为通过jvm可以保证类在加载的时候就已经实例化了一个对象。

       本文主要介绍了单例模式的创建条件和方式,通过分析对象的创建方式来逐步完善单例模式的创建过程,以保证该类的实例只有一个。

通过短时倒谱(Cepstrogram)计算进行时-倒频分析研究(Matlab代码实现)内容概要:本文主要介绍了一项关于短时倒谱(Cepstrogram)计算在时-倒频分析中的研究,并提供了相应的Matlab代码实现。通过短时倒谱分析方法,能够有效提取信号在时间与倒频率域的特征,适用于语音、机械振动、生物医学等领域的信号处理与故障诊断。文中阐述了倒谱分析的基本原理、短时倒谱的计算流程及其在实际工程中的应用价值,展示了如何利用Matlab进行时-倒频图的可视化与分析,帮助研究人员深入理解非平稳信号的周期性成分与谐波结构。; 适合人群:具备一定信号处理基础,熟悉Matlab编程,从事电子信息、机械工程、生物医学或通信等相关领域科研工作的研究生、工程师及科研人员。; 使用场景及目标:①掌握倒谱分析与短时倒谱的基本理论及其与傅里叶变换的关系;②学习如何用Matlab实现Cepstrogram并应用于实际信号的周期性特征提取与故障诊断;③为语音识别、机械设备状态监测、振动信号分析等研究提供技术支持与方法参考; 阅读建议:建议读者结合提供的Matlab代码进行实践操作,先理解倒谱的基本概念再逐步实现短时倒谱分析,注意参数设置如窗长、重叠率等对结果的影,同时可将该方法与其他时频分析方法(如STFT、小波变换)进行对比,以提升对信号特征的理解能力。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值