单例模式的几种方式整理

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要重复实例化该类的对象。保证一个类仅有一个实例,并提供一个访问它的全局访问点。
优点是在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。避免对资源的多重占用(比如写文件操作)。

单例模式的实现有多种方式例如饿汉模式、懒汉模式、双重检查模式、静态内部类方式以及枚举类。其中饿汉模式、懒汉模式又有多种变形,这里只介绍其基本的方式。

1.饿汉模式

public class Singleton {
    // 饿汉模式
    public static Singleton getInstance() {
        return instance;
    }
    private static Singleton instance = new Singleton();

    private Singleton() {
    }

}

特点:在类加载的时候就完成了实例化,避免了线程同步问题,因此线程安全。由于是在类加载时就完成了实例化,没有达到懒加载的效果。如果一直没有使用过这个实例,就造成了内存的浪费!内存资源不是特别匮乏的情况下都可以使用这种模式。简单、实用。

2.懒汉模式

public class Singleton2 {
    // 懒汉模式
    private static Singleton2 instance = null;
    public static Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
    private Singleton2() {
    }
}

特点:实现了对象的延迟加载,在一定程度节省了资源。在单线程情况下可以使用,多线程情况下不安全。原因在于 instance = new Singleton2() 这个行代码的执行可能会发生重排序,具体原因在下面双检查模式中解释。

3.双重检查

public class Singleton3 {
    // 双重检查
    private static Singleton3 instance = null;
    public static Singleton3 getInstance() {
        if (instance == null) {                   // 1
            synchronized (Singleton3.class) {  
                if (instance == null) {          // 2
                    instance = new Singleton3(); // 3
                }
            }
        }
        return instance;
    }
    private Singleton3() {
    }
}

分析:synchronized 会导致性能开销,因此尽量在必要的位置使用,将同步块的位置范围限制的尽可能小。双检查模式在代码 1 处第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作,降低了synchronized 带来的性能开销。
但上面的代码是经典的错误方式,原因在代码 3 处, instance = new Singleton3(); 这行代码大致可以分成三个步骤:

  • 1.分配对象所需要的内存
  • 2.初始化对象
  • 3.设置instance 指向刚才分配的内存地址

单线程情况下我们可以认为jvm是按照这样的顺序执行的,就算编译器优化代码被重排序,把步骤2和步骤3的顺序调换,也不影响最后结果,这种重排序是被允许的。

在多线程情况下,如果线程A执行到instance = new Singleton3()并且步骤2和步骤3发生了重排,instance 指向了刚才分配的内存地址,但还没完成初始化。这时线程B执行到代码1处,判断instance不为null,访问到了一个未被初始化的对象,得到不可预知的结果。

解决这个问题有两个思路:

1.禁止代码重排,使用volatile关键字修饰变量,禁止步骤2和步骤3的重排序。代码如下:

 private static volatile Singleton3 instance = null;

了解更多关于volatile关键字可以参考:深入理解volatile关键字

2.允许步骤2和步骤3重排序,但不允许其他线程观察到这个重排序,即下面介绍的静态内部类方式。

4.静态内部类方式

public class Singleton4 {
    private static class InstanceHolder {
        private static Singleton4 instance = new Singleton4();
    }
    private Singleton4() {
    }
    public static Singleton4 getInstance() {
        return InstanceHolder.instance;
    }
}

分析:
Singleton4 getInstance() 被调用时会触发InstanceHolder 类的初始化,在初始化阶段执行类构造器clinit方法,clinit是class类构造器对静态变量,静态代码块进行初始化。

Java虚拟机必须保证一个类的clinit方法在多线程环境中被正确地加锁同步,如果多个线程同时去初始化一个类,那么只会有其中一个线程去执行这个类的clinit方法,其他线程都需要阻塞等待,直到活动线程执行完毕clinit方法,其他线程虽然会被阻塞,但如果执行clinit方法的那条线程退出clinit方法后,其他线程唤醒后则不会再次进入clinit方法。同一个类加载器下,一个类型只会被初始化一次。

在多线程情况下,A线程在调用getInstance()方法就算发生了:

  • 2.初始化对象
  • 3.设置instance 指向刚才分配的内存地址

的代码重排序也不影响线程安全,因为B线程看不到这个变化,可以这样理解:对线程B来说,InstanceHolder静态内部类实例化Singleton4对象的过程是一个原子操作。

相对于volatile双检查方式,静态内部类方式的代码更简洁。

5. 枚举类方式

代码:

public enum Singleton5 {
    INSTANCE;
    public void methodA() {
        System.out.println("枚举类方式!");
    };

分析:
枚举类本身很符合单例模式的特点,它的构造方法是私有的。
枚举类由JVM在加载的时候,实例化枚举对象,在枚举类中定义了多少个就会实例化多少个,JVM为了保证每一个枚举类元素的唯一实例,是不会允许外部进行new的,所以会把构造函数设计成private,防止用户生成实例,破坏唯一性。
代码中INSTANCE就是一个枚举类的实例。

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,认为是实现单例模式的最佳方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,防止多次实例化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值