JavaEE——单例模式

目录

一、什么是单例模式

二、饿汉模式

三、懒汉模式


一、什么是单例模式

单例模式是一种常见的设计模式,主要的适用场景就是在某些程序中,我们希望整个程序,有些类只出现一个实例,也即是只存在一个该类的对象。

在 Java 中有很多种写法可以来完成该模式,典型的两种就是饿汉模式和懒汉模式。

二、饿汉模式

饿汉模式,就是在程序一启动,类加载的时候,就将该类的对象创建出来。

单例模式的书写主要分为以下几个步骤:

首先,需要定义出一个 private 的类属性,并创建出一个该类对象赋值给该属性,这便是整个程序中唯一存在的一个实例。由于该属性是 static 修饰的类属性,而类属性是属于类对象的,类对象是由对应的 .class 文件加载出来的。因此类对象也是单例的( .class 文件只有一份,对应的类对象也只有一个 ),因此属于类对象的属性也是单例的。

然后,将构造方法设置为 private 的,此时外部就无法通过构造方法来创建该类的其他实例了。

最后,定义一个方法来获取最开始创建的唯一实例,并且必须是类方法,这样外部才能直接通过类名进行调用。

一番操作过后,整个程序中只能获取到我们在类中创建出的对象了,并且都是获取到这同一个对象。这一点我们可以通过程序验证:

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

    private Singleton() {
    }

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

public class Demo1 {
    public static void main(String[] args) {
        Singleton s1 = Singleton.getInstance();
        Singleton s2 = Singleton.getInstance();

        System.out.println(s1 == s2);
    }
}

可以看到 s1 和 s2 都是通过 getInstance 来获取 Singleton 类的对象,我们可以通过输出结果得知,它们所获取到的确实是同一个对象:

饿汉模式既简便又安全。本身书写起来就很简单,而且天生就是线程安全的,里面只涉及到变量的读取操作,因此无需考虑线程安全问题。 

三、懒汉模式

懒汉模式,与饿汉模式相比,最大的区别就是" 懒 "。因为其创建对象的时机是在第一次使用到该对象的时候。其余大致与饿汉模式相同。

class SingletonLazy {
    private static SingletonLazy instance;

    private SingletonLazy() {
    }

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

只有当调用到了 getInstance 方法的时候,才会创建出唯一实例。如果整个程序没有人调用该方法,那么就不会创建该实例。而在饿汉模式中,由于是在类加载的时候就创建出该对象,因此无论是否有人调用了该方法,都会创建。

秉持着" 能省就省 "的原则,因此懒汉模式也更常使用到。

但是上面的代码中还存在着线程安全问题:

1)由于 getInstance 内部涉及到判断与赋值操作,这两个本身是原子操作的就需要看作一个整体,因此就不是一个原子操作了,就容易产生线程安全问题:

例如:线程 1 执行到 if 判断的时候,此时线程 2 也执行到此处,时机刚好处在线程 1 执行 if 判断和赋值操作之间,此时两个线程都没执行到赋值操作,因此 if 判断处都为 true,就会导致两个线程都进行了赋值操作,出现了两个实例,这就不符合单例模式的本意了。

解决该问题的办法就是加锁,使 if 和赋值变成一个原子操作:

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

 2)除了非原子操作问题外,还存在着可能发生的内存可见性和指令重排序问题。

由于可能会存在多个线程反复读取 instance 的值来进行判断,因此也可能会触发编译器优化,直接从寄存器中读取 instance 的值而非从内存中读取,因此就可能出现内存可见性问题。

除此之外,还可能发生指令重排序问题:

在 Java 中,创建对象并赋值可以分为三步:

1. 给对象创建出内存空间,得到内存地址;

2. 给空间上调用构造方法,对对象进行初始化;

3. 给内存地址,赋值给 instance 引用。

而指令重排序作为编译器优化的另一种手段,可能会调整第2、3步的顺序。如果是在单线程的条件下,就不会产生过多影响,而现在考虑的是多线程的环境,因此就可能发生以下情况:

编译器刚好触发优化,调整了顺序变成了 1、3、2,当线程 1,刚完成第二步时,刚好发生了线程切换,调度到其他线程。此时刚好有另外一个线程执行 if 判断时,由于第一个线程已经完成了第三步,赋值给了 instance ,但是赋值的对象是不完整的,还没进行初始化的,但是 instance 已经不为空,if 处就为 false,直接返回了。

综上两个问题,需要给 instance 加上 volatile,一次性规避两个问题。

3)最后还需考虑的就是调用 getInstance 时都会加锁。频繁加锁就会产生更多的资源耗费。加锁本身并不会消耗很多成本,但是加锁就会产生锁竞争,而锁竞争所需要等待的时间是无法估计的,因此消耗的成本也就无法估计了。

但是通过分析可以发现,只有当第一次调用该方法创建对象时才需要加锁,后续每次调用其实都没必要加锁了,因此我们可以加多一次判断是否是第一次调用该方法:也就再次判断 instance 是否为空。

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

虽然两次 if 的条件判断一样,看起来很不合理,但是实际上是很合理的。

在单线程的情况下,这种做法就是很不正常的,但是现在是多线程,同时会有多个线程在执行,两个 if 之间隔了一层锁,因为这个锁停滞了多久,是无法预知的,在这一个等待的时间内,很可能其他线程就将 instance 的值就给修改了。并且两个 if 的作用是不同的:第一个 if 是为了判断是否需要加锁,第二个 if 是为了判断是否需要创建对象,因此该做法是可取的。

综上,就把可能出现的问题都给解决了,下面就是最后的版本:

class SingletonLazy {
    private static volatile SingletonLazy instance;

    private SingletonLazy() {
    }

    public static SingletonLazy getInstance() {
        if(instance == null) {
            synchronized (SingletonLazy.class) {
                if (instance == null) {
                    instance = new SingletonLazy();
                }
            }
        }
        return instance;
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值