一、饿汉式单例:
public class HungrySingleton {private HungrySingleton(){}private static HungrySingleton hungrySingleton = new HungrySingleton();public static HungrySingleton getInstance(){return hungrySingleton;}
}
缺点:一开始就加载,可能会浪费空间
二、懒汉式单例
普通:
public class LazySingleton {private LazySingleton(){}private static LazySingleton lazySingleton;public static LazySingleton getInstacne(){if (lazySingleton==null){lazySingleton = new LazySingleton();}return lazySingleton;}
}
这种模式下无法保证多线程的并发问题,会造成单例被破坏。
双重锁检测:
public class LazySingleton {private LazySingleton(){}private static LazySingleton lazySingleton;public static LazySingleton getInstacne(){if (lazySingleton == null){synchronized(LazySingleton.class){if (lazySingleton==null){lazySingleton = new LazySingleton();}}}return lazySingleton;}
}
这种模式下,第一层lazySingleton判空是为了提高性能,同时保证是单例,只有初始为空的情况下才会继续执行下去,第二层lazySingleton判空是为了避免小概率事件的发生。在多线程的情况下,线程A和线程B同时通过第一层判空,进入到获得锁的入口,由于只有一个线程获得锁,假设线程A获得了锁,并创建了lazySingleton对象,当线程A执行完synchronize内部的代码释放锁,当线程B拿到锁后,就会执行第二层判空,就不会重复创建lazySingleton对象。如果没有第二层判空,线程B也会执行synchronize内部的代码,也就造成了创建2个lazySingleton对象。
但这种模式下,仍会出现问题。由于创建对象的过程并不是原子性操作的,在JVM层面来说,有3个执行指令,第一个是分配内存空间,第二个是执行构造方法,初始化对象,第三个是将对象指向空间,但由于CPU的指令优化,会导致指令执行顺序发生变化,可能存在执行顺序为132的情况。当线程A的指令执行顺序为132的情况,线程B获取lazySingleton时检测到lazySingleton不为null,就直接返回lazySingleton对象,但是lazySingleton对象还没有完成构造,所以线程B获取的lazySingleton为null。所以要保证lazySingleton对象必须加volatile。
volatile能保证可见性(当被volatile修饰的变量不会被缓存,一旦发生变化时就立即同步到主内存,不会出现线程A修改了变量,由于缓存机制的问题导致线程B无法看见修改后的变量值)和有序性(volatile有一个特性——禁止指令重排优化,这就保证了指令执行的顺序不会被改变),但无法保证原子性。
完整版双重检测锁(DCL懒汉):
public class LazySingleton {private LazySingleton(){}private volatile static LazySingleton lazySingleton;public static LazySingleton getInstacne(){if (lazySingleton == null){synchronized(LazySingleton.class){if (lazySingleton==null){lazySingleton = new LazySingleton();}}}return lazySingleton;}
}