- 单例的创建常分为2种类型
1
2懒汉式:使用的时候才创建
饿汉式:类加载的视角就创建了实例
懒汉式
- 常见例子,线程不安全的懒汉式
1 | public class Singleton { |
- 单线程的时候工作正常,但在多线程的情况下就有问题了。如果两个线程同时运行到判断instance是否为null的if语句,并且instance的确没有被创建时,那么两个线程都会创建一个实例
- 多线程的懒汉式: 通过synchronized方式来确保线程安全,但是因为是锁的方式,每次调用getInstance()方法时都被synchronized关键字锁住了,会引起线程阻塞,影响程序的性能
1
2
3
4
5
6public static synchronized Singleton1 getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
饿汉式
- 无线程安全问题,不能延迟加载,影响系统性能。
1 | public class Singleton { |
- 如何解决线程安全,并能做到性能不受影响
好的单例方式
- 考虑到线程安全,性能问题,延迟初始化角度进行单例的创建和使用
1.双重检验锁:
1 | public class Singleton { |
- 为什么要双重检验:
1 | 第一重判断在同步前通过判读singleton是否初始化,减少不必要的同步开销。第2重抢到锁之后再次判断是否为空, 多线程情况下如果第2个线程抢到锁后发现不为空了,就不在创建。 |
- 整体好处:
1 | 延迟初始化。和懒汉模式一致,只有在初次调用静态方法getSingleton,才会初始化signleton实例。 |
volatile的作用是什么,volatile主要包含两个功能。
1
2保证可见性。使用 volatile 定义的变量,将会保证对所有线程的可见性。
禁止指令重排序优化。由于 volatile 禁止对象创建时指令之间重排序,所以其他线程不会访问到一个未初始化的对象,从而保证安全性。
上边代码为什么要使用volatile ?
虽然已经使用synchronized进行同步,但在第4步创建对象时,会有下面的伪代码:
1
2
3memory=allocate(); //1:分配内存空间
ctorInstance(); //2:初始化对象
singleton=memory; //3:设置singleton指向刚排序的内存空间复制代码当线程A在执行上面伪代码时,2和3可能会发生重排序,因为重排序并不影响运行结果,还可以提升性能,所以JVM是允许的。如果此时伪代码发生重排序,步骤变为1->3->2,线程A执行到第3步时,线程B调用getsingleton方法,在判断singleton==null时不为null,则返回singleton。但此时singleton并还没初始化完毕,线程B访问的将是个还没初始化完毕的对象。当声明对象的引用为volatile后,伪代码的2、3的重排序在多线程中将被禁止!
2.静态内部类模式:
- 静态内部类,线程安全,主动调用时才实例化,延迟加载效率高,推荐使用。
1 | public class Singleton { |
- 静态内部类方式的好处:
1 | 外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存 |
静态内部类又是如何实现线程安全的?
虚拟机会保证一个类的
()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的 ()方法,其他线程都需要阻塞等待,直到活动线程执行 ()方法完毕 可以看出instance在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化
其他知识: init和clinit区别
init和clinit方法执行时机不同
1
init是对象构造器方法,也就是说在程序执行 new 一个对象调用该对象类的 constructor 方法时才会执行init方法,而clinit是类构造器方法,也就是在jvm进行类加载—–验证—-解析—–初始化,中的初始化阶段jvm会调用clinit方法。
执行目的的不同
1 | init是instance实例构造器,对非静态变量解析初始化,而clinit是class类构造器对静态变量,静态代码块进行初始化 |
3.枚举单例模式
- 枚举类型,无线程安全问题,在涉及到反射和序列化的单例中,建议使用下文的枚举类型模式。避免反序列化创建新的实例, Effective Java 是推荐该方法的
- 枚举单例模式的线程安全, 同样利用静态内部类中的类初始化锁, 枚举单例模式能够在序列化和反射中保证实例的唯一性。
1 | public enum Singleton { |
为什么枚举单例是线程安全的
其实枚举在经过javac的编译之后,会被转换成形如public final class T extends Enum的定义,枚举中的各个枚举项同事通过static来定义的。 例如枚举:
1
2
3public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
}反编译之后
1 | public final class T extends Enum |
- 枚举类编译后默认为final class,可防止被子类修改。常量类可被继承修改、增加字段等,容易导致父类的不兼容。枚举类型是线程安全的,并且只会装载一次,充分的利用了枚举的这个特性来实现单例模式。
- 枚举实现的单例可以避免反射、序列化问题。序列化会通过反射调用无参数的构造方法创建一个新的对象, 枚举是无法进行反射的,所以也达到了防止反射和反序列化相关隐患
- static类型的属性会在类被加载之后被初始化, 当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的
破坏单例模式的方法及预防措施
1、除枚举方式外,其他方法都会通过反射的方式破坏单例。反射是通过强行调用私有构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例,,则阻止生成新的实例,解决办法如下:
1
2
3
4
5private Singleton(){
if (instance != null){
throw new RuntimeException("实例已经存在,请通过 getInstance()方法获取");
}
}2、如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例。所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(),反序列化时直接返回相关单例对象。
1 | public Object readResolve() throws ObjectStreamException { |