单例模式是23种设计模式中最简单的之一,本文将从原理是讲解五种线程安全的单例模式写法
1.饿汉式
特点:在jvm加载类时,就会初始化静态属性instance,因此是线程安全的
缺陷:这种方式不是一种懒加载方式,即使并没有调用getInstance方法也会实例化对象;另外如果单例对象的创建需要依赖外部资源时,这种方式也就不适用。
public class Singleton{
public static final Singleton instance = new Singleton();
private Singleton(){};
public static Singleton getInstance(){
return instance;
}
}
2.懒汉式
特点:通过使用synchronized来保证线程安全
缺陷:synchronized修饰的方法执行效率低,任何时刻都只能有一个线程能调用getInstance()方法,并且同步操作也只有在第一次创建单例对象的时候才需要
public class Singleton{
public static Singleton instance;
private Singleton(){};
public static synchronized Singleton getInstance(){
if(instance == null)
instance = new Singleton();
return instance;
}
}
3.双检锁
特点:在synchronized同步块里面和外面分别做一次空判断,防止多个线程通过第一层判断,并且只有第一次创建单例对象才会进入同步块,相对于懒汉式效率高
public class Singleton{
public static volatile Singleton instance;
private Singleton(){};
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
问题:为什么要用volatile修饰instance?
instance = new Singleton()这一行并不是一个原子操作,是以下三个步骤组成:
1.给instance对象分配内存空间
2.调用Singleton的构造方法初始化对象
3.将instance对象指向分配的内存空间
执行完第三步,instance对象就不为null了,如果它没有被volatile修饰,编译器在编译时会进行指令重排序优化,无法保证2、3步的执行顺序,可能是1-2-3或1-3-2,如果是按照1-3-2的顺序执行,则有可能会执行了1-3,另一个线程在调用getInstance()时,看到的是一个非空但未构造完成的instance,如果此时使用就会报错。
使用volatile修饰instance可以禁止指令重排序优化,在对volatile赋值操作之后会有一个内存屏障 lock add $0x0,(%esp) ;它保证了对volatile变量的写操作,都先行发生于后面对这个变量的读操作,”后面”是指时间上的先后顺序,在以上例子在,其他线程对instance的读取操作必然发生在1-2-3或1-3-2执行完成之后,从而不会发生读取到一个”创建了一半”的对象。
4. 静态内部类
特点:依赖jvm类加载机制来实现线程安全,但是由于SingletonHolder类是私有的,在调用getInstance()之前不会被初始化(反射除外),因此它是一种懒汉式,同时没有线程同步的效率问题,一般推荐采用这种写法。
public class Singleton{
private Singleton(){};
public static final Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
}
5.枚举
特点:单元素的枚举实现单例,无偿的提供了序列化,绝对防止多次实例化,即使是面对复杂的序列化和反射攻击时。“单元素的枚举类型已成为实现单例的最佳方法”(摘自《Effective java 》)
public enum Singleton{
INSTANCE;
}