设计模式-单例模式

1、单例模式概述

单例模式(Singleton Pattern)是设计模式中最简单的形式之一。这一模式的目的是**使得类的一个对象成为系统中的唯一实例。**要实现这一点,可以从客户端对其进行实例化开始。因此需要用一种只允许生成对象类的唯一实例的机制,"阻止"所有想要生成对象的访问。使用工厂方法来限制实例化过程。这个方法应该是静态方法(类方法),因为让类的实例去生成另一个唯一实例毫无意义。

显然单例模式的要点有三个:

  • 某个类只能有一个实例;
  • 它必须自行创建这个实例;
  • 它必须自行向整个系统提供这个实例。

从具体实现角度来说,就是以下三点:

  • 单例模式的类只提供私有的构造函数;
  • 类定义中含有一个该类的静态私有对象;
  • 该类提供了一个静态的公有的函数用于创建或获取它本身的静态私有对象。

2、单例模式结构

单例模式结构如下:

(1)单例类:只能创建一个实例的类;

(2)访问类:使用单例类。

3、单例模式实现方式

单例模式提供了两种实现方式,如下所示:

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

3.1、饿汉式-静态变量方式

优缺点说明

**优点:**写法简单,在类加载的时候完成实例化,避免了线程同步的问题。

**缺点:**在类加载的时候就完成了实例化工作,没有达到延迟加载(Lazy Loading)的效果,如果一直没有使用这个实例,那么就造成了内存的浪费。

**总结:**这种方式基于 ClassLoader 机制,避免了多线程的同步问题,不过,instance 在类加载的时候就实例化,在单例模式中大多数都是调用 getInstance 方法,但是导致装载的原因有多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到延迟加载的效果。

**结论:**可用,但可能造成内存的浪费。

代码示例:

单例类:

javascript 复制代码
public class Singleton {

    //1、构造器私有化,防止 new 对象
    private Singleton() {}

    //2、声明Singleton类型的私有静态变量并赋值
    private static final Singleton instance = new Singleton();

    //3、通过public对外提供获取该类对象的方法
    public static Singleton getInstance() {
        return instance;
    }
}

客户端:

java 复制代码
 public class Client {
 ​
     public static void main(String[] args) {
         Singleton singleton1 = Singleton.getInstance();
         Singleton singleton2 = Singleton.getInstance();
         System.out.println("singleton1 = " + singleton1);
         System.out.println("singleton2 = " + singleton2);
         System.out.println(singleton1 == singleton2);
    }
 ​
 }

执行结果:

singleton1 = com.design.pattern.singleton.demo1.Singleton@1540e19d

singleton2 = com.design.pattern.singleton.demo1.Singleton@1540e19d

true

3.2、饿汉式-静态代码块方式

代码示例:

单例类:

java 复制代码
public class Singleton {
 ​
     //1、私有化构造方法,方式 new 对象
     private Singleton() {}
 ​
     //2、声明Singleton类型的私有静态变量
     private static Singleton instance;
 ​
     //3、在静态代码块中给静态对象赋值
     static {
         instance = new Singleton();
    }
 ​
     //4、通过public对外提供获取该类对象的方法
     public Singleton getInstance() {
         return instance;
    }
 }

代码分析:

和静态变量实例化相似,不同点在于将实例化的过程放在了静态代码块中执行,也是在类加载的时候,就执行了静态代码块中的代码。

可用,但可能造成内存的浪费。

3.3、懒汉式-线程不安全

代码示例:

java 复制代码
public class Singleton {
 ​
     //私有构造方法
     private Singleton() {}
 ​
     //声明Singleton类型的静态变量,此时不赋值,在调用方式时才赋值
     private static Singleton instance;
 ​
     //对外提供访问方法
     public static Singleton getInstance() {
         //判断instance是否为null,如果为null,说明还没有创建Singleton对象
         //如果没有,创建一个并返回,如果有,直接返回
         if (instance == null) {
             //线程1等待,线程2获取到CPU执行权,也会进入到判断中,进而导致线程不安全
             instance = new Singleton();
        }
         return instance;
    }
 }

3.4、懒汉式-线程安全

代码示例:

java 复制代码
public class Singleton {
 ​
     //私有构造方法
     private Singleton() {}
 ​
     //声明Singleton类型的静态变量,此时不赋值,在调用方式时才赋值
     private static Singleton instance;
 ​
     //对外提供访问方法,加锁后,线程只有执行完成释放锁后,另外一个线程才能进入
     public static synchronized Singleton getInstance() {
         //判断instance是否为null,如果为null,说明还没有创建Singleton对象
         //如果没有,创建一个并返回,如果有,直接返回
         if (instance == null) {
             instance = new Singleton();
        }
         return instance;
    }
 }

代码分析:

该方式也实现了懒加载的效果,同时又解决了线程安全问题,但是在 getInstance 方法上添加了 synchronized 关键字,导致该方法执行效率特别低。

从上面代码,我们可以看出,其实在初始化 instance 对象的时候才会出现线程安全问题,一旦初始化完成后就不存在了。

3.5、懒汉式-双重检查锁

对于单例模式中的 getInstance 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必要让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的懒汉式单例模式实现方式:双重检查锁模式。

代码示例:

java 复制代码
public class Singleton {
     //私有构造方法
     private Singleton() {}
 ​
     //声明Singleton类型的静态变量,此时不赋值,在调用方式时才赋值
     private static Singleton instance;
 ​
     //对外提供公共访问方法
     public Singleton getInstance() {
         //第一次判断,如果instance的值不为null,不需要抢占锁,直接返回对象
         if (instance == null) {
             synchronized (Singleton.class) {
                 //第二次判断,防止此时CPU被其他线程占用
                 if (instance == null) {
                     instance = new Singleton();
                }
            }
        }
         return instance;
    }
 }

代码分析:

双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检查锁模式看上去完美无缺,其实是存在问题的。

在多线程的情况下,可能会出现空指针问题,出现问题的原因是 JVM 在实例化对象的时候,会进行优化和指令重排序操作。

要解决双重检查锁模式带来的空指针异常的问题,只需要使用 volatile 关键字,volatile 关键字可以保证可见性和有序性。

修改代码如下,变更Singleton对象的声明方式:

java 复制代码
 //声明Singleton类型的静态变量,通过volatile修饰,保证变量的可见性和有序性
 //此时不赋值,在调用方式时才赋值
 private static volatile Singleton instance;

3.6、懒汉式-静态内部类方式

静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中,是不会加载静态内部类的,只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。

静态属性由于被 static 修饰,保证只能被实例化一次,并且严格保证实例化顺序,解决指令重排序问题。

代码示例:

java 复制代码
public class Singleton {
 ​
     //私有构造方法
     private Singleton() {}
 ​
     //定义一个静态私有内部类
     private static class SingletonHolder {
         //在内部类中声明并初始化外部类的对象
         private static final Singleton INSTANCE = new Singleton();
    }
 ​
     //提供公共的访问方法
     private static Singleton getInstance() {
         return SingletonHolder.INSTANCE;
    }
 }

代码分析:

第一次加载 Singleton 类时不会初始化 INSTANCE,只有第一次调用 getInstance 方法,虚拟机加载 SingletonHolder 并初始化 INSTANCE,这样不仅能确保线程安全,也能保证 Singleton 的唯一性。

总结:

静态内部类单例模式是一种优秀的单例模式,是开源项目中比较常用的一种单例模式。在没有加任何锁的情况下,保证了多线程下的安全,并且没有任何性能影响和空间的浪费。

3.7、饿汉式-枚举方式

代码示例:

java 复制代码
public enum Singleton {
     INSTANCE;
 }

总结:

**枚举类实现单例模式是极为推荐的单例实现模式,它基于JVM底层实现。因为枚举类型是线程安全的,并且只会装载一次,**设计者充分利用了枚举的这个特性来实现单例模式。

枚举的写法非常简单,而且枚举类型是所有单例实现模式中唯一一种不会被破坏的单例实现模式。

4、存在的问题

4.1、问题演示

破坏单例模式:

使上面创建的单例类可以创建多个对象,枚举方式除外。破坏方式有两种:分别是序列化和反射。

4.1.1、序列化和反序列化

代码示例:

单例类:

java 复制代码
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;
    }
 }

客户端:

java 复制代码
public class Client {
     public static void main(String[] args) {
         try {
             readObjectFromFile();
             readObjectFromFile();
        }catch (Exception e) {
             e.printStackTrace();
        }
    }
 ​
     private static void writeObjectToFile() throws Exception {
         //1、获取Singleton对象
         Singleton instance = Singleton.getInstance();
         //2、创建对象输出流对象
         ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/Users/chengjunyu/file1.txt"));
         //3、写对象
         outputStream.writeObject(instance);
         //4、释放资源
         outputStream.close();
    }
 ​
     private static void readObjectFromFile() throws Exception {
         //1、创建对象输入流对象
         ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("/Users/chengjunyu/file1.txt"));
         //2、读对象
         Singleton instance = (Singleton) inputStream.readObject();
         System.out.println(instance);
         //3、释放资源
         inputStream.close();
    }
 }

执行结果:

com.design.pattern.singleton.demo6.Singleton@4fca772d

com.design.pattern.singleton.demo6.Singleton@9807454

4.1.2、反射

反射破坏单例模式主要是通过设置构造器取消检查的方式来实现。

代码示例:

单例类:

java 复制代码
 public class Singleton {
 ​
     //私有构造方法
     private Singleton() {}
 ​
     //定义一个静态私有内部类
     private static class SingletonHolder {
         //在内部类中声明并初始化外部类的对象
         private static final Singleton INSTANCE = new Singleton();
    }
 ​
     //提供公共的访问方法
     public static Singleton getInstance() {
         return SingletonHolder.INSTANCE;
    }
 }

客户端:

java 复制代码
public class Client {
     public static void main(String[] args) throws Exception {
         //1、获取Singleton的字节码对象
         Class singletonClass = Singleton.class;
         //2、获取无参构造对象
         Constructor con = singletonClass.getDeclaredConstructor();
         //3、取消访问检查
         con.setAccessible(true);
         //4、创建单例对象
         Singleton instance1 = (Singleton) con.newInstance();
         Singleton instance2 = (Singleton) con.newInstance();
         //5、比较两个单例对象是否相等,结果为false,说明这两个对象不是同一个对象,已经破坏了单例模式
         System.out.println(instance1 == instance2);
    }
 }

4.2、问题解决方案

4.2.1、序列化和反序列化

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

代码实现:

单例类:

在单例类中增加 readResolve() 方法,通过这个方法,反序列化时直接返回该方法的返回值。代码如下:

java 复制代码
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;
    }
 ​
     //当进行反序列化时,会自动调用该方法,将该方法的返回值直接返回
     public Object readResolve() {
         return SingletonHolder.INSTANCE;
    }
 }

客户端:

java 复制代码
 public class Client {
     public static void main(String[] args) throws Exception {
         //writeObjectStream();
         readObjectStream();
         readObjectStream();
    }
 ​
     private static void writeObjectStream() throws Exception {
         //创建一个单例对象
         Singleton singleton = Singleton.getInstance();
         //创建输出流
         ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("/Users/chengjunyu/file2.txt"));
         //将对象写到输出流中
         outputStream.writeObject(singleton);
         //释放资源
         outputStream.close();
    }
 ​
     private static void readObjectStream() throws Exception {
         //创建输入流对象
         ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("/Users/chengjunyu/file2.txt"));
         //读取对象
         Singleton instance = (Singleton) inputStream.readObject();
         System.out.println(instance);
         //释放资源
         inputStream.close();
    }
 }

执行结果:

可以看到,此时执行两次方法后,获取到的对象是同一个,由此可见通过 readResolve() 方法解决了序列化和反序列化破坏单例模式的问题。

4.2.2、反射

单例类中判断当前是不是第一次创建单例对象,如果不是,抛出异常。代码如下:

单例类:

java 复制代码
 /**
  * @ClassName: Singleton
  * @Description: 单例模式-静态内部类
  * @Author chengjunyu
  */
 public class Singleton {
 ​
     private static boolean flag = false;
 ​
     //第一次获取对象是,flag为false,可以成功创建对象,再次创建对象时,flag为true,抛出异常
     private Singleton() {
         synchronized (Singleton.class) {
             if (flag) {
                 throw new RuntimeException("不能创建多个对象");
            }
             flag = true;
        }
    }
 ​
     //创建静态内部类
     private static class SingletonHolder {
         //在静态内部类中创建并初始化外部类对象
         private static final Singleton INSTANCE = new Singleton();
    }
 ​
     //提供公共的访问方法
     public Singleton getInstance() {
         return SingletonHolder.INSTANCE;
    }
 }

客户端:

java 复制代码
 public class Client {
     public static void main(String[] args) throws Exception {
         //1、获取Singleton的字节码对象
         Class cls = Singleton.class;
         //2、通过字节码对象获取该类的构造器
         Constructor cons = cls.getDeclaredConstructor();
         //3、取消访问检查
         cons.setAccessible(true);
         //4、创建单例对象
         Singleton instance1 = (Singleton) cons.newInstance();
         System.out.println("instance1:" + instance1);
         //5、再次创建单例对象
         Singleton instance2 = (Singleton) cons.newInstance();
         System.out.println("instance2:" + instance2);
    }
 }

执行结果:

instance1:com.design.pattern.singleton.demo10.Singleton@1540e19d

Exception in thread "main" java.lang.reflect.InvocationTargetException

at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)

at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)

at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)

at java.lang.reflect.Constructor.newInstance(Constructor.java:423)

at com.design.pattern.singleton.demo10.Client.main(Client.java:19)

Caused by: java.lang.RuntimeException: 不能创建多个对象

at com.design.pattern.singleton.demo10.Singleton.<init>(Singleton.java:16)

... 5 more

5、源码分析

Java 源码中,Runtime 类就是典型的使用了单例模式的类,部分源码如下。

java 复制代码
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() {}   
 }
相关推荐
2402_857589366 分钟前
SpringBoot框架:作业管理技术新解
java·spring boot·后端
HBryce249 分钟前
缓存-基础概念
java·缓存
一只爱打拳的程序猿24 分钟前
【Spring】更加简单的将对象存入Spring中并使用
java·后端·spring
杨荧25 分钟前
【JAVA毕业设计】基于Vue和SpringBoot的服装商城系统学科竞赛管理系统
java·开发语言·vue.js·spring boot·spring cloud·java-ee·kafka
minDuck27 分钟前
ruoyi-vue集成tianai-captcha验证码
java·前端·vue.js
为将者,自当识天晓地。1 小时前
c++多线程
java·开发语言
daqinzl1 小时前
java获取机器ip、mac
java·mac·ip
激流丶1 小时前
【Kafka 实战】如何解决Kafka Topic数量过多带来的性能问题?
java·大数据·kafka·topic
Themberfue1 小时前
Java多线程详解⑤(全程干货!!!)线程安全问题 || 锁 || synchronized
java·开发语言·线程·多线程·synchronized·