前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java 单例模式

Java 单例模式

作者头像
星姮十织
发布2022-01-02 01:16:13
5890
发布2022-01-02 01:16:13
举报
文章被收录于专栏:技术-汇集区

3. 单例模式

3.1 定义

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

3.2 问题场景

2.6.2 中,我们读取了配置文件中的内容。假设我们把读入的配置文件封装成一个类:

AppConfig.java:

代码语言:javascript
复制
package singleton;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * 读取配置文件
 */
public class AppConfig {
    /**
     * 用来存放配置文件中属性A的值
     */
    private String parameterA;
    /**
     * 用来存放配置文件中属性B的值
     */
    private String parameterB;

    /**
     * 取出属性A的值
     * @return 属性A的值
     */
    public String getParameterA(){
        return parameterA;
    }
    /**
     * 取出属性B的值
     * @return 属性B的值
     */
    public String getParameterB(){
        return parameterB;
    }

    /**
     * 构造方法
     */
    public AppConfig() {
        //调用读取配置文件的方法
        readConfig();
    }

    /**
     * 读取配置文件
     */
    public void readConfig() {
        Properties p = new Properties();
        InputStream in = null;
        try {
            in = ApiFactory.class.getResourceAsStream("AppConfig.properties");
            p.load(in);
            this.parameterA = p.getProperty("parameterA");
            this.parameterB = p.getProperty("parameterB");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

AppConfig.properties:

代码语言:javascript
复制
parameterA=A
parameterB=B

客户端(main 方法):

代码语言:javascript
复制
package singleton;

public class Client {
    public static void main(String[] args) {
        AppConfig config = new AppConfig();
        String parameterA = config.getParameterA();
        String parameterB = config.getParameterB();
        System.out.println("parameterA="+parameterA+" parameterB="+parameterB);
    }
}

输出结果:

代码语言:javascript
复制
parameterA=A parameterB=B

以上就是我们之前实现读取配置文件这个功能时的做法。那么,这样的做法有什么问题呢?

可以看出,客户端使用这个类时,是通过 new AppConfig() 获得一个 AppConfig 的实例来得到一个操作配置文件内容的对象。如果在系统运行中,有很多地方都需要使用配置文件的内容,那么就会在很多地方都创建 AppConfig 对象的实例。换句话说,在系统运行期间,系统中会存在很多个 AppConfig 的实例对象,这有什么问题吗?

答案当然是有问题的。试想一下,每一个 AppConfig 实例对象里面都封装着配置文件的内容。系统中有多个AppConfig 实例对象,也就是说系统中会同时存在多份配置文件的内容,这样会严重浪费内存资源。如果配置文件内容较少,问题可能不会严重;但如果配置文件内容本来就多的话,对于系统资源的浪费问题就会明显暴露出来。事实上,对于 AppConfig 这种类,在运行期间,只需要一个实例对象就足够了。

因此,把上面的描述进一步抽象一下,问题就描述出来了:在一个系统运行期间,某个类只需要一个类实例就可以了,这该怎样实现呢?

3.3 解决方案

仔细分析 3.2 中产生的问题,不难发现,这是由于构造方法公开化导致的。因为这样,外部类可以调用构造方法来创建任意个实例。换句话说,只要构造方法还是公开的,就没有办法控制外部类创建这个类的实例的个数。

要想控制一个类只被创建一个实例,那么首要的问题就是要把创建实例的权限收回来,让类自身来负责自己类实例的创建工作。然后由这个类来提供外部可以访问这个类实例的方法,这就是单例模式的实现方式。

在 Java 中,单例模式的实现又分为两种,一种称为懒汉式,一种称为饿汉式。其实就是在具体创建对象实例的处理上,有不同的实现方式。下面分别来看看这两种实现方式的代码示例:

3.3.1 懒汉式

LazySingleton.java:

代码语言:javascript
复制
package singleton;

/**
 * 懒汉式单例模式
 */
public class LazySingleton {
    /**
     * 定义一个变量来存储创建好的类实例
     */
    private static LazySingleton uniqueInstance = null;
    /**
     * 私有化构造方法
     */
    private LazySingleton(){}

    /**
     * 为外部类提供创建实例的方法
     * @return 存储的实例
     */
    public static synchronized LazySingleton getInstance(){
        //判断存储实例的变量是否有值
        if (uniqueInstance == null) {
            //如果没有,就创建一个,否则就不创建
            uniqueInstance = new LazySingleton();
        }
        return uniqueInstance;
    }

    /**
     * 用来测试输出的属性
     */
    private String text;
    /**
     * 赋值方法
     * @param text 留待输出的内容
     */
    public void setText(String text) {
        this.text = text;
    }
    /**
     * 用来测试输出的方法
     */
    public void test(){
        System.out.println("懒汉式:" + text);
    }
}

客户端(main 方法):

代码语言:javascript
复制
package singleton;

public class Client {
    public static void main(String[] args) {
        LazySingleton lazy1 = LazySingleton.getInstance();
        LazySingleton lazy2 = LazySingleton.getInstance();
        LazySingleton lazy3 = LazySingleton.getInstance();
        lazy1.setText("这是懒汉式方式的单例模式");
        lazy1.test();
        lazy2.setText("单利模式下无论调用多少次getInstance()方法");
        lazy2.test();
        lazy3.setText("都只会创建一个实例");
        lazy3.test();
        System.out.println("三个变量是否为同一个实例:" + (lazy1 == lazy2 && lazy2 ==lazy3 && lazy1 == lazy3));
        lazy1.test();
    }
}

输出结果:

代码语言:javascript
复制
懒汉式:这是懒汉式方式的单例模式
懒汉式:单利模式下无论调用多少次getInstance()方法
懒汉式:都只会创建一个实例
三个变量是否为同一个实例:true
懒汉式:都只会创建一个实例

在本例中,我们调用了三次 LazySingleton 的 getInstance 方法,保存到了三个变量中。显然,这三个变量指向的是同一个实例。因此,当我们对它们三个两两进行 == 的判断时,返回的结果为 true;也因此,当我们调用 lazy3 的方法 setText 改变其属性 text 之后,lazy1 和 lazy2 的属性也就跟着变了。

由此可以得出,使用懒汉式的具体方法步骤如下:

  1. 私有化构造方法
  2. 提供获取实例的方法
  3. 把获取实例的方法变为静态
  4. 定义存储实例的属性
  5. 把该属性同样变为静态
  6. 在获取实例的方法中控制实例的创建
3.3.2 饿汉式
代码语言:javascript
复制
package singleton;

/**
 * 饿汉式单例模式
 */
public class HungrySingleton {
    /**
     * 定义一个变量来存储创建好的类实例
     */
    private static HungrySingleton uniqueInstance = new HungrySingleton();
    /**
     * 私有化构造方法
     */
    private HungrySingleton(){}

    /**
     * 为外部类提供创建实例的方法
     * @return 存储的实例
     */
    public static HungrySingleton getInstance(){
        //直接返回存储实例的变量即可
        return uniqueInstance;
    }

    /**
     * 用来测试输出的属性
     */
    private String text;
    /**
     * 赋值方法
     * @param text 留待输出的内容
     */
    public void setText(String text) {
        this.text = text;
    }
    /**
     * 用来测试输出的方法
     */
    public void test(){
        System.out.println("懒汉式:" + text);
    }
}

客户端(main 方法):

代码语言:javascript
复制
package singleton;

public class Client {
    public static void main(String[] args) {
        HungrySingleton hungry1 = HungrySingleton.getInstance();
        HungrySingleton hungry2 = HungrySingleton.getInstance();
        HungrySingleton hungry3 = HungrySingleton.getInstance();
        hungry1.setText("这是饿汉式的单例模式");
        hungry1.test();
        hungry2.setText("与懒汉式单例模式的差别不大");
        hungry2.test();
        hungry3.setText("唯一的区别在于存储实例的变量何时初始化");
        hungry3.test();
        System.out.println("三个变量是否为同一个实例:" + (hungry1 == hungry2 && hungry2 ==hungry3 && hungry1 == hungry3));
        hungry1.test();
    }
}

输出结果:

代码语言:javascript
复制
饿汉式:这是饿汉式的单例模式
饿汉式:与懒汉式单例模式的差别不大
饿汉式:唯一的区别在于存储实例的变量何时初始化
三个变量是否为同一个实例:true
饿汉式:唯一的区别在于存储实例的变量何时初始化

饿汉式与懒汉式的特性没有任何不同,它们之间唯一的区别在于创建实例的时机。懒汉式是在调用 getInstance 方法(推荐总是这么命名)的时候才去创建,而饿汉式则是在类加载的时候就一并创建了。

由此可以得出,使用饿汉式的具体方法步骤如下:

  1. 私有化构造方法
  2. 提供获取实例的方法
  3. 把获取实例的方法变为静态
  4. 定义存储实例的静态属性并直接创建实例
  5. 在获取实例的方法中直接返回存储的实例
3.3.3 重写示例

既然单例模式的饿汉式与懒汉式并没有特性上的区别,因此我们就只用一种方式去改写就好了,这里选择懒汉式:

AppConfig.java:

代码语言:javascript
复制
package singleton;

import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;

/**
 * 读取配置文件
 */
public class AppConfig {
    /**
     * 用来存放创建的实例
     */
    private static AppConfig appConfig = null;
    /**
     * 用来存放配置文件中属性A的值
     */
    private String parameterA;
    /**
     * 用来存放配置文件中属性B的值
     */
    private String parameterB;

    /**
     * 取出属性A的值
     * @return 属性A的值
     */
    public String getParameterA(){
        return parameterA;
    }
    /**
     * 取出属性B的值
     * @return 属性B的值
     */
    public String getParameterB(){
        return parameterB;
    }

    /**
     * 私有化构造方法
     */
    private AppConfig() {
        //调用读取配置文件的方法
        readConfig();
    }

    /**
     * 懒汉式获取实例的方法
     * @return 存储的实例
     */
    public static synchronized AppConfig getInstance() {
        if (appConfig==null) {
            appConfig = new AppConfig();
        }
        return appConfig;
    }

    /**
     * 读取配置文件
     */
    public void readConfig() {
        Properties p = new Properties();
        InputStream in = null;
        try {
            in = AppConfig.class.getResourceAsStream("AppConfig.properties");
            p.load(in);
            this.parameterA = p.getProperty("parameterA");
            this.parameterB = p.getProperty("parameterB");
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                in.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

客户端(main 方法):

代码语言:javascript
复制
package singleton;

public class Client {
    public static void main(String[] args) {
        AppConfig config = AppConfig.getInstance();
        String parameterA = config.getParameterA();
        String parameterB = config.getParameterB();
        System.out.println("parameterA="+parameterA+" parameterB="+parameterB);
    }
}

输出结果:

代码语言:javascript
复制
parameterA=A parameterB=B

3.4 优缺点

  • 时间和空间 懒汉式:时间换空间 饿汉式:空间换时间
  • 线程安全 懒汉式:不加同步则线程不安全 饿汉式:线程安全

3.5 更好的实现方式

懒汉式的优势在于实现了延迟加载,而饿汉式的优势在于线程安全。懒汉式虽然通过添加 synchronized 的方式也能实现线程安全,但是这样会大幅度地降低访问速度。那么,有没有一种方法,既能实现延迟加载,又能在不降低访问速度的情况下实现线程安全呢?

事实上,饿汉式已经做到了在不降低访问速度的情况下实现线程安全。只是,它没有实现延迟加载,因而会在类装载的时候就初始化对象,不论是否需要,这回造成空间的浪费。

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

InnerSingleton.java:

代码语言:javascript
复制
package singleton;

/**
 * 类级内部类实现单例模式
 */
public class InnerSingleton {
    /**
     * 类级内部类
     */
    private static class SingletonHolder {
        /**
         * 静态初始化器
         */
        private static InnerSingleton instance = new InnerSingleton();
    }
    /**
     * 私有化构造方法
     */
    private InnerSingleton(){}
    /**
     * 获取实例
     */
    public static InnerSingleton getInstance() {
        return SingletonHolder.instance;
    }
    /**
     * 测试用的属性
     */
    private String text;

    /**
     * 设置属性
     * @param text 带输出的文本
     */
    public void setText(String text) {
        this.text = text;
    }
    /**
     * 测试用的方法
     */
    public void test(){
        System.out.println("类级内部类:"+text);
    }
}

客户端(main 方法):

代码语言:javascript
复制
package singleton;

public class Client {
    public static void main(String[] args) {
        InnerSingleton inner1 = InnerSingleton.getInstance();
        InnerSingleton inner2 = InnerSingleton.getInstance();
        inner1.setText("可以同时实现延迟加载和线程安全");
        inner1.test();
        inner2.setText("比懒汉式和饿汉式更好的单例实现方式");
        inner2.test();
        inner1.test();
    }
}

输出结果:

代码语言:javascript
复制
类级内部类:可以同时实现延迟加载和线程安全
类级内部类:比懒汉式和饿汉式更好的单例实现方式
类级内部类:比懒汉式和饿汉式更好的单例实现方式

getInstance 方法第一次被调用的时候,它第一次读取 SingletonHolder.instance,导致 SingletonHolder 类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建 Singleton 的实例。由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

这个模式的优势在于,getInstance 方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

3.6 最佳实践:枚举

使用枚举来实现单实例控制会更加简洁,而且无偿地提供了序列化的机制,并由 JVM 从根本上提供保障,绝对防止包括反射方式在内的多次实例化,是更简洁、高效、安全的实现单例的方式。示例代码如下:

代码语言:javascript
复制
package singleton;

/**
 * 枚举
 */
public enum SingletonEnum {
    /**
     * 定义一个枚举的元素,它就代表 SingletonEnum 的一个实例
     */
    uniqueInstance;
    /**
     * 测试属性
     */
    private String text;
    /**
     * 设置属性
     */
    public void setText(String text) {
        this.text = text;
    }
    /**
     * 测试方法
     */
    public void test(){
        System.out.println("通过枚举实现单例:"+text);
    }
}

客户端(main 方法):

代码语言:javascript
复制
package singleton;

public class Client {
    public static void main(String[] args) {
        SingletonEnum singletonEnum1 = SingletonEnum.uniqueInstance;
        SingletonEnum singletonEnum2 = SingletonEnum.uniqueInstance;
        singletonEnum1.setText("更简洁、高效、安全");
        singletonEnum1.test();
        singletonEnum2.setText("是最佳实践");
        singletonEnum2.test();
        singletonEnum1.test();
    }
}

输出结果:

代码语言:javascript
复制
通过枚举实现单例:更简洁、高效、安全
通过枚举实现单例:是最佳实践
通过枚举实现单例:是最佳实践

本文系转载,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文系转载前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 3. 单例模式
    • 3.1 定义
      • 3.2 问题场景
        • 3.3 解决方案
          • 3.3.1 懒汉式
          • 3.3.2 饿汉式
          • 3.3.3 重写示例
        • 3.4 优缺点
          • 3.5 更好的实现方式
            • 3.6 最佳实践:枚举
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档