JAVA设计模式—单例模式

一、什么是单例模式?

顾名思义,就是某个类在全局中只有一个实例。如果一个类可以在外部随意通过new方法来实例化,那么它一定不是单例的。无论在哪里获取该类的实例,都应该是唯一的实例,即在不同地方获取到的实例实际上是同一个对象,哪怕是在不同的线程里获取到的依然是唯一实例。

通常我们用getInstance()方法来对外提供该类的单例。

二、单例模式的实现方式:

2.1 简单的饿汉式

public class Singleton { 
    private static Singleton instance = new Singleton();
 
    private Singleton() {
    // 在私有构造方法里进行一些变量的初始化操作
    }
 
    public static getInstance() {
        return instance;
    }
}

所谓饿汉,即提前加载,在第一次引用该类的时候就直接进行实例化(static关键字的作用)—即使并没有调用getInstance()方法。(可以理解为一个汉子饿怕了,所以不管他到底需不需要吃东西,都提前把吃的已经准备好了)

这种形式的缺点:资源浪费!假如我们只是引用该类的其他变量,也依然会实例化,造成了资源浪费。

2.2 简单的懒汉式

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

所谓懒汉,即实现懒加载,只有在调用到getInstance()方法的时候才会实例化。

这种形式的缺点:线程不安全!如果有多个线程同时调用getInstance()方法,就完全无法保证单例了,很可能导致new了多个对象,尤其是在私有构造方法里,如果里面的初始化操作比较多,就特别容易出现new了多个对象。

2.3 DCL型懒汉式

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

所谓DCL,就是Double-Checked Locking,也就是双重检查+锁的方式。

这种形式看起来似乎没问题了:10个线程在调用getInstance()时,instance为null,一个线程拿到锁,在锁里再经过一次检查,instance依然为null,则进行初始化操作。其他线程则在进行等待,在第一个线程完成初始化并拿到了instance实例后,再依次获取实例。但实际上这种形式依然存在一个坑,有一定的可能导致获取到的instance为null!问题就出在instance = new Singleton();这里了,实际上它并不是原子性的(atomic),一个简单的初始化实际上分成3个步骤:

  • 1、分配内存
  • 2、将分配的内存指向实例的引用
  • 3、将对象Singleton初始化给分配的内存

如果第一个线程的还卡在“将对象Singleton初始化给分配的内存”这里,第二个线程就有可能获取到锁,然后开心的以为拿到了Non-Null的instance,用的时候直接就挂了……所以这段代码还是需要更改的。

2.4 DCL增强型懒汉式

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

所谓增强,跟之前的一段代码相比,只多了一个volatile关键字。

当volatile用于一个作用域时,Java保证如下:

  • 1、(适用于Java所有版本)读和写一个volatile变量有全局的排序。也就是说每个线程访问一个volatile作用域时会在继续执行之前读取它的当前值,而不是(可能)使用一个缓存的值。(但是并不保证经常读写volatile作用域时读和写的相对顺序,也就是说通常这并不是有用的线程构建)。
  • 2、(适用于Java5及其之后的版本)volatile的读和写建立了一个happens-before关系,类似于申请和释放一个互斥锁。

显然第二点是我们所需要的。在使用了volatile之后,第一个线程的初始化未完成时,其他线程是得不到instance的。

至此,问题已经圆满解决了!但是,还有没有更好的办法?当然有!

2.5 静态内部类单例模式

public class Singleton {
    private Singleton() {}
 
    private static class SingletonHolder {
        private static Singleton singletonHolder = new Singleton();
    }
 
    public static Singleton getInstance() {
        return SingletonHolder.singletonHolder;
    }
}

把Singleton实例放到一个静态内部类SingletonHolder中,这样就避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的,可以说这种方法已经比较完善了,写起来也不复杂。

至此,我们通过各种尝试,已经实现了既保证单例又能实现延迟加载的实现了,基本上快要达到圆满了。但是,凡是就怕但是,它们依然不完美:

如果不自行实现序列化,那么每次去反序列化一个序列化的对象都会创建一个新的实例。
虽然我们的构造方法是private的,但在其他地方如果通过反射的方式还是可以调用构造方法,当然我们可以通过在构造方法里进行判断,创建第二个实例的时候抛出异常。

那么到底有没有,完美的、一劳永逸的、简洁明了的单例呢?有!

2.6 枚举实现单例模式

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

是的,enum的构造方法天生就是私有的。也直接提供了线程安全,还可以防止反序列化时创建新对象,说是完美并不夸张。

也许有人会有疑问,以前Android官网明确写着尽量不要用enum,因为会增加内存开销:

毫无疑问,枚举一定会比直接定义常量多占用内存(因为枚举是一个完整的类,而常量只占用了最基本的内存),但我们是在实现单例啊!本身就要写一个类,所以枚举本来的那点问题就不算是问题了。再说,以Android设备现在的硬件配置,尤其是内存容量,我们完全不需要避讳枚举了。如果通过利用枚举能显著提升程序的可读性和可维护性,那么就不需要纠结这点内存消耗了。

最后,此文借鉴于某作者的原文:https://www.kaelli.com/24.html ,特此感谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值