乐趣区

设计模式之单例模式

java 设计模式之 – 单例模式

单例模式

单例模式限度类的实例和确保 java 类在 java 虚拟机中只有一个实例的存在。

单例类必须提供一个全局的拜访来获取类的实例。

单例模式用来日志,驱动对象,缓存和线程池。

单例设计模式也用在其余设计模式,例如形象工厂,建造者,原型,门面等设计模式。

单例模式还用在外围 java 中,例如 java.lang.Runtime, java.awt.Desktop

java 单例模式

为了实现 Singleton 模式,咱们有不同的办法,但它们都有以下独特的概念。

  • 公有构造方法限度从其余类初始化类的实例。
  • 公有动态变量与该类的实例雷同。
  • 私有静态方法返回类的实例,这是提供给内部拜访的全局拜访点来获取单例类的实例。在以下的章节,咱们将学习单例模式的不同实现办法。

常见的实现形式如下

  • 饿汉式
  • 懒汉式
  • volatile 双重查看锁机制
  • 动态外部类
  • 枚举(天生单例)

饿汉式

顾名思义,饿汉式就是第一次援用该类的时候就创立实例对象,而不论是否须要。代码如下:

    public class Singleton {private static Singleton singleton = new Singleton();
        private Singleton() {}
        public static Singleton getSignleton(){return singleton;}
    }

优缺点:这样做的益处是代码简略,然而无奈做到提早加载。然而很多时候咱们心愿可能提早加载,从而减小负载,所以就有了上面的懒汉式;

懒汉式

单线程写法
这种写法是最简略的,由公有结构器和一个私有动态工厂办法形成,在工厂办法中对 singleton 进行 null 判断,如果是 null 就 new 一个进去,最初返回 singleton 对象。
这种办法能够实现延时加载,然而有一个致命弱点:
线程不平安。如果有两条线程同时调用 getSingleton()办法,就有很大可能导致反复创建对象。

public class Singleton {
   private static Singleton singleton = null;
   
   private Singleton(){}
   
   public static Singleton getSingleton() {if(singleton == null) {singleton = new Singleton();
       }
       return singleton;
   }
}

线程平安写法
这种写法思考了线程平安,将对 singleton 的 null 判断以及 new 的局部应用 synchronized 进行加锁。同时,对 singleton 对象应用 volatile 关键字进行限度,保障其对所有线程的可见性,并且禁止对其进行指令重排序优化。如此即可从语义上保障这种单例模式写法是线程平安的。留神,这里说的是语义上,理论应用中还是存在小坑的,会在后文写到。

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

双重查看锁

尽管下面这种写法是能够正确运行的,然而其效率低下,还是无奈理论利用。因为每次调用 getSingleton()办法,都必须在 synchronized 这里进行排队,而真正遇到须要 new 的状况是非常少的。所以,就诞生了第三种写法:

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

这种写法被称为“双重查看锁”,顾名思义,就是在 getSingleton()办法中,进行两次 null 查看。看似多此一举,但实际上却极大晋升了并发度,进而晋升了性能。为什么能够进步并发度呢?就像上文说的,在单例中 new 的状况非常少,绝大多数都是能够并行的读操作。因而在加锁前多进行一次 null 查看就能够缩小绝大多数的加锁操作,执行效率进步的目标也就达到了;

双重查看锁机制的坑

那么,这种写法是不是相对平安呢?后面说了,从语义角度来看,并没有什么问题。然而其实还是有坑。

  • 说这个坑之前咱们要先来看看 volatile 这个关键字。其实这个关键字有两层语义。
  • 第一层语义大家绝对比拟相熟,可见性。可见性是指在一个线程中对该变量的批改由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反馈到其余线程的读写操作中。顺便一提,工作内存和主内存能够近似了解成电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的
  • volatile 的第二层语义是避免指令重排。大家晓得咱们写的代码(尤其是多线程代码),因为编译器优化,在理论执行的时候可能和咱们编写的程序不同。编译器只保障程序执行后果和源代码雷同,却不保障理论指令的程序和源代码雷同。这在单线程没什么问题,然而一旦引入多线程,这种乱序就可能导致重大问题。volatile 关键字就能够从语义上解决这个问题。

留神,禁止指令重排优化这条语义直到 jdk1.5 当前能力正确工作。此前的 JDK 中即便将变量申明为 volatile 也无奈完全避免重排序所导致的问题。所以,在 jdk1.5 版本前,双重查看锁模式的单例模式是无奈保障线程平安的。

动态外部类

那么,有没有一种延时加载,并且能保障线程平安的简略写法呢?咱们能够把 Singleton 实例放到一个动态外部类中,这样就防止了动态实例在 Singleton 类加载的时候就创建对象,并且因为动态外部类只会被加载一次,所以这种写法也是线程平安的:

public class Singleton {

    private static class Holder {private static Singleton singleton = new Singleton();
    }
 
    private Singleton(){}
 
    public static Singleton getSingleton(){return Holder.singleton;}
}

然而,下面提到的所有实现形式都有两个独特的毛病:

  1. 都须要额定的工作 (Serializable、transient、readResolve()) 来实现序列化,否则每次反序列化一个序列化的对象实例时都会创立一个新的实例。
  2. 可能会有人应用反射强行调用咱们的公有结构器(如果要防止这种状况,能够批改结构器,让它在创立第二个实例的时候抛异样)。

枚举写法

当然,还有一种更加优雅的办法来实现单例模式,那就是枚举写法:

public enum SingleEnum {
    NEW_INSTANCE {
        @Override
        protected void doSomething() {System.out.println("---- 业务办法调用 ----");
        }
    };

    SingleEnum() {}

    /**
     * 业务办法定义
     */
    protected abstract void doSomething();

    public static void main(String[] args) {SingleEnum.NEW_INSTANCE.doSomething();
    }
}

应用枚举除了线程平安和避免反射强行调用结构器之外,还提供了主动序列化机制,避免反序列化的时候创立新的对象。因而,Effective Java 举荐尽可能地应用枚举来实现单例。

总结

代码没有一劳永逸的写法,只有在特定条件下最合适的写法。在不同的平台、不同的开发环境(尤其是 jdk 版本)下,天然有不同的最优解(或者说较优解)。
比方枚举,尽管 Effective Java 中举荐应用,然而在 Android 平台上却是不被举荐的。在这篇 Android Training 中明确指出:

Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.

再比方双重查看锁法,不能在 jdk1.5 之前应用,而在 Android 平台上应用就比拟释怀了(个别 Android 都是 jdk1.6 以上了,不仅修改了 volatile 的语义问题,还退出了不少锁优化,使得多线程同步的开销升高不少)。

最初,不论采取何种计划,请时刻牢记单例的三大要点:

  • 线程平安
  • 提早加载
  • 序列化与反序列化平安

参考资料

《Effective Java(第二版)》
《深刻了解 Java 虚拟机——JVM 高级个性与最佳实际(第二版)》

退出移动版