Java单例模式,看这一篇就够了

本文涉及的产品
系统运维管理,不限时长
简介: 单例模式总结,各种实现,各种破坏~

1、饿汉

1.1、饿汉式(静态常量,线程安全)

publicclassHungrySingle {
privatefinalstaticHungrySingleinstance=newHungrySingle();
privateHungrySingle() {
System.out.println(Thread.currentThread().getName());
    }
publicstaticHungrySinglegetInstance() {
returninstance;
    }
publicstaticvoidmain(String[] args) {
for (inti=0; i<100; i++) {
newThread((()-> {
HungrySingle.getInstance();
            })).start();
        }
    }
}

输出

Thread-0

步骤:

  1. 构造器私有
  2. 创建内部对象
  3. 写一个静态公共方法


优点:

  • 这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。


缺点:

  • 在类装载的时候就完成实例化,没有达到Lazy Loading 的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
  • 这种方式基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,在单例模式中大多数都是调用getInstance 方法, 但是导致类装载的原因有很多种,因此不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 就没有达到 lazy loading 的效果


这种单例模式可用,可能造成内存浪费。

1.2、饿汉式(静态代码块,线程安全)

publicclassHungrySingle {
privatefinalstaticHungrySingleinstance;
static {
instance=newHungrySingle();
    }
privateHungrySingle() {
System.out.println(Thread.currentThread().getName());
    }
publicstaticHungrySinglegetInstance() {
returninstance;
    }
publicstaticvoidmain(String[] args) {
for (inti=0; i<100; i++) {
newThread((()-> {
HungrySingle.getInstance();
            })).start();
        }
    }
}

输出

Thread-100

步骤:

  1. 构造器私有
  2. 创建内部对象
  3. 静态代码块实例化内部对象
  4. 写一个静态公共方法


优缺点同饿汉(静态常量)


2、懒汉

2.1、懒汉式(普通,线程不安全)

publicclassUnsafeLazySingle {
privatestaticUnsafeLazySingleinstance;
privateUnsafeLazySingle() {
System.out.println(Thread.currentThread().getName());
    }
publicstaticUnsafeLazySinglegetInstance() {
if (null==instance) {
instance=newUnsafeLazySingle();
        }
returninstance;
    }
publicstaticvoidmain(String[] args) {
for (inti=0; i<100; i++) {
newThread((()-> {
UnsafeLazySingle.getInstance();
            })).start();
        }
    }
}

输出

Thread-0Thread-3Thread-2Thread-1

优点:

  • 起到了 Lazy Loading 的效果,但是只能在单线程下使用

缺点:

  • 如果在多线程下,一个线程进入了 if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式


多线程不能用


2.2、懒汉式(同步方法,线程安全)

classLazySingleF{
privatestaticLazySingleFinstance;
privateLazySingleF(){
System.out.println(Thread.currentThread().getName());
    }
publicstaticsynchronizedLazySingleFgetInstance() {
if (instance==null) {
instance=newLazySingleF();
        }
returninstance;
    }
}

输出

Thread-0

优点:

  • 解决了线程安全问题


缺点:      

  • 效率太低了,每个线程在想获得类的实例时候,执行 getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return 就行了。


在实际开发中,不推荐使用这种方式


2.3、懒汉式(同步代码块,线程不安全)

classLazySingleB{
privatestaticLazySingleBinstance;
privateLazySingleB() {
System.out.println(Thread.currentThread().getName());
    }
publicstaticLazySingleBgetInstance() {
if (instance==null) {
synchronized (LazySingleB.class) {
instance=newLazySingleB();
            }
        }
returninstance;
    }
}

输出

Thread-4Thread-6Thread-1Thread-5Thread-2Thread-0Thread-3

缺点:

  • 不能线程同步。假如一个线程进入了if (instance == null),还未来得及执行,另一个线程也通过了这个判断语句,会产生多个实例
  • 效率很低


多线程不能使用

2.4、懒汉式(双重检查,线程安全)

Double Check Lock(DCL)

classLazySingleD{
privatevolatilestaticLazySingleDinstance;
privateLazySingleD() {
System.out.println(Thread.currentThread().getName());
    }
publicstaticLazySingleDgetInstance() {
if (null==instance) {
synchronized (LazySingleD.class) {
if (instance==null) {
instance=newLazySingleD();
                }
            }
        }
returninstance;
    }
}

输出

Thread-0

Double-Check Lock概念是多线程开发中常使用到的,如代码中所示,我们进行了两次if (singleton == null)检查,这样就可以保证线程安全了。

这样,实例化代码只用执行一次,后面再次访问时,判断 if (singleton == null),直接 return 实例化对象,也避免的反复进行方法同步.


注意:

由于jvm存在乱序执行功能,DCL也会出现线程不安全的情况。具体分析如下:

INSTANCE  = new SingleTon();

这个步骤,其实在jvm里面的执行分为三步:

  1. 在堆内存开辟内存空间。
  2. 在堆内存中实例化SingleTon里面的各个参数。
  3. 把对象指向堆内存空间。

由于jvm存在乱序执行功能,所以可能在2还没执行时就先执行了3,如果此时再被切换到线程B上,由于执行了3,INSTANCE 已经非空了,会被直接拿出来用,这样的话,就会出现异常。这个就是著名的DCL失效问题。


不过在JDK1.5之后,官方也发现了这个问题,故而具体化了volatile,即在JDK1.6及以后,只要定义为

private volatile static LazySingleD instance;

就可解决DCL失效问题。volatile的内存栅栏功能,告知编译器的在标记的变量前后不使用优化功能。


优点:

线程安全;延迟加载;效率较高


在实际开发中,推荐使用这种单例设计模式


3、内部静态类

3.1、具体实现

classInnerClass {
privateInnerClass() {
System.out.println(Thread.currentThread().getName());
    }
privatestaticclassInnerClassHolder {
privatestaticInnerClassinstance=newInnerClass();
    }
publicstaticInnerClassgetInstance() {
returnInnerClassHolder.instance;
    }
}

输出

Thread-0

外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。

即当InnerClass第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,才会去初始化INSTANCE,第一次调用getInstance()方法会导致虚拟机加载SingleTonHoler类,这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。


3.2、类加载

JAVA虚拟机在有且仅有的5种场景下会对类进行初始化。

  • 遇到new、getstatic、setstatic或者invokestatic这4个字节码指令时,对应的java代码场景为:
  • new一个关键字或者一个实例化对象时
  • 读取或设置一个静态字段时(final修饰、已在编译期把结果放入常量池的除外)
  • 调用一个类的静态方法时。
  • 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没进行初始化,需要先调用其初始化方法进行初始化。
  • 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
  • 当使用JDK  1.7等动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

以上是类的主动引用。除此之外,所有引用类都不会对类进行初始化,称为被动引用。


静态内部类就属于被动引用的行列。

为什么?

我们再回头看下getInstance()方法,调用的是InnerClassHolder.instance,取的是InnerClassHolder里的instance对象,跟上面那个DCL方法不同的是,getInstance()方法并没有多次去new对象,故不管多少个线程去调用getInstance()方法,取的都是同一个instance对象,而不用去重新创建。当getInstance()方法被调用时,InnerClassHolder才在InnerClass的运行时常量池里,把符号引用替换为直接引用,这时静态对象instance也真正被创建,然后再被getInstance()方法返回出去,这点同饿汉模式。


那么instance在创建过程中又是如何保证线程安全的呢?

虚拟机会保证一个类的()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕。如果在一个类的()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行()方法后,其他线程唤醒之后不会再次进入()方法。同一个加载器下,一个类型只会初始化一次。

可以看出INSTANCE在创建过程中是线程安全的,所以说静态内部类形式的单例可保证线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。


静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去。


4、枚举

enumEnums {
/*** 实例对象*/INSTANCE;
}

输出:

 
         

并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在“底层”还是做了线程安全方面的保证的。

图片.png

5、破坏单例模式的方法及解决办法

5.1、反射

除枚举方式外, 其他方法都会通过反射的方式破坏单例,反射是通过调用构造方法生成新的对象,所以如果我们想要阻止单例破坏,可以在构造方法中进行判断,若已有实例, 则阻止生成新的实例

publicstaticvoidreflect() {
try {
HungrySingleCsingleC1=HungrySingleC.getInstance();
Constructor<HungrySingleC>constructor=HungrySingleC.class.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleCsingleC2=constructor.newInstance();
System.out.println(singleC1.hashCode() ==singleC2.hashCode());
        } catch (Throwablee) {
e.printStackTrace();
        }
    }

输出

false

解决办法:

privateHungrySingleC() {
if (instance==null) {
thrownewRuntimeException("已存在");
        }
System.out.println(Thread.currentThread().getName());
    }

反射试图破坏枚举:

publicstaticvoidreflect() {
try {
Enumse1=Enums.INSTANCE;
Constructor<Enums>constructor=Enums.class.getDeclaredConstructor();
constructor.setAccessible(true);
Enumse2=constructor.newInstance();
System.out.println(e1.hashCode() ==e2.hashCode());
    } catch (Throwablee) {
e.printStackTrace();
    }
}

输出

java.lang.NoSuchMethodException: cn.edu..pattern.single.Enums.<init>()
atjava.base/java.lang.Class.getConstructor0(Class.java:3355)
atjava.base/java.lang.Class.getDeclaredConstructor(Class.java:2559)
atcn.edu..pattern.single.EnumSingle.reflect(EnumSingle.java:19)
atcn.edu..pattern.single.EnumSingle.main(EnumSingle.java:35)

源码

publicabstractclassEnum<EextendsEnum<E>>implementsConstable, Comparable<E>, Serializable {
privatefinalStringname;
privatefinalintordinal;
protectedEnum(Stringname, intordinal) {
this.name=name;
this.ordinal=ordinal;
    }
}

枚举Enum是个抽象类,其实一旦一个类声明为枚举,实际上就是继承了Enum,所以会有(String.class,int.class)的构造器。既然是可以获取到父类Enum的构造器,那你也许会说刚才我的反射是因为自身的类没有无参构造方法才导致的异常,并不能说单例枚举避免了反射攻击。好的,那我们就使用父类Enum的构造器,看看是什么情况:

publicstaticvoidreflect() {
try {
Enumse1=Enums.INSTANCE;
Constructor<Enums>constructor=Enums.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
Enumse2=constructor.newInstance();
System.out.println(e1.hashCode() ==e2.hashCode());
    } catch (Throwablee) {
e.printStackTrace();
    }
}

输出

java.lang.IllegalArgumentException: Cannotreflectivelycreateenumobjectsatjava.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:493)
atjava.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
atcn.edu..pattern.single.EnumSingle.reflect(EnumSingle.java:21)
atcn.edu..pattern.single.EnumSingle.main(EnumSingle.java:35)

Constructor类的newInstance方法源码:

@CallerSensitive@ForceInline// to ensure Reflection.getCallerClass optimizationpublicTnewInstance(Object ... initargs)
throwsInstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException    {
Class<?>caller=override?null : Reflection.getCallerClass();
returnnewInstanceWithCaller(initargs, !override, caller);
    }
/* package-private */TnewInstanceWithCaller(Object[] args, booleancheckAccess, Class<?>caller)
throwsInstantiationException, IllegalAccessException,
InvocationTargetException    {
if (checkAccess)
checkAccess(caller, clazz, clazz, modifiers);
if ((clazz.getModifiers() &Modifier.ENUM) !=0)
thrownewIllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessorca=constructorAccessor;   // read volatileif (ca==null) {
ca=acquireConstructorAccessor();
        }
@SuppressWarnings("unchecked")
Tinst= (T) ca.newInstance(args);
returninst;
    }

反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。

5.2、序列化

如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。


在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过java.lang.Enum的valueOf方法来根据名字查找枚举对象。同时,编译器是不允许任何对这种序列化机制的定制的,因此禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。


普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。


但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。

publicstaticvoidserializable() {
try {
LazySingleDd=LazySingleD.getInstance();
ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("LazySingleD.obj"));
oos.writeObject(d);
oos.flush();
oos.close();
FileInputStreamfis=newFileInputStream("LazySingleD.obj");
ObjectInputStreamois=newObjectInputStream(fis);
LazySingleDd2= (LazySingleD) ois.readObject();
ois.close();
System.out.println(d.hashCode() ==d2.hashCode());
        } catch (Throwablee) {
e.printStackTrace();
        }
    }

输出

false

序列化试图破坏枚举

publicstaticvoidserializable() {
try {
EnumSerializablee=EnumSerializable.INSTANCE;
ObjectOutputStreamoos=newObjectOutputStream(newFileOutputStream("EnumSerializable.obj"));
oos.writeObject(e);
oos.flush();
oos.close();
FileInputStreamfis=newFileInputStream("EnumSerializable.obj");
ObjectInputStreamois=newObjectInputStream(fis);
EnumSerializablee2= (EnumSerializable) ois.readObject();
ois.close();
System.out.println(e.hashCode() ==e2.hashCode());
        } catch (Throwablee) {
e.printStackTrace();
        }
    }

输出

true

在序列化方面,Java中有明确规定,枚举的序列化和反序列化是有特殊定制的。这就可以避免反序列化过程中由于反射而导致的单例被破坏问题。

目录
相关文章
|
5天前
|
设计模式 安全 Java
Java编程中的单例模式深入剖析
【10月更文挑战第21天】在Java的世界里,单例模式是设计模式中一个常见而又强大的存在。它确保了一个类只有一个实例,并提供一个全局访问点。本文将深入探讨如何正确实现单例模式,包括常见的实现方式、优缺点分析以及最佳实践,同时也会通过实际代码示例来加深理解。无论你是Java新手还是资深开发者,这篇文章都将为你提供宝贵的见解和技巧。
89 65
|
2天前
|
设计模式 SQL 安全
Java编程中的单例模式深入解析
【10月更文挑战第24天】在软件工程中,单例模式是设计模式的一种,它确保一个类只有一个实例,并提供一个全局访问点。本文将探讨如何在Java中使用单例模式,并分析其优缺点以及适用场景。
6 0
|
6天前
|
SQL 设计模式 Java
[Java]单例模式
本文介绍了单例模式的概念及其实现方式,包括饿汉式和懒汉式两种形式,并详细探讨了懒汉式中可能出现的线程安全问题及其解决方案,如锁方法、锁代码块和双重检查锁(DCL)。文章通过示例代码帮助读者更好地理解和应用单例模式。
20 0
|
2月前
|
设计模式 安全 Java
Java 编程中的设计模式:单例模式的深度解析
【9月更文挑战第22天】在Java的世界里,单例模式就像是一位老练的舞者,轻盈地穿梭在对象创建的舞台上。它确保了一个类仅有一个实例,并提供全局访问点。这不仅仅是代码优雅的体现,更是资源管理的高手。我们将一起探索单例模式的奥秘,从基础实现到高级应用,再到它与现代Java版本的舞蹈,让我们揭开单例模式的面纱,一探究竟。
36 11
|
14天前
|
设计模式 SQL 安全
【编程进阶知识】Java单例模式深度解析:饿汉式与懒汉式实现技巧
本文深入解析了Java单例模式中的饿汉式和懒汉式实现方法,包括它们的特点、实现代码和适用场景。通过静态常量、枚举类、静态代码块等方式实现饿汉式,通过非线程安全、同步方法、同步代码块、双重检查锁定和静态内部类等方式实现懒汉式。文章还对比了各种实现方式的优缺点,帮助读者在实际项目中做出更好的设计决策。
26 0
|
3月前
|
设计模式 存储 负载均衡
【五】设计模式~~~创建型模式~~~单例模式(Java)
文章详细介绍了单例模式(Singleton Pattern),这是一种确保一个类只有一个实例,并提供全局访问点的设计模式。文中通过Windows任务管理器的例子阐述了单例模式的动机,解释了如何通过私有构造函数、静态私有成员变量和公有静态方法实现单例模式。接着,通过负载均衡器的案例展示了单例模式的应用,并讨论了单例模式的优点、缺点以及适用场景。最后,文章还探讨了饿汉式和懒汉式单例的实现方式及其比较。
【五】设计模式~~~创建型模式~~~单例模式(Java)
|
2月前
|
设计模式 Java 安全
Java设计模式-单例模式(2)
Java设计模式-单例模式(2)
|
3月前
|
设计模式 安全 Java
Java 单例模式,背后有着何种不为人知的秘密?开启探索之旅,寻找答案!
【8月更文挑战第30天】单例模式确保一个类只有一个实例并提供全局访问点,适用于需全局共享的宝贵资源如数据库连接池、日志记录器等。Java中有多种单例模式实现,包括饿汉式、懒汉式、同步方法和双重检查锁定。饿汉式在类加载时创建实例,懒汉式则在首次调用时创建,后者在多线程环境下需使用同步机制保证线程安全。单例模式有助于提高代码的可维护性和扩展性,应根据需求选择合适实现方式。
33 1
|
3月前
|
SQL 设计模式 安全
Java编程中的单例模式深入解析
【8月更文挑战第27天】本文旨在探索Java中实现单例模式的多种方式,并分析其优缺点。我们将通过代码示例,展示如何在不同的场景下选择最合适的单例模式实现方法,以及如何避免常见的陷阱。
|
3月前
|
设计模式 安全 Java
Java编程中的单例模式深度解析
【8月更文挑战第31天】 单例模式,作为设计模式中的经典之一,在Java编程实践中扮演着重要的角色。本文将通过简洁易懂的语言,逐步引导读者理解单例模式的本质、实现方法及其在实际应用中的重要性。从基础概念出发,到代码示例,再到高级应用,我们将一起探索这一模式如何优雅地解决资源共享和性能优化的问题。