单例模式
单例模式(Single Pattern)是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。即需要隐藏其所有的构造方法,只能通过全局访问点来创建。
常见的单例有:ServletContext、ServletConfig、ApplicationContext、DBPool。
单例分为饿汉式单例,懒汉式单例,注册式单例,ThreadLocal单例。
饿汉式单例
饿汉式单例是指在单例类首次加载时就创建实例,缺点是:浪费内存空间。即不管用不用,也不管什么时候用,先创建,放着,占着内存空间。饿着,先吃饱。
public class HungrySingleton { //第一步,不管用不用,也不管什么时候用,先创建出来,占着内存空间 private static final HungrySingleton instance = new HungrySingleton(); //第二步,私有化构造方法,也即隐藏构造方法 private HungrySingleton(){}; //第三步,提供一个全局访问点,并返回创建的实例 public static HungrySingleton getInstance(){ return instance; } }
第二种,高端一点的饿汉式单例
public class HungryStaticSingleton { //第一步,不管用不用,也不管什么时候用,先创建出来,占着内存空间 private static final HungryStaticSingleton instance; static{ instance = new HungryStaticSingleton(); } //第二步,私有化构造方法,也即隐藏构造方法 private HungryStaticSingleton(){}; //第三步,提供一个全局访问点,并返回创建的实例 public static HungryStaticSingleton getInstance(){ return instance; } }
这两种方式,都叫做饿汉式单例写法。
缺点:不管用不用,都初始化,如果大批量的采用这种写法,造成内存的浪费。
第二种为什么写在static块中呢?这是涉及到java的类加载机制,即:先静态后动态,先属性后方法,先上后下。所以写在static中,创建较快。
懒汉式单例
懒汉式单例是指先定义好类,被外部内调用的时候再创建实例。不用就一直不创建。
public class lazySimpleSingleton { // 第一步,先定义好 private static lazySimpleSingleton instance; // 第二步,私有化构造方法,也即隐藏构造方法 private lazySimpleSingleton() {}; // 第三步,提供一个全局访问点,如果没有就创建 public static lazySimpleSingleton getInstance() { if (instance == null) { instance = new lazySimpleSingleton(); } return instance; } }
优点:解决内存空间浪费的问题。但是风险是:有可能创建多个不同的实例(在多线程中可能创建多个不同的实例,也即违背了单例模式)。即饿汉式变成懒汉式有线程安全问题。产生的原因是:多个线程同时进入,同时判断,导致同时创建,也即创建了两个不同的对象。
解决方法,加锁
public class lazySimpleSingleton { // 第一步,先定义好 private static lazySimpleSingleton instance; // 第二步,私有化构造方法,也即隐藏构造方法 private lazySimpleSingleton() {}; // 第三步,提供一个全局访问点,如果没有就创建 public static synchronized lazySimpleSingleton getInstance() { if (instance == null) { instance = new lazySimpleSingleton(); } return instance; } }
加锁过后,执行new就有了顺序。
但是锁类,会发生阻塞,锁的力度太粗,如果锁方法呢?
public class DoubleCheckSingleton { // 第一步,先定义好 private static DoubleCheckSingleton instance; // 第二步,私有化构造方法,也即隐藏构造方法 private DoubleCheckSingleton() {}; // 第三步,提供一个全局访问点,如果没有就创建 public static DoubleCheckSingleton getInstance() { //不是任何时候进来都要加锁,只有创建对象才加锁 if(instance == null){ //锁方法,不会发生阻塞 synchronized (DoubleCheckSingleton.class) { if (instance == null) { instance = new DoubleCheckSingleton(); } } } return instance; } }
这即是双重检查锁。
以上方法没得问题,但是如果在超高并发的条件下,必须加上volatile,也即:
public class DoubleCheckSingleton { // 第一步,先定义好 private static volatile DoubleCheckSingleton instance; // 第二步,私有化构造方法,也即隐藏构造方法 private DoubleCheckSingleton() {}; // 第三步,提供一个全局访问点,如果没有就创建 public static DoubleCheckSingleton getInstance() { //不是任何时候进来都要加锁,只有创建对象才加锁 if(instance == null){ //锁方法,不会发生阻塞 synchronized (DoubleCheckSingleton.class) { if (instance == null) { instance = new DoubleCheckSingleton(); } } } return instance; } }
加上volatile是为了禁止指令重排。
那什么是指令重排?
在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。
上述代码其中的关键在于,初始化和赋值操作是分开的。在多线程情况下,由于CPU考虑到提高自身利用率,会进行指令的重排。导致的结果是,可能会出现下面这种危险的情况:
第一步:在堆上开辟空间;
第二步:把刚刚开辟的地址空间赋值给instace变量,此时对象的值默认为0;
第三步:初始化对象,即把具体对象赋值给instace变量。
假如在第二步的时候,另外的线程调用了,得到的是一个半初始化的对象。用这个对象来计算,如果是库存信息,会把库存清零;如果是价格,会把价格设为0;导致发生重大问题。
加上volatile之后,会禁止JVM指令重排,保证其按照顺序执行,不会出错。其具体实现方法就是在该对象的创建代码的前后加上内存屏障。