Java 设计模式——单例模式

目录

  • 1.概述
  • 2.实现
    • 2.1.饿汉式
      • 2.1.1.静态变量
      • 2.1.2.静态代码块
      • 2.1.3.枚举方式
    • 2.2.懒汉式
      • 2.2.1.synchronized 线程安全
      • 2.2.2.双重检查锁
      • 2.2.3.静态内部类方式
  • 3.破坏单例模式
    • 3.1.序列化反序列化
    • 3.2.反射
  • 4.问题解决
  • 5.优缺点
  • 6.应用场景
  • 7.JDK 源码解析——Runtime 类

1.概述

(1)单例模式 (Singleton Pattern) 是 Java 中最简单的设计模式之一。它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

(2)单例模式的目的是限制一个类的实例数量,并提供全局访问点以方便其他组件使用。它通常在需要共享资源、管理全局状态或限制某个组件数量的情况下使用。然而,单例模式也有一些缺点,例如增加了代码的耦合性和可扩展性的限制,因此在使用时需要权衡其利弊。

(3)单例模式的主要有单例类和访问类这两个角色:

单例类只能创建一个实例的类
访问类使用单例类

2.实现

单例模式分类两种:饿汉式和懒汉式。

饿汉式类加载就会导致该单实例对象被创建
懒汉式类加载不会导致该单实例对象被创建,而是首次使用该对象时才会创建

2.1.饿汉式

2.1.1.静态变量

package com.itheima.patterns.singleton.hungryman1;

//饿汉式
public class Singleton{
    
    //私有构造方法
    private Singleton(){}
    
    //静态变量创建类的对象
    private static Singleton instance = new Singleton();

    //对外提供静态方法获取该对象
    public static Singleton getInstance(){
        return instance;
    }
}

说明: 方式一在成员位置声明 Singleton 类型的静态变量,并创建 Singleton 类的对象 instance。instance 对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。

2.1.2.静态代码块

package com.itheima.patterns.singleton.hungryman2;

public class Singleton{
    
    //私有构造方法
    private Singleton(){}
    
    //在静态代码块中进行创建
    private static Singleton instance;
    static {
        instance = new Singleton();
    }
    
    //对外提供静态方法获取该对象
    public static Singleton getInstance(){
        return instance;
    }
}

说明:方式二在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是随着类的加载而创建。所以和方式一基本一样,也存在内存浪费问题。

2.1.3.枚举方式

enum Singleton {
    //INSTANCE;

    INSTANCE("Tom", 21);

    // 添加其他成员变量
    private String name;
    private int age;

    Singleton(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    // 添加其他方法
    public void sayHello() {
        System.out.println("Hello, I'm " + name + ", " + age + " years old.");
    }
}

说明:枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。测试代码如下:
Client.java

public class Client {
    public static void main(String[] args) {
        Singleton instance1 = Singleton.INSTANCE;
        Singleton instance2 = Singleton.INSTANCE;
        System.out.println("instance1 == instance2 的结果为: " + (instance1 == instance2));   // true

        System.out.println(instance1.getAge());
        instance1.setAge(50);
        System.out.println(instance2.getAge());
    }
}

输出结果如下:

instance1 == instance2 的结果为: true
21
50

2.2.懒汉式

2.2.1.synchronized 线程安全

package com.itheima.patterns.singleton.Lazyman;

public class Singleton{
    
    //私有构造方法
    private Singleton(){}
    
    //声明 Singleton 类型的变量 instance,并未进行赋值
    private static Singleton instance;
    
    //使用关键字 synchronized 的目的在于保证线程安全
    public static synchronized Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
}

说明:该方式实现了懒加载效果,同时又解决了线程安全问题。但是在 getInstance() 方法上添加了 synchronized 关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化 instance 的时候才会出现线程安全问题,一旦初始化完成就不存在了。

2.2.2.双重检查锁

public class Singleton {
    
    //私有构造方法
    private Singleton(){}
    
    //声明 Singleton 类型的变量 instance,并未进行赋值
    private static volatile Singleton instance;
    
    public static Singleton getInstance(){
        //第一次检查,若 instance 不为 null,则不进入抢锁阶段,直接返回实际值即可
        if (instance == null) {
            synchronized(Singleton.class){
                //第二次检查,得到锁之后再次判断 instance 是否为空
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

(1)双重检验锁 (Double-Checked Locking) 是一种在多线程环境下实现单例模式的优化方式。其主要原理如下:

  • 首先检查实例是否已经被创建,如果已经创建,则直接返回已经创建的实例,不再进入同步代码块,提高性能。
  • 如果实例尚未创建,则进入同步代码块,在同步代码块内再次检查实例是否已经被创建。
  • 在同步代码块内部进行第二次检查时,由于进入同步块的只有一个线程,其他线程处于等待状态,避免了多个线程同时进入同步代码块创建实例。
  • 在第二次检查时,如果实例尚未创建,则创建实例,并将实例赋值给成员变量,确保只有一个实例被创建。
  • 最后,释放锁,并返回实例。

(2)这种方式结合了懒加载线程安全的特点,在第一次调用时才创建实例,在多线程环境下确保只有一个实例被创建。双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题。

(3)在双重检查锁的实现方式中,如果没有使用 volatile 关键字,可能会出现指令重排序的问题。具体来说,当一个线程进入第一个 if 判断时,如果实例还未被创建,那么它会获取锁并创建实例。但是由于指令重排序的影响,实例的初始化可能会在获取锁之前被重排序到锁的后面,这就导致其他线程在第二个 if 判断中认为实例已经创建,从而返回一个未完全初始化的实例

(4)通过使用 volatile 关键字修饰 instance 变量,可以保证在多线程环境下对 instance 的读取和写入操作都是有序的,避免了指令重排序问题,并且保证了其他线程能够正确地看到已经完全初始化的实例。

相关知识点:
Java 并发编程面试题——synchronized 与 volatile

2.2.3.静态内部类方式

package com.itheima.patterns.singleton.lazyman3;

public class Singleton {
    
    //私有构造方法
    private Singleton(){}
    
    //静态内部类
    private static class SingletonHolder{
        private static final Singleton INSTANCE = new Singleton();
    }
    
    //对外提供静态方法获取该对象
    public static Singleton getInstance(){
        return SingletonHolder.INSTANCE;
    }
}

(1)第一次加载 Singleton 类时不会去初始化 INSTANCE,只有第一次调用 getInstance() 时,虚拟机才加载 SingletonHolder,并初始化 INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 类的唯一性。总之,静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费

(2)静态内部类实现的单例可以做到线程安全且可延迟加载的原因如下:

  • 线程安全:静态内部类的加载是在第一次使用时进行的,并且由 Java 虚拟机来保证类加载的线程安全性。在类加载过程中,虚拟机会对类进行初始化,并且只会进行一次,这样就避免了多线程环境下的竞争条件。因此,通过静态内部类实现的单例可以在多线程环境下安全地被多个线程共享。
  • 可延迟加载:静态内部类的初始化是在第一次使用时进行的,即在调用 getInstance 方法时才会加载内部类。这样可以延迟单例对象的初始化过程,只有在需要使用到单例对象时才进行初始化。这对于资源消耗较大的对象或需求延迟加载的场景特别有用,可以节省内存空间和系统资源,并提高应用的启动性能。

3.破坏单例模式

破坏单例模式演示(序列化反序列化和反射)

3.1.序列化反序列化

package com.itheima.patterns.singleton.problem1;

import java.io.Serializable;

public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}
package com.itheima.patterns.singleton.problem1;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;


public class Client {
    public static void main(String[] args) throws Exception {
        writeObject2File();
        readObjectFromFile();
        readObjectFromFile();
    }
    
    //从文件读取数据(对象)
    public static void readObjectFromFile() throws Exception {
        //1.创建对象输入流对象
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\testData\\a.txt"));
        //2.读取对象
        Singleton instance = (Singleton) ois.readObject();        
        System.out.println(instance);        
        //释放资源
        ois.close();
    }
    
    //向文件中写数据(对象)
    public static void writeObject2File() throws Exception {
        //1.获取Singleton对象
        Singleton instance = Singleton.getInstance();
        //2.创建对象输出流对象
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\testData\\a.txt"));
        //3.写对象
        oos.writeObject(instance);
        //4.释放资源
        oos.close();
    }
}

3.2.反射

public class Singleton {
	//私有构造方法
	private Singleton() {}
	private static volatile Singleton instance;
	//对外提供静态方法获取该对象
	public static Singleton getInstance() {
		if(instance != null) {
			return instance;
		}
		synchronized (Singleton.class) {
			if(instance != null) {
				return instance;
			}
			instance = new Singleton();
			return instance;
		}
	}
}
import java.lang.reflect.Constructor;

public class Client {
    public static void main(String[] args) throws Exception {
        //获取 Singleton 类的字节码对象
        Class clazz = Singleton.class;
        //获取 Singleton 类的私有无参构造方法对象
        Constructor constructor = clazz.getDeclaredConstructor();
        //取消访问检查
        constructor.setAccessible(true);
        //创建 Singleton 类的对象 s1
        Singleton s1 = (Singleton) constructor.newInstance();
        //创建 Singleton 类的对象 s2
        Singleton s2 = (Singleton) constructor.newInstance();
        //判断通过反射创建的两个 Singleton 对象是否是同一个对象
        System.out.println(s1 == s2);  // false
    }
}

4.问题解决

(1)序列化、反序列化方式破坏单例模式的解决方法
在 Singleton 类中添加 readResolve() 方法,在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的值,如果没有定义,则返回新 new 出来的对象。

import java.io.Serializable;

public class Singleton implements Serializable {
    //私有构造方法
    private Singleton() {}
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    
    //解决序列化反序列化破解单例模式
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

具体的深入分析可以参考单例、序列化和 readResolve() 方法。

(2)反射方式破解单例的解决方法
当通过反射方式调用构造方法创建对象时,直接抛异常,不运行此种操作。

import java.io.Serializable;

public class Singleton implements Serializable {
    
    private static boolean flag = false;
    
    //私有构造方法
    private Singleton() {
        synchronized (Singleton.class){
            //若 flag 的值为 true,说明不是第一次访问,直接抛一个异常
            if (flag){
                throw new RuntimeException("不能创建多个对象!");
            }
            flag = true;
        }
    }
    
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    
    //对外提供静态方法获取该对象
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
    
    //解决序列化反序列化破解单例模式
    private Object readResolve() {
        return SingletonHolder.INSTANCE;
    }
}

但是上面的操作也不一定安全,因为可以通过反射的方式来修改 flag,最安全的方式应该是使用枚举方式来创建单例对象,其原因在于 JDK 底层在通过反射创建对象时,会检查对象类型是否为枚举类型,如果是,则会抛出 IllegalArgumentException 异常,从而创建对象失败,这样做的目的在于保证枚举对象的单例性。具体相关源码如下所示:

@CallerSensitive
public final class Constructor<T> extends Executable {

	//...
	
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        //检查给定的 clazz 对象的修饰符中是否包含枚举类型的标志位,以确定其是否表示一个枚举类型
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }
}

5.优缺点

(1)单例模式是一种只允许创建一个实例的设计模式。它的主要优点是:

  • 独一无二的实例:单例模式确保在整个应用程序中只有一个实例存在。这在某些情况下是非常有用的,例如需要共享资源或限制系统中某个组件的数量。
  • 全局访问点:由于单例实例是全局可访问的,所以可以方便地从任何地方访问它。这对于需要频繁访问单例实例的组件非常有用,避免了传递实例的麻烦。

(2)然而,单例模式也有一些缺点:

  • 耦合性:使用单例模式可能导致代码中的紧耦合,因为它引入了全局状态。这会使代码在可测试性和可维护性方面变得更加困难,因为任何依赖单例实例的组件都与该实例紧密耦合。
  • 难以扩展:由于单例模式只允许存在一个实例,因此在需要扩展功能时可能会遇到限制。如果需要创建多个实例来满足新的需求,则需要修改单例类的实现。
  • 隐藏依赖关系:使用单例模式可能会隐藏代码中的依赖关系。由于可以从任何地方访问单例实例,组件之间的依赖关系可能不明显,导致代码更难以理解和维护。

6.应用场景

(1)单例模式可以应用于许多场景,其中一些常见的应用场景包括:

  • 日志记录器:在应用程序中,通常只需要一个日志记录器来记录系统的日志信息。使用单例模式可以确保只有一个日志记录器实例存在,并且可以从任何地方方便地访问它。
  • 数据库连接池:在需要频繁访问数据库的应用程序中,可以使用单例模式来管理数据库连接池。通过保持只有一个数据库连接池实例存在,可以避免创建过多的数据库连接,提高性能和资源利用率。
  • 配置信息管理器:在应用程序中,通常需要加载和访问配置信息。使用单例模式可以创建一个全局的配置信息管理器,用于加载和提供应用程序所需的配置信息,避免重复加载和管理多个实例。
  • 线程池:在需要管理线程执行的应用程序中,可以使用单例模式来创建并管理线程池。通过保持只有一个线程池实例存在,可以方便地分配和管理线程,提高并发性能。
  • GUI 应用程序中的窗口管理器:在 GUI 应用程序中,通常需要管理窗口的创建、显示和销毁。使用单例模式可以创建一个全局的窗口管理器,用于管理应用程序中的所有窗口。

(2)这只是一些常见的应用场景,实际上,任何需要全局访问点和只允许存在一个实例的情况都可以考虑使用单例模式。但请注意,在使用单例模式时需要慎重考虑其优缺点,并确保它满足设计需求。

7.JDK 源码解析——Runtime 类

(1)Runtime 类就是使用的单例设计模式,其部分源代码如下:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    /**
     * Returns the runtime object associated with the current Java application.
     * Most of the methods of class <code>Runtime</code> are instance
     * methods and must be invoked with respect to the current runtime object.
     *
     * @return  the <code>Runtime</code> object associated with the current
     *          Java application.
     */
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    /** Don't let anyone else instantiate this class */
    private Runtime() {}
	...
}

从上面源代码中可以看出 Runtime 类使用的是饿汉式(静态属性)方式来实现单例模式的。

(2)使用 Runtime 类

import java.io.IOException;
import java.io.InputStream;

public class RunTimeDemo {
    
    public static void main(String[] args) throws IOException {
        //获取RunTime类对象
        Runtime runtime = Runtime.getRuntime();
    
        System.out.println("JVM 空闲内存 =" + runtime.freeMemory() / (1024*1024) + "M");
        System.out.println("JVM 总内存 =" + runtime.totalMemory() / (1024*1024) + "M");
        System.out.println("JVM 可用最大内存 =" + runtime.maxMemory() / (1024*1024) + "M");
        
        //调用 runtime 的方法 exec,参数为一个命令
        Process process = runtime.exec("ipconfig");
        //调用 process 对象的获取输入流的方法
        InputStream is = process.getInputStream();
        byte[] arr = new byte[1024 * 1024 * 100];
        int length = is.read(arr);
        System.out.println(new String(arr,0,length, "GBK"));
    }
}

文章出处登录后可见!

已经登录?立即刷新

共计人评分,平均

到目前为止还没有投票!成为第一位评论此文章。

(0)
社会演员多的头像社会演员多普通用户
上一篇 2023年12月15日
下一篇 2023年12月15日

相关推荐