01.单例模式的5中实现方式

为什么要用单例模式?

我们首先看两个案例:

案例一:加载配置文件

我们先看第一个例子,就是平时我们加载配置文件的常用代码,具体实现代码如下:

/**
 * 读取应用配置文件
 */
public class AppConfig {
    private String parameterA;
    private String parameterB;
    
    public AppConfig(){
        readConfig();
    }

    /**
     * 读取配置文件,把配置文件中的内容读出来设置到属性上
     */
    private void readConfig() {
        Properties p = new Properties();
        InputStream in = null;
        try{
            in = AppConfig.class.getResourceAsStream("appconfig.properties");
            p.load(in);
            this.parameterA = p.getProperty("paramA");
            this.parameterB = p.getProperty("paramB");
        }catch (Exception e){

        }finally {
            if(in !=null){
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

客户端:

public class Main {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();

        String paramA = appConfig.getParameterA();
        String paramB = appConfig.getParameterB();
    }
}

上面代码存在什么问题呢?

我们使用的时候是new 一个 AppConfig 来得到一个操作配置文件内容的对象。如果在系统运行中,有多个地方使用配置文件的
内容,那么就会new 出多个AppConfig 对象。

也就是说,系统中会同时存在多份配置文件的内容,这样会严重浪费内存资源,对于AppConfig 这种类,在运行期间,只需要
一个实例对象就够了。

案例二: 处理资源冲突问题

这个例子我们自定义实现了一个往文件中打印日志的Logger类。具体的代码实现如下:

public class Logger {
    private FileWriter fileWriter;
    public Logger(){
        File file = new File("log.txt");
        fileWriter = new FileWriter(file,true);
    }
    public void log(String msg){
        try {
            fileWriter.write(msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
public class OrderController {
    private Logger logger = new Logger();
    public void createOrder(String orderNum,String orderMsg){
        logger.log("创建订单信息,orderNum="+orderNum+";orderMsg="+orderMsg);
    }
}
public class UserController {
    private Logger logger = new Logger();

    public void addUser(String username,String password){
        logger.log("创建用户信息;username="+username+";password="+password);
    }
}

上面代码中,所有的日志都要写入到一个log.txt文件中,在UserController 和 OrderController对象中,分别new 了一个Logger对象,但是底层写的是同一个文件,由于Web容器都是线程池,如果访问UserController 和 OrderController对象的线程不同的线程,而且是同时访问,就有可能出现在日志信息中互相覆盖的问题。

那么为什么会出现覆盖呢?

因为在多线程环境下,多个线程会出现同时读取可写的文件位置,比如pos=200,然后再把信息写入文件中,此时其中的一个下次呢还给你根本不知道别的线程还在同一个位置写,就会出现覆盖的现象。

那么怎么解决呢? 我们想到的第一个方法应该是给log方法加锁。

    public synchronized  void log(String msg){
        try {
            fileWriter.write(msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

此时 synchronized 加的锁,是对象锁,只有是同一个对象在不同的线程中才会被顺序执行,不同的对象之间并不共享这把锁。也就是不起作用。

其实 FileWriter 的 write() 方法本身已经加锁,所以没必要再加synchronized锁。

    public void write(String str, int off, int len) throws IOException {
        synchronized (lock) {
            char cbuf[];
            if (len <= WRITE_BUFFER_SIZE) {
                if (writeBuffer == null) {
                    writeBuffer = new char[WRITE_BUFFER_SIZE];
                }
                cbuf = writeBuffer;
            } else {    // Don't permanently allocate very large buffers.
                cbuf = new char[len];
            }
            str.getChars(off, (off + len), cbuf, 0);
            write(cbuf, 0, len);
        }
    }

还有一种解决办法即使 加上类锁

    public synchronized  void log(String msg){
        try {
            synchronized (Logger.class){
                fileWriter.write(msg);    
            }
            fileWriter.write(msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

还有一种简单地方法就是 单例

public class Logger {
    private FileWriter fileWriter;
    private static final Logger LOGGER = new Logger();
    private  Logger(){
        File file = new File("log.txt");
        try {
            fileWriter = new FileWriter(file,true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static Logger getInstance(){
        return LOGGER;
    }
    public synchronized  void log(String msg){
        try {
            synchronized (Logger.class){
                fileWriter.write(msg);
            }
            fileWriter.write(msg);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

单例模式的定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点

一个类能够被创建多个实例,问题的根源在于类的构造方法是公开的,也就是可以让类的外部来通过
构造方法创建多个实例。换句话说,只要构造方法能让外部访问,就没有办法控制外部来创建这个类的
实例个数

如何实现一个单例

要实现单例,需要实现以下几点:

  • 构造函数是private 访问权限,这样才能避免外部通过new创建实例。
  • 考虑对象创建时的线程安全
  • 考虑是否支持延迟加载
  • 考虑getInstance()性能是否高(是否加锁)

1. 懒汉式

懒汉式的优势是支持延迟加载

public class Singleton {
    //定义一个变量来存储创建好的类实例
    //因为这个变量要在静态方法中使用,所以需要加上static修改
    private static Singleton instance;

    //私有化构造方法,在内部控制创建实例的数目
    private Singleton(){}

    /**
     * 定义一个方法来为客户端提供类实例
     * 这个方法需要定义成类方法,也就是需要加上static
     * @return
     */
    public synchronized  static  Singleton getInstance(){
        //判断存储实例的变量是否有值
        if(instance ==null){
            // 如果没有,就创建一个类实例,并把值给存储类实例的变量
            instance = new Singleton();
        }
        return instance;
    }
}

getInstance() 如果不加synchronized关键字,则会出现线程安全问题, 加上synchronized 则是对类加锁,并发度为1,如果频繁被用到,则会出现频繁的加锁和解锁以及并发度低的问题,会导致性能瓶颈,这种实现不可取。

2. 饿汉式

饿汉式即在类加载的时候就已经创建初始化好了,所以 是线程安全的。

public class Singleton {
    //定义一个变量来存储创建好的类实例
    //因为这个变量要在静态方法中使用,所以需要加上static修改
    private static final  Singleton instance  = new Singleton();
    //私有化构造方法,在内部控制创建实例的数目
    private Singleton(){}
    public   static  Singleton getInstance(){
        return instance;
    }
}
  • static 成员变量在类装载的时候进行初始化
  • 多个实例的static变量会共享同一块内存区域

网上很多人都说,这种实现方式不好,不支持延迟加载,如果实例占用资源多的话或者初始化耗时长,提前初始化会浪费资源,最好的办法就是用到的时候再去初始化。

但是我个人不支持这种观点,如果占用资源多,用到的时候也会出现占用多的,如果在启动的时候就能监测到占用资源多,甚至出现OOM,不就当场可以解决,如果在运行的时候出现OOM,那时候就完了。

还有就是初始化时间长,如果使用的时候再初始化,那将会出现某一次访问特别慢,还不如在启动的时候一起进行初始化。

如果初始化消耗资源过多,反而推荐懒汉式,早日发现问题

3.双重监测

双重检测,只要instance被创建之后,即使再次调用getInstance()函数,也不会到加锁的逻辑中,具体代码如下:

public class Singleton {
    private static  volatile Singleton instance  = null;
    private Singleton(){}
    public   static  Singleton getInstance(){
        if(instance==null){// 4.第一次检查
            synchronized (Singleton.class){ // 5.加锁
                if(instance ==null){// 6. 第二次检查
                    instance = new Singleton();// 7.问题的根源处在这里
                }
            }
        }
        return instance;
    }
}

网上有人说,这种实现方式,会出现指令排序,可能new出来的对象为null, 还没来得及初始化就被使用了

要解决这个问题,需要给成员变量 instance变量加上volatile关键字,禁止指令排序才行。

下面我们解释,没加 volatile关键字会出现的问题:

上面代码有一个问题:

在线程执行到第4行,代码读取到 instance 不为 null 时,instance引用的对象有可能还没有完全初始化。

问题的根源:

前面的 双重检查锁定上述代码的第7行(instance = new Singleton())创建了一个对象。这一行代码可以分解为 下面的3行代码。

memory = alloct();  //:1:分配对象的内存空间
ctorinstance(memory);  //2: 初始化对象
instace = memory;  //3:设置instace指向刚分配的内存地址

上面的3行伪代码中的2和3之间,可能被重排序,重排序如下:

memory = alloct();  //:1:分配对象的内存空间
instace = memory; //3:设置instace指向刚分配的内存地址
                  // 主要此对象还没有被初始化
ctorinstance(memory);  //2: 初始化对象

多线程执行结果如下:
在这里插入图片描述
实际上,只有很低版本的java 才会出现这种问题,高版本的Java已经在JDK内部解决了这个问题。解决办法很简单,只要把对象new操作和初始化操作设计为原子操作,就自然能禁止重排序了。

4. 静态内部类

要想很简单地实现线程安全,可以采用静态初始化器的方式,它可以由JVM来保证线程的安全性。

如果有一种方法能够让类装载的时候不去初始化对象,那不就解决问题了吗?
一种可行的是采用类级内部类,在这个类级内部类里面去创建对象实例。这样一来,只要不使用到这个类级内部类,就永远不会创建对象实例,从而同时实现延迟加载和线程安全。

public class Singleton {
    private Singleton(){}
    private static class SingletonHoler{
        private static final Singleton SINGLETON = new Singleton();
    }
    public   static  Singleton getInstance(){
        return SingletonHoler.SINGLETON;
    }
}

仔细想想,是不是很巧妙!
当getInstance 方法第一次被调用的时候,它第一次读取 SingletonHolder.instance,导致 SingletonHolder类得到初始化;而这个类在装载被初始化的时候,会初始化它的静态域,从而创建Singleton实例,由于是静态域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
所以,这种方式既保证了线程安全,又能做到延迟加载。

5. 枚举

  • java 的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法
  • java 枚举类型的基本思想是通过公有的静态final域为每个枚举常量导出实例的类
  • 从某个角度讲,枚举是单例的泛型化,本质上是单元素的枚举
public enum Singleton {
    uniqueInstance;
}

单例模式的命名

单例模式不适用于集群环境
一般建议单例模式的方法命名为getInstance(),这个方法的返回类型肯定是单例类的类型类。getInstance()方法可以有参数,这些参数可能是创建类实例锁需要的参数。

何时选用单例模式

建议在如下情况下,选用单例模式

当需要控制一个类的实例只能有一个,而且客户只能从一个全局点访问它时,可以选用单例模式,这些功能恰好是单例模式要解决的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

半夏_2021

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

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

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

打赏作者

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

抵扣说明:

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

余额充值