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高级个性与最佳实际(第二版)》