定义:类加载的时候就创建对象。
优点:简单,无线程安全问题。
缺点:浪费空间,不够灵活。
public class Hungry {
public final static Hungry INSTANCE = new Hungry();
private Hungry() {
}
}
饿汉式 静态代码块
public class HungryStatic {
public final static HungryStatic INSTANCE;
static {
INSTANCE = new HungryStatic("例如从配置文件读入");
}
private HungryStatic(String config) {
}
private String config;
public String getConfig() {
return config;
}
public void setConfig(String config) {
this.config = config;
}
}
枚举
优点:线程安全,不会被反射破坏。
public enum Enum {
INSTANCE;
}
懒汉式 (线程不安全)
定义:在使用的时候如果没有创建过则创建。
优点:节约空间。
缺点:存在线程安全问题。
public class Lazy {
private static Lazy instance;
private Lazy() {
}
public static Lazy getInstance() {
if (instance == null) {
instance = new Lazy();
}
return instance;
}
}
懒汉式 双检锁 DCL
public class LazySafe {
private static volatile LazySafe instance;
private LazySafe() {
}
public static LazySafe getInstance() {
if (instance == null) {
synchronized (LazySafe.class) {
if (instance == null) {
instance = new LazySafe();
}
}
}
return instance;
}
}
为什么需要第二次判断
因为 如果两个线程 同时判断 instance == null ,开始争抢锁,第一个线程执行过后,如果不加锁,线程二会直接new 一个新对象,所以必须加锁保证只有一个对象创建 。
为什么要加 volatile一句话总结就是防止拿到 instance == null 的 对象。
反编译一条 new 指令可以拿到以下的结果。
步骤4 初始化对象,步骤7 指针指向该对象,由于 cpu存在指令重排序,先完成指针指向的话就会产生null对象的情况,所以需要禁止指令重排序保证对象初始化之后在完成指向的动作。
原理 : 静态内部类的延迟初始化,且类只会加载一次。
public class LazyInner {
private LazyInner() {
}
private static class Inner {
private static final LazyInner INSTANCE = new LazyInner();
}
public static LazyInner getInstance() {
return Inner.INSTANCE;
}
}
The “Double-Checked Locking is Broken” Declaration
破坏单例 多线程破坏单例多线程环境下会有很多线程同时进行 if 判断,导致创建多个独享。
虽然可以保证线程安全,但是方法性能太低,任意读请求都需要持有锁之后才可以进行相应操作。
使用双检锁,或者内部类的形式保证线程安全。
指令重排序破坏单例双检锁不使用 volatile 关键字修饰,就会产生重排序情况的产生。
克隆破坏单例 反序列化破坏单例 反射破坏单例结论:使用枚举类来实现单例,防止反射破坏单例。
破坏实例我们以Runtime为例来演示使用反射破坏单例。
Class解决方案aClass = Runtime.class; Constructor cons = aClass.getDeclaredConstructor(); cons.setAccessible(true); Runtime runtime = cons.newInstance(); Runtime runtime2 = cons.newInstance(); System.out.println(runtime); System.out.println(runtime2); ---- java.lang.Runtime@568db2f2 java.lang.Runtime@378bf509
优先使用枚举来解决反射破坏单例的问题。
我们来获取一下枚举类中的构造器
ClassaClass = Enum.class; Constructor>[] constructors = aClass.getDeclaredConstructors(); // Constructor cons = aClass.getDeclaredConstructor(); Constructor cons2 = aClass.getDeclaredConstructor(String.class, int.class); // cons.setAccessible(true); cons2.setAccessible(true); // Enum e1 = cons.newInstance(); // Enum e2 = cons.newInstance(); Enum e3 = cons2.newInstance(); Enum e4 = cons2.newInstance();
可以看到里面会生成一个String , int 参数类型的构造器
idea 反编译结果跟预期明显不同。
javap 反编译 class 文件后发现也没有相应的构造方法
我们更换反编译工具,使用 jad 进行后续的测试
jad下载地址
将 jad.exe 放在 jdk 的 bin 目录下即可
jad 反编译后成功看到Enum类中 String 和 int 参数的构造函数。
单例在java的应用场景 Runtime饿汉式实现单例。



