持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第9天,点击查看活动详情
单例模式属于创建型模式的一种,主要是希望某个类在程序的始终只存在一个对象,在这个类提供访问这唯一对象的访问方式,这个类就叫做单例类,这种模式就称为单例模式。
单说理论有点干……
平常我们创建对象基本上都是使用关键字new,而new关键字是每使用一次就会产生一个新的对象,从JVM模型的角度来看就是,每new一次对象(普通对象)都会在堆中开辟新的内存空间,然后在栈中产生一个指向该内存的指针,当然了这里不详谈,以后有机会再写,至少现在还没这个能力写这么庞大的文章。
那么,说了这么多,为什么要创建一个单例对象呢?那就是要避免频繁创建和销毁系统全局使用的对象,这话看似很普通,但是我们在编程的时候经常会用到,比如最经典的就是,为什么要使用线程池?因为如果不用线程池,线程的上下文切换会消耗系统资源,而频繁切换则会大量消耗;同理,频繁创建一个常用的同样的对象也会消耗系统资源,还不如从一至终只创建一个。
单例模式的特点:其实上面就有提到
(1)单例类只能有一个实例。
(2)单例类必须自己创建唯一实例。
(3)单例类必须给其他类提供访问这一实例的入口。
需要说明的是,单例模式有好几种,有懒汉式、饿汉式、饿汉式同步锁、双重校验锁、静态内部类、枚举类,下面一一举例说明
一、饿汉式
饿汉式由于程序启动时直接创建对象实例,并且将唯一构造器设置成private,避免了其他程序访问,保证了线程安全。
class HungryMan {
// 只提供无参构造,并设置为private,避免其他类直接访问
private HungryMan(){}
// 设置成private 与 static,程序启动时直接创建对象实例
private static HungryMan hungryMan = new HungryMan();
// 设置成public的访问入口,供其他对象访问
public static HungryMan getInstance(){
// 返回已经创建好的实例,保证唯一性
return hungryMan;
}
}
优点:简单、直接
缺点:直接在程序启动时创建,降低了程序的启动速度;不论该实例是否被使用,浪费了内存;
二、懒汉式
针对饿汉式启动即创建的缺点,懒汉式在该实例被用到时才会创建
class LazyMan {
// 只提供无参构造,并设置为private,避免其他类直接访问
private LazyMan() {
}
// 设置成private 与 static
private static LazyMan lazyMan = null;
// 设置成public的访问入口,供其他对象访问
public static LazyMan getInstance() {
// 第一次调用时才会创建,后续调用直接返回创建好的(这里会出现线程不安全问题)
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
}
优点:克服了饿汉式启动即创建的缺点
缺点:缺点也很明显,就是会出现线程不安全问题(多线程情况下有可能会创建多个实例),简单描述一下问题,在如果在多线程下, 一个线程进入了if (lazyMan == null) 判断语句时,还未往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。也就是说,如果是单线程程序,还是可以用这个模式,但是多线程则不可以了。
三、同步锁-懒汉式
那么针对与懒汉式线程不安全的问题,有没有什么解决办法呢?那当然,不要问,问就是加锁
class SynLazyMan {
// 只提供无参构造,并设置为private,避免其他类直接访问
private SynLazyMan() {
}
// 设置成private 与 static,并且要加volatile关键字
private static volatile SynLazyMan synLazyMan = null;
// 设置成public的访问入口,供其他对象访问
public static SynLazyMan getInstance() {
// 加锁,每次只有一个线程访问,其他的堵塞再此
synchronized (SynLazyMan.class){
// 第一次调用时才会创建,后续调用直接返回创建好的
if (synLazyMan == null) {
synLazyMan = new SynLazyMan();
}
}
return synLazyMan;
}
}
优点:克服了多线程下线程不安全的问题;
缺点:每次都会加锁和释放锁操作,并且除了第一个拿到锁的线程之外,其他的线程都需要堵塞在此,效率低。
PS:这里在定义的时候加了volatile关键字,但在这里其实可以不加,因为没有拿到锁线程直接被阻塞在锁之外,具体为什么要加volatile关键字通过下面的double-check模式说明。
四、双重检测-懒汉式
应该叫双重检测-同步锁-懒汉式,也叫double-check模式,据说是美团整出来的,为了克服同步锁-懒汉式效率低的问题,在拿到锁之前再加一层判断
class DoubleCheckLazyMan {
// 只提供无参构造,并设置为private,避免其他类直接访问
private DoubleCheckLazyMan() {
}
// 设置成private 与 static,并且要加volatile关键字
private static volatile DoubleCheckLazyMan doubleCheckLazyMan = null;
// 设置成public的访问入口,供其他对象访问
public static DoubleCheckLazyMan getInstance() {
// 再加一层判断,如果已经有线程创建好了实例,后续实例就不需要再尝试获取锁造成堵塞了
if(doubleCheckLazyMan == null){
// 加锁,每次只有一个线程访问,其他的堵塞再此
synchronized (DoubleCheckLazyMan.class){
// 第一次调用时才会创建,后续调用直接返回创建好的
if (doubleCheckLazyMan == null) {
doubleCheckLazyMan = new DoubleCheckLazyMan();
}
}
}
return doubleCheckLazyMan;
}
}
这种方式是比较完美的,即克服了线程不安全的问题,又解决了效率低下的问题(当然了,是针对于同步锁-懒汉式而言,事实上加了锁肯定会效率低下一些);而这个模式最关键的是volatile关键字
private static volatile DoubleCheckLazyMan doubleCheckLazyMan = null;
不加volatile关键字会发生什么?
主要原因在于doubleCheckLazyMan = new DoubleCheckLazyMan();并不是原子性的操作。
创建一个对象可以分为三步:
1.分配对象的内存空间(称为半初始化对象)
2.初始化对象(可以理解为给当前对象赋值)
3.设置doubleCheckLazyMan指向刚分配的内存地址(栈中的指针指向堆内存)
当doubleCheckLazyMan指向分配地址时,doubleCheckLazyMan是不为null的
上面三步按照我们正常的逻辑是1、2、3,但是JVM会用它以为效率好的方式对代码进行重排序,也就是在这里的步骤有可能变成1、3、2
那么在这种重排序步骤情况下,我们假设线程A执行到了doubleCheckLazyMan = new DoubleCheckLazyMan()这一步,并且完成了1、3,此时doubleCheckLazyMan已经指向堆内存了,也就是不为null了,恰好线程B到了最外面的if(doubleCheckLazyMan == null)判断,好,返回false,线程B直接跳过锁块,拿到了一个半初始化对象。
那么这里为什么加了volatile关键字就解决了这个问题呢?
private static volatile DoubleCheckLazyMan doubleCheckLazyMan = null;
volatile关键字在java程序起到的作用:
(1) 确保线程可见性;(加了volatile关键字的变量被修改之后会立马刷新回主内存,其他线程必须强行从主内存中读取最新的值)
(2) 禁止指令重排序;
这里最重要的就是禁止指令重排序了,也就是使得上面的逻辑一定是1、2、3,不会再发生1、3、2这样的步骤。
优点:效率高,线程安全。
缺点:代码复杂(其实也没多复杂),这应该是比较常用的了。
五、静态内部类
静态内部类能实现单例模式主要依赖JVM类加载的特性,主要有下面两点:
(1)JVM在加载外部类的时候并不会加载其静态内部类,在使用到静态内部类的时候才会对静态内部类进行加载。我们知道,类的静态变量是在类加载的时候进行加载的,这样静态内部类实现单例模式就有一个特性:如果没有使用到这个实例,这个实例就不会进行加载。这和懒汉模式一样,能有效节省资源。
(2)JVM底层保证类加载的安全,即使在高并发的情况下,类的加载都只有一次,这就保证了创建单例时的并发安全性。
class SingleStatic {
// 只提供无参构造,并设置为private,避免其他类直接访问
private SingleStatic() {
}
// 使用内部类方式创建
private static class InnerClass {
public static SingleStatic singleStatic = new SingleStatic();
}
// 第一次调用时才会创建(静态内部类只会加载一次),后续调用直接返回创建好的
public static SingleStatic getInstance() {
return InnerClass.singleStatic;
}
}
六、枚举类
枚举类单例模式直接通过枚举类是线程安全而实现,并且只会加载一次
class EnumSingleton {
// 只提供无参构造,并设置为private,避免其他类直接访问
private EnumSingleton(){
}
// 枚举类型是线程安全的,并且只会装载一次
private enum Singleton{
INSTANCE;
private final EnumSingleton instance;
Singleton(){
instance = new EnumSingleton();
}
private EnumSingleton getInstance(){
return instance;
}
}
// 设置成public的访问入口,供其他对象访问
public static EnumSingleton getInstance(){
return Singleton.INSTANCE.getInstance();
}
}
优点:枚举类是唯一不会被反射破坏单例的实现方式
总结:前五种单例方式都可以被Java最大的“bug”破坏掉,那就是反射,即使构造器是私有的也能拿到;而枚举类虽然构造器也是私有的,但是java 的反射 API 已经通过写死的方式限制了不能为枚举类型创建实例,所以没办法破坏。也就是说,枚举类是唯一一个相对安全的单例模式。当然了,实际上咱也不会手贱整反射破坏,所以根据情况使用其他的单例模式即可。