定义: 在程序上下文中,只有一个对象实例
单例模式的特点
- 私有化构造器
- 私有化成员属性
- 对外提供一个public方法用于访问对象
创建单例的方式
- 饿汉式:类加载时就会创建对象 优点:线程安全 缺点:一开始就创建,需要内存资源
public final class SingleCase {
private static SingleCase instance = new SingleCase();
private SingleCase() {
}
public static SingleCase getInstance () {
return instance;
}
}
// 问题1:为什么加 final // 问题2:如果实现了序列化接口, 还要做什么来防止反序列化破坏单例 // 问题4:这样初始化是否能保证单例对象创建时的线程安全? // 问题5:为什么提供静态方法而不是直接将 INSTANCE 设置为 public, 说出你知道的理由 //问题6:反射也会破坏单例,如何防范反射攻击? }
问题1:防止子类中不适当的方法覆盖父类的方法破坏单例
问题2:在java中除了可以通过new关键字创建一个对象外,还可以通过反射、clone()、反序列化的方式创建对象。如果通过反序列化生成了对象,就不能维护单例了。我们可以通过以下代码防止反序列化破坏单例
//当用反序列化创建对象时,会调用readResolve(),因此我们直接给它重写,让它返回我们创建的对象就行了 public Object readResolve() { return INSTANCE; }
问题3:防止通过构造器创建实例。不能防止,如以下代码就可以通过反射拿到构造器并创建实例
问题4:没有,静态成员变量的初始化都是在类加载的时候完成的,由JVM保证线程安全性
问题5:更好的封装,能够在封装的方法中做一些其他的操作,例如惰性加载;泛型的支持;更好的控制
问题6:可以将单例类声明成抽象类,抽象类的构造器不具备创建对象的功能,所以反射也无法突破限制。那么本类如何创建单例对象呢?可以使用抽象类的引用指向匿名子类的实例,匿名子类中什么都不需要写
- 懒汉式(双检锁):类加载时不会创建对象,只有在首次使用时才会创建对象 优点:使用的时候才创建,没有一开始占用内存 缺点:线程不安全
public class SingleCase {
private SingleCase instance;
private SingleCase() {
}
public static SingleCase getInstance () {
// 双检锁
if (instance == null) {
synchronized (SingleCase.class) {
if (instance == null) {
instance = new SingleCase();
}
}
}
return instance;
}
}
// 问题1:解释为什么要加 volatile ?
private static volatile Singleton INSTANCE = null;
// 问题2:对比实现3, 说出这样做的意义
// 问题3:为什么还要在这里加为空判断, 之前不是判断过了吗
问题一:为了防止synchronized同步块中实例创建和赋值的指令重排序导致其他线程拿到不完整实例的问题,volatile可以防止指令重排,在写之后加一个写屏障防止写之前的代码重排序到写之后(具体过程看附录)
问题二:减小了锁的范围,性能更好 问题三:为了防止首次实例化时多线程并发的问题,即有多个线程同时判断INSTANCE为空后往下执行的情况,所以需要再次判断
懒惰实例化 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外 上面的代码看上去好像并没有什么问题,使用双重检测锁既防止多次进入synchronized耗费性能,又能有效的防止多线程下多次实例化的问题。但是其实上面的代码还是有问题的,那就是第五行和第十三行可能会发生指令重排序
- 枚举 线程安全,一开始就创建
// 问题1:枚举单例是如何限制实例个数的
// 问题2:枚举单例在创建时是否有并发问题
// 问题3:枚举单例能否被反射破坏单例
// 问题4:枚举单例能否被反序列化破坏单例
// 问题5:枚举单例属于懒汉式还是饿汉式
// 问题6:枚举单例如果希望加入一些单例创建时的初始化逻辑该如何做
enum Singleton {
INSTANCE;
}
问题一:枚举对象定义时有几个,就会产生几个,相当于枚举类的静态成员变量
问题二:不会有并发变量,成员变量为静态成员变量,在类加载的时候就初始化了,由JVM保证线程安全
问题三:不能,因为枚举类型没有构造器不能被实例化
问题四:不能,我们从字节码中可以看到枚举类型enum继承自java.lang.Enum,其父类已经实现了Serializable接口并且默认就是反序列化的,所以说枚举单例天生防止反序列化破坏单例
问题5:饿汉式
问题六:枚举也可以写构造方法,可以在构造方法中进行初始化
public enum EnumSingleton {
INSTANCE;
EnumSingleton(){
}
}
- 静态内部类(推荐) 优点:使用的时候才创建,没有以开始占用内存,线程安全
public class SingleCase {
private SingleCase() {
}
public static SingleCase getInstance () {
return Holder.instance;
}
public static class Holder{
static final SingleCase instance = new SingleCase();
}
}
// 问题1:属于懒汉式还是饿汉式
// 问题2:在创建时是否有并发问题
- 问题一:属于懒汉式,因为类加载本身就是懒汉式的,如果没有调用
getInstance时,JVM是不会去加载LazyHolder类的 - 问题二:不会有并发问题,类加载中的线程安全是由
JVM保证的,所以不会有并发问题