五种单例模式

本文介绍了Java单例模式的几种常见实现方式,包括饿汉式和懒汉式的不足,以及推荐的双重检查锁(DCL)模式、静态内部类和枚举单例的优点。DCL通过细粒度锁提高效率,静态内部类利用JVM初始化保证线程安全,而枚举单例利用Java内置特性简化并发控制。
摘要由CSDN通过智能技术生成

饿汉式(不推荐)

顾名思义,有饥饿感,提前就加载了,饿汉式在类被加载的时候就创建好了实例,此时有可能实例还没有用到。两种写法:

public class Singleton {
    private static Singleton instance = new Singleton();  // 直接初始化一个实例对象

    // private类型的构造函数,确保其它类对象不能new一个本类的实例
    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}
public class Singleton {
    private static Singleton instance;
    
    static {
        instance = new Singleton();
    }

    // private类型的构造函数,确保其它类对象不能new一个本类的实
    private Singleton() {
    }

    public static Singleton getInstance() {
        return instance;
    }
}

懒汉式(不推荐)

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉式只有在实际需要的时候才会创建实例,实现起来也很简单,由一个静态方法返回实例,同时对这个静态方法加了一把锁,所有来获取实例的方法都会去竞争这把锁,不管实例有没有创建。不加锁线程不安全。

但这也带来了一个问题,就是锁的粒度太粗。实际上,只有首次创建实例时,为了防止多线程同时创建多个实例才需要锁,当实例已经创建完成后,直接返回实例就好,不再需要什么锁了。但在当前示例里,不管实例有没有创建,只要想获取实例,就要去竞争锁,显然是扩大了锁的竞争范围,效率肯定会降低。所以引入双重锁检查模式

双重锁检测模式(Double Check Lock)(推荐)

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if(instance == null) {
            synchronized (Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}

DCL模式也是在使用时才会创建,且只在首次创建实例时有锁竞争,实例创建完成后,每次获取实例时,在第一次检测时就已经知道实例创建好了,不会再执行synchronized里的代码段,也就没有锁竞争了,效率明显比懒汉式提高了不少

解释下为什么获取到锁之后还要再次检测:因为第一次检测时没有加锁,那么获取到锁之后,有可能别的线程已经创建好了,如果不判断直接创建,就可能实际上创建了多个实例,达不到单例的目的了。

同时,请注意,这里还用到了volatile关键字来修饰instance,其最关键的作用是防止指令重排序,具体分析过程如下:
当在Java中new一个对象时,如instance = new Singleton();,其实在JVM里面的步骤分为三步:

  1. 在堆内存开辟内存空间
  2. 在堆内存中使用参数对Singleton进行实例化
  3. 把变量instance指向堆内空间地址

由于JVM存在乱序执行功能,有可能第3步骤先于第2步执行。假如线程A执行完第3步后,线程B来获取实例,此时instance已经不为null了,线程B获取到了实例,但此时线程A第2步还没执行,实例还没完成初始化,线程B获取到的是不完整的实例,那么在使用时就会出问题。
volatile的可以禁止指令重排序,从而避免了上述问题的出现。

静态内部类模式(推荐)

public class Singleton {
    private Singleton() {
    }

    private static class Inner {
        private static Singleton instance = new Singleton();
    }

    public static Singleton getInstance() {
        return Inner.instance;
    }
}

静态内部类模式的实现原理:外部类加载时,并不会立刻加载内部类,内部类不被加载,就不会去创建实例,只有当getInstance方法被调用时,才会加载内部类去创建实例,所以实现了延迟加载功能。

那么内部静态类是如何保证线程安全的呢?在《深入理解JAVA虚拟机》中,有这么一句话:

虚拟机会保证一个类的<cinit>()方法在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<cinit>()方法,其它线程都需要阻塞等待,直到活动线程执行<cinit>()方法完毕。如果一个类的<cinit>()方法有耗时很长的操作,就可能造成多个线程阻塞(需要注意的是,其它线程虽然会被阻塞,但如果执行<cinit>()方法后,其它线程被唤醒之后不会再次进入<cinit>()方法。同一个加载器下,一个类型只会被初始化一次),在实际应用中,这种阻塞可能是很隐蔽的。

通过枚举实现单例

最佳的单例实现模式就是枚举模式。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。

public enum Singleton {
    INSTANCE;

    public void doSomething() {
        System.out.println("do something!");
    }
}

调用方法:

public class Main {
    public static void main(String[] args) {
        Singleton.INSTANCE.doSomething();
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值