单例设计模式-Java

单例设计

定义

​ 确保一个类只有一个实例,并提供该实例的全局访问点。

特点

单例模式有 3 个特点:

  1. 单例类只有一个实例对象;
  2. 该单例对象必须由单例类自行创建;
  3. 单例类对外提供一个访问该单例的全局访问点。

单例模式的优点:

  • 单例模式可以保证内存里只有一个实例,减少了内存的开销。
  • 可以避免对资源的多重占用。
  • 单例模式设置全局访问点,可以优化和共享资源的访问。

单例模式的缺点:

  • 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
  • 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
  • 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

单例模式看起来非常简单,实现起来也非常简单。单例模式在面试中是一个高频面试题。希望大家能够认真学习,掌握单例模式,提升核心竞争力,给面试加分,顺利拿到 Offer。

懒汉式

Ⅰ懒汉式-线程不安全

​ 以下实现中,私有静态变量 s1 被延迟实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 s1,从而节约资源。

​ 这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (s1 == null) ,并且此时 s1 为 null,那么会有多个线程执行 s1= new Singleton(); 语句,这将导致实例化多次 uniqueInstance。

public class Singleton1 {
    private Singleton1() {
    }

    private  static Singleton1 s1;

    public static Singleton1 getInstance(){
        if(s1 == null){
            s1 = new Singleton1();
        }
       return  s1;
    }
}

Ⅱ懒汉式-线程安全

只需要对 getInstance () 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 s2。但是当一个线程进入该方法之后,其它试图进入该方法的线程都必须等待,即使 s2已经被实例化了。这会让线程阻塞时间过长,因此该方法有性能问题,不推荐使用。

public class Singleton2 {
    private  Singleton2(){
    }

    private volatile  static  Singleton2 s2;

    public  static synchronized  Singleton2  getInstance(){
        if (s2 == null) {
            s2 = new Singleton2();
        }
        return s2;
    }
}

Ⅲ双重校验锁-线程安全

s3只需要被实例化一次,之后就可以直接使用了。加锁操作只需要对实例化那部分的代码进行,只有当 s3没有被实例化时,才需要进行加锁。

双重校验锁先判断 s3是否已经被实例化,如果没有被实例化,那么才对实例化语句进行加锁。

public class Singleton3 {
    private Singleton3() {
    }

    private static volatile Singleton3 S3;


    public static Singleton3 getInstance() {
        if (S3 == null) {
            synchronized (Singleton3.class){
                if (S3 == null){
                    S3 = new Singleton3();
                }
            }
        }
        return S3;
    }
}

​ 考虑下面的实现,也就是只使用了一个 if 语句。在 s3 == null 的情况下,如果两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。虽然在 if 语句块内有加锁操作,但是两个线程都会执行 s3= new Singleton(); 这条语句,只是先后的问题,那么就会进行两次实例化。因此必须使用双重校验锁,也就是需要使用两个 if 语句:第一个 if 语句用来避免 s3 已经被实例化之后的加锁操作,而第二个 if 语句进行了加锁,所以只能有一个线程进入,就不会出现 s3 == null 时两个线程同时进行实例化操作。

synchronized (Singleton3.class){
    if (S3 == null){
        S3 = new Singleton3();
    }
}

s3 采用 volatile 关键字修饰也是很有必要的, s3= new Singleton(); 这段代码其实是分为三步执行:

  1. 为 s3分配内存空间
  2. 初始化 s3
  3. 将 s3 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getInstance() 后发现 s3不为空,因此返回 s3,但此时 s3还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

Ⅳ静态内部类实现

当 Singleton 类被加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance() 方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。

这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。

public class Singleton5 {
    private  Singleton5(){}

    private  static class SingletonHolder{
        private static final Singleton5 S5 = new Singleton5();
    }

    public static Singleton5 getInstance(){
        return  SingletonHolder.S5;
    }
}

懒汉式

Ⅴ饿汉式-线程安全

线程不安全问题主要是由于 s4 被实例化多次,采取直接实例化 s4的方式就不会产生线程不安全问题。

但是直接实例化的方式也丢失了延迟实例化带来的节约资源的好处。

public class Singleton4 {
    private  Singleton4(){}

    private  static  Singleton4 singleton4 = new Singleton4();

    public   static  Singleton4 getInstance(){
        return singleton4;
    }
}

PS参考

CN-NOTES

剑指Offer(第二版)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值