彻底玩转单例模式
Posted Baret-H
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了彻底玩转单例模式相关的知识,希望对你有一定的参考价值。
- 饿汉式:类加载时初始化,不存在并发访问问题,会有资源浪费
- 懒汉式:延时加载,使用时才实例化对象,存在并发访问问题,资源利用率高
- 双重检测锁式 :利用sychronized关键字解决了懒汉式并发访问问题,同时为了解决指令重排问题使用了volatile关键字
- 静态内部类式:兼并并发高效调用和延迟加载的优势
- 枚举单例:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!但是不能延时加载。
1. 饿汉式
package 单例模式;
//饿汉式
public class Hungry {
private static Hungry hungry = new Hungry();
//构造器私有
private Hungry() {
}
public static Hungry getInstance() {
return hungry;
}
}
优点:static变量会在类装载时初始化,不涉及多个线程访问该对象的问题,可以省略synchronized关键字
缺点:类初始化时就创建了对象,如果只是加载本类,而不是要调用 getInstance(),甚至永远没有调用,则会造成资源浪费!
2. 懒汉式
package 单例模式;
//懒汉式
public class Lazy {
private static Lazy lazy;
private Lazy() {
}
public static Lazy getInstance() {
if (lazy == null)
lazy = new Lazy();
return lazy;
}
}
优点:延迟加载,真正用的时候才实例化对象,提高了资源的利用率
缺点:存在并发访问的问题,以下测试并发访问情况
package 单例模式;
//懒汉式
public class Lazy {
private static Lazy lazy;
private Lazy() {
System.out.println("创建示例");
}
public static Lazy getInstance() {
if (lazy == null)
lazy = new Lazy();
return lazy;
}
public static void main(String[] args) {
//10条线程并发访问下
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Lazy.getInstance();
}).start();
}
}
}
根据结果,可以看到有5个线程打印了结果,也就说进行了5次初始化,这是非常大的漏洞,出现了并发访问的问题
3. 双重检测锁式
为了解决懒汉式并发访问的问题,加入了sychronized
关键字
package 单例模式;
//双重检测锁式
public class DoubleLock {
private static DoubleLock doubleLock;
private DoubleLock() {
System.out.println("创建示例");
}
public static DoubleLock getInstance() {
if (doubleLock == null) {
synchronized (Lazy.class) {
if (doubleLock == null)
doubleLock = new DoubleLock();
}
}
return doubleLock;
}
public static void main(String[] args) {
//10条线程并发访问下
for (int i = 0; i < 10; i++) {
new Thread(() -> {
DoubleLock.getInstance();
}).start();
}
}
}
根据打印结果,解决了并发访问的问题;但是这样仍然会存在问题,因为我们new
对象时并不是一个完整的原子性操作,而是分为以下三部:
- 分配内存空间
- 执行构造方法,初始化对象
- 把这个对象指向这个空间
单个线程A执行的情况下可以123按顺序执行,也可能由于指令重排按132执行;但是如果线程A按132顺序执行到3时来了一个线程B,此时该对象已经指向了分配的空间,因此B判断对象不是null,就会直接返回对象,但其实对象并没有进行初始化,就造成了错误
因此指令重排也会导致错误,因此完整的双重检测锁式
还加入了Volatile
关键字来避免指令重排,完整代码如下:
package 单例模式;
//双重检测锁式
public class DoubleLock {
private volatile static DoubleLock doubleLock;
private DoubleLock() {
System.out.println("创建示例");
}
public static DoubleLock getInstance() {
if (doubleLock == null) {
synchronized (Lazy.class) {
if (doubleLock == null)
doubleLock = new DoubleLock();
}
}
return doubleLock;
}
}
4. 静态内部类式
package 单例模式;
public class InnerClass {
private InnerClass() {
}
//静态内部类里面创建对象
public static class inner {
private static final InnerClass innerClass = new InnerClass();
}
public static InnerClass getInstance() {
return inner.innerClass;
}
}
- 延时加载,只有真正调用getinstance(),才会加载静态内部类。
- 线程安全的,Instance是
static final
类型,保证了内存中只有这样一个实例存在,而且只能被赋值一次,从而保证了线程安全性。 - 兼备了并发高效调用和延迟加载的优势
— 反射破坏单例模式,引入枚举单例
以下通过反射对双重检测锁式单例进行破坏
package 单例模式;
import java.lang.reflect.Constructor;
//双重检测锁式
public class DoubleLock {
private volatile static DoubleLock doubleLock;
private DoubleLock() {
System.out.println("创建示例");
}
public static DoubleLock getInstance() {
if (doubleLock == null) {
synchronized (Lazy.class) {
if (doubleLock == null)
doubleLock = new DoubleLock();
}
}
return doubleLock;
}
public static void main(String[] args) throws Exception {
DoubleLock instance1 = doubleLock.getInstance();
Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
DoubleLock instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
根据结果,看到创建了两个实例,也就是单例模式被破坏,那么怎么解决呢?
可以在私有构造中加锁
package 单例模式;
import java.lang.reflect.Constructor;
//双重检测锁式
public class DoubleLock {
private volatile static DoubleLock doubleLock;
private DoubleLock() {
synchronized (DoubleLock.class){
if(doubleLock!=null){
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
System.out.println("创建示例");
}
public static DoubleLock getInstance() {
if (doubleLock == null) {
synchronized (Lazy.class) {
if (doubleLock == null)
doubleLock = new DoubleLock();
}
}
return doubleLock;
}
public static void main(String[] args) throws Exception {
DoubleLock instance1 = doubleLock.getInstance();
Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
DoubleLock instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
根据结果,可以看到避免了单例模式的破坏?可是上述两个对象一个是通过单例获取,一个通过反射获取;
那如果两个对象都是通过反射获取呢?
public static void main(String[] args) throws Exception {
Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
DoubleLock instance1= constructor.newInstance();
DoubleLock instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
根据结果,可以看到单例模式又被破坏了,创建了两个对象!这种情况如何解决呢?
可以通过红绿灯方法实现,定义一个标志位记录对象是否创建
package 单例模式;
import java.lang.reflect.Constructor;
//双重检测锁式
public class DoubleLock {
private volatile static DoubleLock doubleLock;
//标志位
private static boolean flag = false;
private DoubleLock() {
synchronized (DoubleLock.class) {
if (flag == false)
flag = true;
else
throw new RuntimeException("不要试图使用反射破坏异常");
}
System.out.println("创建示例");
}
public static DoubleLock getInstance() {
if (doubleLock == null) {
synchronized (Lazy.class) {
if (doubleLock == null)
doubleLock = new DoubleLock();
}
}
return doubleLock;
}
public static void main(String[] args) throws Exception {
Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
constructor.setAccessible(true);
DoubleLock instance1 = constructor.newInstance();
DoubleLock instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
}
可以看到我们通过设置标志位flag
再次解决了这个问题,但是一旦被获取了这个关键字,单例模式仍然可以通过反射被破解,如下所示
public static void main(String[] args) throws Exception {
Constructor<DoubleLock> constructor = DoubleLock.class.getDeclaredConstructor(null);
Field declaredField = DoubleLock.class.getDeclaredField("flag");
constructor.setAccessible(true);
declaredField.setAccessible(true);
DoubleLock instance1 = constructor.newInstance();
declaredField.set(instance1, false);//第一个对象创建完毕后将flag改为false
DoubleLock instance2 = constructor.newInstance();
System.out.println(instance1);
System.out.println(instance2);
}
可以看到单例模式再次被破坏;因此为了让程序更加安全,通常对flag
关键字进行加密处理
那么到底如何完全的避免反射破坏单例模式呢?我们查看newInstance
的源码
可以看到,如果是枚举类型的话,就不能通过反射获取枚举;
因此引入了第5种单例模式
5. 枚举单例
-
优点:实现简单,枚举本身就是单例模式。由JVM从根本上提供保障!避免通过反射和反序列化的漏洞!
-
缺点:无延迟加载
package 单例模式;
import java.lang.reflect.Constructor;
//enum本质上就是一个Class类
public enum EnumSingle {
INSTANCE;
public static void main(String[] args) throws Exception {
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);
}
}
我们再次通过反射创建对象,根据结果报错没有EnumSingle的空构造方法,这不是我们希望看到的
我们对EnumSingle
的class文件进行反编译,可以看到明明有空构造方法
但是执行明明报错没有无参构造,我们使用更专业的反编译工具jad
对class文件再进行反编译
可以看到枚举类本质上就是继承了Enum
类,本身就是一个Class,而且没有无参构造,而是含两个参数的有参构造,我们修改代码在测试
public static void main(String[] args) throws Exception {
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);
}
这才正确显示了报错的信息:无法反射地创建枚举对象
以上是关于彻底玩转单例模式的主要内容,如果未能解决你的问题,请参考以下文章