神的孩子都在歌唱

神的孩子都在歌唱

前言

一. 简介

单例模式(Singleton Pattern的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

二. 单例模式的原则

  • 单例模式限制类的实例化,并确保Java虚拟机中只存在该类的一个实例。
  • 单例类必须提供一个全局访问点来获取该类的实例。
  • 单例模式用于日志记录、驱动对象、缓存和线程池
  • 单例设计模式还用于其他设计模式,如抽象工厂等。
  • 单例设计模式也用在核心 Java 类中(例如,java.lang.Runtimejava.awt.Desktop)。

三. 单例模式的实现

1.1 饿汉式

1.1.1 静态变量初始化方式

在系统初始化时候,单例类的实例是在类加载时创建的。静态变量方式初始化的缺点是,即使客户端应用程序可能没有使用该方法,也会创建该方法。这是静态初始化单例类的实现:

/**
 * @author: 那就叫小智吧
 * @date: 2022/3/3 15:41
 * @Description:  静态变量创建类对象
 * 	该方式在成员位置声明Singleton类型的静态变量,并创建Singleton类的对象instance。instance对象是随着类的加载而创建的。如果该对象足够大的话,而一直没有使用就会造成内存的浪费。
 */
public class Singleton1 {
    // 私有构造方法
    private Singleton1(){
        System.out.println("通过静态变量创建类对象");
    }

    // 在成员位置创建该类的对象
    private static Singleton1 instance= new Singleton1();

    // 对外提供静态方法获取该对象
    public static Singleton1 getInstance(){
        return instance;
    }
}
1.1.2 静态代码块初始化方式

静态代码块方式实现与静态变量初始化方式类似,不同之处在于类的实例是在提供异常处理选项的静态块中创建的。

/**
 * @author: 那就叫小智吧
 * @date: 2022/3/3 15:49
 * @Description: 在静态代码块中创建该类对象
 * 该方式在成员位置声明Singleton类型的静态变量,而对象的创建是在静态代码块中,也是对着类的加载而创建。当然该方式也存在内存浪费问题。
 */
public class Singleton2 {

    // 私有构造方法
    private Singleton2(){
        System.out.println("在静态代码块中创建该类对象");
    };

    // 在成员位置声明静态变量
    private static Singleton2 instance;

    static {
        try {
            instance = new Singleton2();
        } catch (Exception e) {
            throw new RuntimeException("创建单例实例时发生异常");
        }
    }

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

}
1.1.3 枚举方式
/**
 * @author: 那就叫小智吧
 * @date: 2022/3/3 16:36
 * @Description: 恶汉式
 * 枚举类实现单例模式是极力推荐的单例实现模式,因为枚举类型是线程安全的,并且只会装载一次,设计者充分的利用了枚举的这个特性来实现单例模式,枚举的写法非常简单,而且枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式。
 */
public enum Singleton {
    INSTANCE;
}

1.2 懒汉式

1.2.1 懒加载初始化方法 (线程不安全)
/**
 * @author: 那就叫小智吧
 * @date: 2022/3/3 15:59
 * @Description: 懒汉式 :线程不安全
 * 从下面面代码我们可以看出该方式在成员位置声明Singleton类型的静态变量,并没有进行对象的赋值操作,
 * 那么什么时候赋值的呢?当调用getInstance()方法获取Singleton类的对象的时候才创建Singleton类的对象,这样就实现了懒加载的效果。但是,如果是多线程环境,会出现线程安全问题。
 */
public class Singleton1 {
    // 构造私有方法
    private Singleton1(){
        System.out.println("懒汉式:线程不安全");
    }

    // 在成员位置声明静态变量
    private static Singleton1 instance;

    // 对外提供静态方法获取改对象
    public static Singleton1 getInstance(){

        if (instance == null){
            instance = new Singleton1();
        }
        return  instance;

    }
}
1.2.2 懒加载初始化方法 (线程安全)

创建线程安全单例类的一种简单方法是使全局访问方法同步,以便一次只有一个线程可以执行该方法。以下是此方法的一般实现:

/**
 * @author: 那就叫小智吧
 * @date: 2022/3/3 16:04
 * @Description: 懒汉式 : 线程安全
 * 	该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从上面代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。
 */
public class Singleton2 {

    // 私有构造方法
    private Singleton2(){};

    // 在成员位置声明静态变量
    private static Singleton2 instance;

    // 对外提供静态方法获取对象
    public static synchronized Singleton2 getInstance(){

        if (instance != null) {
            instance = new Singleton2();
        }
        return  instance;

    }
}
1.2.3 双重检查锁
/**
 * @author: 那就叫小智吧
 * @date: 2022/3/3 16:08
 * @Description: 懒汉式 :双重检查方式
 * 对于 `getInstance()` 方法来说,绝大部分的操作都是读操作,读操作是线程安全的,所以我们没必让每个线程必须持有锁才能调用该方法,我们需要调整加锁的时机。由此也产生了一种新的实现模式:双重检查锁模式
 */
public class Singleton3 {

    // 私有构造方法
    private Singleton3() {
        System.out.println("懒汉式 : 双重检查方式");
    }

    // 在成员位置声明静态变量
    private static Singleton3 instance;

    // 对外提供静态方法获取该对象
    public static Singleton3 getInstance() {

        // 第一次判断,如果instance不为null,不进入枪锁阶段,直接返回实例
        if (instance == null) {
            synchronized (Singleton3.class) {
                // 抢到锁之后再次判断是否为null
                if (instance == null) {
                    instance = new Singleton3();
                }
            }
        }
        return instance;
    }
}

检查锁模式带来空指针异常的问题

/**
 * @author: 那就叫小智吧
 * @date: 2022/3/3 16:25
 * @Description: 懒汉式: 双重检查方式
 * 双重检查锁模式是一种非常好的单例实现模式,解决了单例、性能、线程安全问题,上面的双重检测锁模式看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。
 *
 * 要解决双重检查锁模式带来空指针异常的问题,只需要使用 `volatile` 关键字, `volatile` 关键字可以保证可见性和有序性。
 */
public class Singleton4 {

    // 私有构造方法
    private Singleton4() {}

    private static volatile Singleton4 instance;

    // 对外提供静态方法获取该对象
    public static Singleton4 getInstance() {

        //第一次判断,如果instance不为null,不进入抢锁阶段,直接返回实际
        if(instance == null) {
            synchronized (Singleton4.class) {
                //抢到锁之后再次判断是否为空
                if(instance == null) {
                    instance = new Singleton4();
                }
            }
        }
        return instance;

    }

}

添加 volatile 关键字之后的双重检查锁模式是一种比较好的单例实现模式,能够保证在多线程的情况下线程安全也不会有性能问题。

1.2.4 静态内部类方式
/**
 * @author: 那就叫小智吧
 * @date: 2022/3/3 16:29
 * @Description: 懒汉式 : 静态内部类方式
 * 静态内部类单例模式中实例由内部类创建,由于 JVM 在加载外部类的过程中, 是不会加载静态内部类的, 只有内部类的属性/方法被调用时才会被加载,并初始化其静态属性。静态属性由于被 `static` 修饰,保证只被实例化一次,并且严格保证实例化顺序。
 */
public class Singleton5 {

    // 私有构造方法
    private Singleton5() {}

    // 创建静态内部类
    private static class SingletonHolder {
        private static final Singleton5 instance = new Singleton5();
    }
    //对外提供静态方法获取该对象
    public static Singleton5 getInstance() {
        return SingletonHolder.instance;
    }

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

文章参考地址

12-25 02:20