单例模式
饿汉模式
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton() {
}
public static Singleton getInstance(){
return singleton;
}
}
此方法能保证singleton唯一性,但一起动则初始化,如果有大量的动作,那么会极为耗费性能,所以引申出懒汉模式。
懒汉模式
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
public synchronized static Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
此方法能保证唯一性,而且系统初始化时也不会耗费性能,只有第一次调用才会耗费性能,但由于使用了synchronized,所以getInstance方法效率很低。
DCL单例模式
懒汉模式是这样的
/**
* 普通的单例模式
* @author
*/
public class Singleton {
private static Singleton singleton;
private Singleton(){}
private synchronized static Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
又或者是这样的
private static Singleton getInstance(){
synchronized(Singleton.class){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
但实质上synchronized放在方法上和代码块上的作用对于同步而言是一样的,都是使其包裹区域同一时间内只有一个线程可以运行(这是原子性),但是我们细心观察发现,我们其实需要保证同步的代码只有“singleton = new Singleton()”这一行代码,而且我们synchronized会造成性能损耗,那么对于这种情况,我们应该有更好的方法,那么就到了我们的DCL(double check locking)单例模式
DCL单例模式
private static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.singleton) { // flag1
if (singleton == null){ // flag2
singleton = new Singleton();
}
}
}
return singleton;
}
这样以后,只有第一个初始化的时候才会进入同步代码块,如果第一次初始化有thread1和thread2执行到flag1代码后,thread1释放完锁后thread2进入并不会再次new实例,这就保证了唯一性
这样看似很完美,但事实并不如此,我们先来看singleton = new Singleton被编译为指令后做了什么操作,做了三步操作:
- 1、给singleton实例分配内存
- 2、执行初始化构造方法
- 3、将instance指向了实际分配的内存空间。
注意,在3这一步的时候因为singleton引用指向了实际的内存空间,所以它不为null了
但jvm指令重拍会造成一个情况,会造成2和3的顺序重排,原因是为了优化指令,所以实际上的指令顺序是这样的
- 1、给singleton实例分配内存
- 3、将instance指向了实际分配的内存空间
- 2、执行初始化构造方法
然后当执行3完后,如果有另一个线程此时调用getInstance方法,那么第一行的判断则不成立,会返回singleton,此时返回的singleton是未初始化完的,那么使用肯定会报错“实例未初始化”,为解决此问题则引入一个volatile关键字,此关键字修饰的变量不会被指令重排(这是有序性,除此之外volatile还保证了可见性),注意volatile修饰的变量不会被指令重排,但其他的代码会被指令重排,但指令重排不会影响被volatile修饰的变量的代码顺序
所以使用volatile修饰
private static volatile Singleton singleton;
完整代码如下
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {
}
private static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.singleton) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
最后做个总结:单例模式最初有饿汉懒汉的区分,但懒汉由于一般使用synchronized代码块修饰导致性能降低,所以我们更改了synchronized的位置保证只影响第一次初始化时的性能,然后又由于jvm指令重排的问题,我们引入了volatile关键字
我们思考一下,能不能使用其他的方法来实现第一次调用时才加载,且不使用synchronized机制呢?答案很显然有,就是Holder单例模式,它使用了静态内部类的方法
Holder单例模式
DCL是为了解决懒汉模式中存在的性能问题,但它的代码逻辑有些许复杂,我们可以用更简单的方式来实现
public class Singleton {
private static class InSingleton{
private static InSingleton inSingleton = new InSingleton();
}
public static InSingleton getInstance(){
return InSingleton.inSingleton;
}
}
为什么这个能保证唯一性,且能保证延时加载,为啥呢?
首先是延时加载的问题:
当我们的系统启动时Singleton被JVM加载,但此时我们内部类InSingleton并没有被加载,这是延时加载,当getInstance方法被调用的时候,InSingleton就被初始化了
然后是唯一性问题:
jvm解释中,虚拟机会保证一个类的init方法被正确的加锁,当多个线程同时去init的时候,只有一个线程会进入init方法,其他线程则会被阻塞,这就保证了唯一性
虽说Holder看起来是比较完美的单例模式,既解决了延时加载,也解决了唯一性安全的问题,但由于是内部类创建的实例,所以如果你想创建实例的时候根据传入参数来创建的话,那么是无法完成的,所以根据这一点来说,DCL和Holder自行评估使用
枚举单例模式
public enum SingletonEnum {
/**
* 需要的对象
*/
INSTANCE
}
枚举单例模式特别简洁,且里面同样可以拥有属性方法(因为编译后枚举也是一个类,只是继承与Enum),且同样能保证唯一性,还有防止序列化和反射的不一致情况,具体可百度,太多不想写了