- 学无止境 Java工程师的进阶之旅
单例模式
一、简介
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。
在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升
单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
二、类型
- 饿汉式:在类加载时已经创建好对象,等待被程序使用
- 懒汉式:在需要使用对象时才去创建该单例对象
三、饿汉模式
3.1、正常创建
- 构造器私有(外部无法直接new对象)
- 手动创建好一个类对象
- 提供方法给外部获取该对象
public class Hungry {
//构造器私有,外部无法直接new对象
private Hungry() {}
//手动创建静态单实例
private final static Hungry HUNGRY = new Hungry();
//给外部提供获取该单实例的方法
public static Hungry getInstance() {
return HUNGRY;
}
}
缺点:可能会浪费内存空间
private byte[] data1 = new byte[1024 * 1024];
private byte[] data2 = new byte[1024 * 1024];
private byte[] data3 = new byte[1024 * 1024];
private byte[] data4 = new byte[1024 * 1024];
3.2、静态内部类
public class Holder {
private Holder() {
}
private static Holder getInstance() {
return InnerClass.HOLDER;
}
public static class InnerClass {
private static final Holder HOLDER = new Holder();
}
}
四、懒汉模式
4.1、单线程使用
存在线程不安全的问题
public class Lazy {
private Lazy() {}
private static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
lazy = new Lazy();
}
return lazy;
}
}
如果两个线程同时判断singleton为空,那么它们都会去实例化一个Singleton对象,这就变成双例了。所以,我们要解决的是线程安全问题。
通过加锁解决
但如果直接在方法或者类上加锁,虽然解决多线程问题,但每次获取对象都要先获取锁,性能比较低
public static synchronized Lazy getInstance() {
if (lazy == null) {
lazy = new Lazy();
}
return lazy;
}
// 或者
public static Lazy getInstance() {
synchronized(Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
return lazy;
}
4.2、双重检测锁(DCL)
如果没有实例化对象则加锁创建,如果已经实例化了,则不需要加锁,直接获取实例
- 假设AB线程一起执行
getInstance()
,此时lazy为空,非空判断通过,接着AB争抢锁 - B抢到了并锁住,再次判断lazy为空(假设别的线程比B先进入,没有这层判断会导致B又创建了对象),创建对象
- 轮到A了,判断lazy不为空(没有这层判断会导致A又创建了对象),直接跳出,返回B创建的对象
- 假设C进来了,非空判断过不去,直接返回B创建的对象
- 如果没有第一层的判断会导致创建了对象后新线程又获取锁,性能降低
public class Lazy {
private Lazy() {}
private static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
}
解决了性能低效+并发安全的问题,但jvm创建对象的过程存在指令重排的现象
4.3、原子性操作
new对象不是原子性操作,内部执行以下
- 分配内存空间
- 执行构造方法,初始化对象
- 把对象指向内存空间
但jvm有可能出现指令重排(执行132)
- A线程执行了13,这个时候lazy的内存空间不等于null,但其实对象还未初始化
- B线程进来了,虽然B进不去同步代码块,但同步代码块前的空判断会判断为非空
- 此时lazy未完成构造,会直接return空对象
可以使用volatile
关键字防止指令重排的发生
public class Lazy {
private Lazy() {}
//防止初始化过程指令重排
private volatile static Lazy lazy;
public static Lazy getInstance() {
if (lazy == null) {
synchronized (Lazy.class) {
if (lazy == null) {
lazy = new Lazy();
}
}
}
return lazy;
}
}
五、反射与单例的斗争
5.1、反射破坏单例
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Lazy instance1 = Lazy.getInstance();
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
//无视私有构造
declaredConstructor.setAccessible(true);
Lazy instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
5.2、私有构造加锁–三重检测锁
但这种只能存在先获取了单例,再通过反射创建对象,如果对象都是反射创建的,这样也无法避免
private Lazy() {
synchronized (Lazy.class) {
if (lazy != null) {
throw new RuntimeException("请勿使用反射破坏单例");
}
}
}
5.3、反射创建对象
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//Lazy instance1 = Lazy.getInstance();
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
//无视私有构造
declaredConstructor.setAccessible(true);
Lazy instance2 = declaredConstructor.newInstance();
Lazy instance3 = declaredConstructor.newInstance();
System.out.println(instance2);
System.out.println(instance3);
}
5.4、私有构造加标记判断
private static boolean laptoy = false;
private Lazy() {
synchronized (Lazy.class) {
if (laptoy == false) {
laptoy = true;
} else {
throw new RuntimeException("请勿使用反射破坏单例");
}
}
}
5.5、若标记被找到
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//Lazy instance1 = Lazy.getInstance();
Field laptoy = Lazy.class.getDeclaredField("laptoy");
laptoy.setAccessible(true);
Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null);
//无视私有构造
declaredConstructor.setAccessible(true);
Lazy instance2 = declaredConstructor.newInstance();
laptoy.set(instance2, false);
Lazy instance3 = declaredConstructor.newInstance();
System.out.println(instance2);
System.out.println(instance3);
}
六、枚举(无法被反射破坏)
6.1、使用
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
class Test {
public static void main(String[] args) {
EnumSingle instance1 = EnumSingle.INSTANCE;
EnumSingle instance2 = EnumSingle.INSTANCE; //相同
}
}
6.2、验证
1、从生成的target目录发现EnumSingle源码类有私有构造
2、尝试用反射newInstance
class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
3、报错,没有此类型的构造器,也就是没有私有无参构造器
4、达不到预期,应该是抛出无法反射地创建枚举对象
5、反编译该类,的确有该构造,但是反射是绝对正确的
6、使用更专业的反编译工具,发现该类没有无参构造,但有一个有参构造
class Test {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
EnumSingle instance1 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
EnumSingle instance2 = declaredConstructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
达到预期效果,反射无法破坏单例