单例模式常用写法与弊端
所谓单例,就是整个程序有且仅有一个实例。该类负责创建自己的对象,同时确保只有一个对象被创建。在Java,一般常用在工具类的实现或创建对象需要消耗资源。
特点:
- 类构造器私有
- 持有自己类型的属性
- 对外提供获取实例的静态方法
单例模式写法共分为以下几类
- 1、饿汉式
- 2、懒汉式
- 3、静态内部类
- 4、枚举类
1、饿汉式
顾名思义,饿汉,我很饿,需要一开始就创建,实例代码如下:
/**
* 饿汉单例模式
* 缺点:类初始化就加载信息,类中可能存在比较大的对象,浪费性能
*
* @author lixiang
* @version V1.0
* @date 2020/3/16 11:24
**/
public class Hungry {
private byte[] data1 = new byte[10240];
private byte[] data2 = new byte[10240];
private byte[] data3 = new byte[10240];
private byte[] data4 = new byte[10240];
/**
* 类初始化直接创建对象
*/
private static Hungry hungry = new Hungry();
/**
* 私有构造方法
*/
private Hungry() {
}
/**
* 共有获取实例对象方法
*
* @return 初始化的实例对象
*/
public static Hungry getInstance() {
return hungry;
}
}
缺点也很明显,当类被加载时就直接实例化类,如果此类中存在一些比较大的对象,这些对象就会直接被加载浪费性能。
2、懒汉式
顾名思义,我很懒,需要我的时候我在创建,
/**
* 懒汉单例模式
* 缺点 多线程下 对象并不是单例的
*
* @author lixiang
* @version V1.0
* @date 2020/3/16 11:33
**/
public class LazyMan {
private static LazyMan lazyMan;
private LazyMan() {
}
private static LazyMan getInstance() {
if (lazyMan == null) {
lazyMan = new LazyMan();
}
return lazyMan;
}
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + LazyMan.getInstance());
}, String.valueOf(i)).start();
}
}
}
缺点,并发情况下,对象无法保证单例,违背初衷!(执行代码中的main方法即可破解)
2.1 懒汉式(DCL)
由于上一版本懒汉模式在多线程情况下无法保证单例。因此采用锁进行处理。但是只使用synchronized会出现部分构造的现象,加入volatile也可以避免指令重排。实例代码如下:
/**
* 懒汉单例模式 DCL(双重校验锁)
* 反射可以破坏单例
*
* @author lixiang
* @version V1.0
* @date 2020/3/16 11:33
**/
public class LazyMan2 {
private volatile static LazyMan2 lazyMan;
private LazyMan2() {
}
private static LazyMan2 getInstance() {
if (lazyMan == null) {
synchronized (LazyMan.class) {
if (lazyMan == null) {
lazyMan = new LazyMan2();
}
}
}
return lazyMan;
}
public static void main(String[] args) {
try {
invokeClass();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void invokeClass() throws Exception {
Constructor<LazyMan2> declaredConstructor = LazyMan2.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
LazyMan2 lazyMan2 = declaredConstructor.newInstance();
LazyMan2 lazyMan21 = declaredConstructor.newInstance();
System.out.println(lazyMan2);
System.out.println(lazyMan21);
}
}
你认为这样就可以了么?如果是按照正常思路,这个获取的对象肯定是满足要求的,但是如果使用反射就无法保证反射的对象单例了(详细见main方法)
3、静态内部类
利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。
/**
* 静态内部类
*
* @author lixiang
* @version V1.0
* @date 2020/3/16 13:39
**/
public class Holder {
private Holder(){};
private static Holder getInstance(){
return InnerClass.holder;
}
private static class InnerClass{
private static final Holder holder = new Holder();
}
}
4、枚举类
利用枚举对象每个实体都是单例的原理,引发出来了最简单的单例模式。
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
}
可能会有人疑问,枚举类可以利用反射构造方法进行创建么?可以看一下Constructor.newInstance(Object … initargs)源码,截图如下:
我们发现,Constructor.newInstance方法源码中class做了判断,如果是枚举类型就会抛出Cannot reflectively create enum objects异常。
作为一个开发人员,不自己尝试一下怎么能够让你信服。
尝试利用反射破解枚举类的单例
下面我们利用反射尝试操作一下,使其出现Cannot reflectively create enum objects异常。
/**
* 枚举
*
* @author lixiang
* @version V1.0
* @date 2020/3/16 13:47
**/
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws Exception {
EnumSingle enumSingle2 = EnumSingle.INSTANCE;
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
// 期望的异常 throw new IllegalArgumentException("Cannot reflectively create enum objects");
// java.lang.NoSuchMethodException: com.coding.single.EnumSingle.<init>()
declaredConstructor.newInstance();
}
}
执行后发现运行结果是NoSuchMethodException,遇到这里发现我们实际出现的异常与预期的异常不符合,我们需要进行深入分析,实际异常信息如下:
Exception in thread "main" java.lang.NoSuchMethodException: com.thislx.juc.single.EnumSingle.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.thislx.juc.single.EnumSingle.main(EnumSingle.java:23)
这时候我们要考虑是不是枚举类没有默认无参构造方法呀,我们使用idea开发工具进行查看。发现存在无参构造方法。如下图所示:
这是什么情况?我们在使用javap命令进行查看,发现依旧存在无参构造方法。如下图所示:
中途放弃不是一个合格的开发人员,我们使用第三方编辑工具jad进行class的反编译。
使用如下命令:
jad -sjava EnumSingle.class
打开EnumSingle.java,看到源代码的我们应该一目了然,原来枚举类只有一个
private EnumSingle(String s, int i) 有参构造方法。
代码如下:
// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)
// Source File Name: EnumSingle.java
package com.thislx.juc.single;
import java.lang.reflect.Constructor;
public final class EnumSingle extends Enum
{
public static EnumSingle[] values()
{
return (EnumSingle[])$VALUES.clone();
}
public static EnumSingle valueOf(String name)
{
return (EnumSingle)Enum.valueOf(com/thislx/juc/single/EnumSingle, name);
}
private EnumSingle(String s, int i)
{
super(s, i);
}
public EnumSingle getInstance()
{
return INSTANCE;
}
public static void main(String args[])
throws Exception
{
EnumSingle enumSingle2 = INSTANCE;
Constructor declaredConstructor = com/thislx/juc/single/EnumSingle.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
declaredConstructor.newInstance(new Object[0]);
}
public static final EnumSingle INSTANCE;
private static final EnumSingle $VALUES[];
static
{
INSTANCE = new EnumSingle("INSTANCE", 0);
$VALUES = (new EnumSingle[] {
INSTANCE
});
}
}
到此我们再次修改我们尝试反射枚举的main方法,修改点:在获取构造方法时传入String.class和int.class。修改后代码如下:
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance() {
return INSTANCE;
}
public static void main(String[] args) throws Exception {
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
// 期望的异常 throw new IllegalArgumentException("Cannot reflectively create enum objects");
declaredConstructor.newInstance();
}
}
运行项目,发现终于出现了Cannot reflectively create enum objects,错误信息,这足以证明枚举实现单例模式是最安全的,无法被多线程、反射破坏