(一)单例模式强化版

简介

单例模式是Java中最简单的设计模式之一。这种设计模式属于创建模式,该模式提供了创建对象的最佳方法之一。
此模式涉及单个类,该类负责创建对象,同时确保仅创建单个对象。此类提供了一种访问其唯一对象的方法,该对象可以直接访问而无需实例化该类的对象。

最简单!!!

饿汉式

代码案例
public class Hunger {
	private Hunger() {
		System.out.println("new instance.");
		byte[] data = new byte[1024 * 1024 * 100];// ① 100MB
	}
	private static Hunger singleton = new Hunger();
	public static Hunger getInstance() {
		return singleton;
	}
	public static boolean isHunger() {// ②
		return true;
	}
}
解析

饿汉式单例模式在通常使用时是没有问题的。但是如果单例方法中存在静态方法,或者静态变量。并且被其他类调用。那么会导致单例类被加载、初始化。由于单例对象singleton是静态的,所以也会被初始化。此时,注释处的代码会导致内存被提前占用。
在这里插入图片描述

懒汉式

  • 单线程环境
public class Lazy {
	private Lazy() {
		System.out.println("new lazy.");
	}

	private static Lazy singleton;

	public static Lazy getInstance() {
		if (singleton == null) {
			singleton = new Lazy();
		}
		return singleton;
	}
}

上面是一个懒汉式的单例模式。当需要时才去实例化。
在单线程环境下是没有问题的,当多线程时会出现问题,会实例化多个对象。
多线程运行效果如下:

public class App {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			new Thread(() -> {
				Lazy.getInstance();
			}).start();
		}
	}
}
new lazy.
new lazy.
new lazy.
new lazy.
new lazy.
new lazy.
new lazy.
new lazy.
new lazy.
new lazy.
  • 多线程环境
    既然上面的单例无法处理并发问题。那么我们就需要改进。改进代码如下:
public class Lazy {
	private Lazy() {
		System.out.println("new lazy.");
	}

	private static volatile Lazy singleton;

	public static Lazy getInstance() {
		synchronized (Lazy.class) {// ①
			if (singleton == null) {
				singleton = new Lazy();
			}
		}
		return singleton;
	}
	
}

在注释处加锁,这样就只能有一个线程进入锁代码块进行实例化操作。同时呢为了避免指令重排,我们使用volatile关键字修饰单例变量。但是这样处理的话性能不好,因为每次获取单例都要加锁、释放锁。
提高性能代码如下:

public class Lazy {
	private Lazy() {
		System.out.println("new lazy.");
	}

	private static volatile Lazy singleton;

	public static Lazy getInstance() {
		if (singleton == null) {// ①
			synchronized (Lazy.class) {
				if (singleton == null) {
					singleton = new Lazy();
				}
			}
		}
		return singleton;
	}
}

在注释处进行判断,这样可以有效的避免锁操作。

静态内部类单例模式

public class Inner {

	private Inner() {
		System.out.println("new Inner.");
	}

	private static class InnerInstance {
		private static Inner singleton = new Inner();
	}

	public static Inner getInstance() {
		return InnerInstance.singleton;
	}
}

通过静态内部类可以实现延迟加载,并且使用static关键词修饰单例可以保证线程安全。

你以为到这里就完事了吗。。。。。。。
当然不是,上面的单例都存在很大的漏洞。我们就拿懒汉式单例举例:

单例漏洞

  • 漏洞一:反射创建单例。
public class App {

	public static void main(String[] args) throws Exception {
		Class<Lazy> lazyClass = Lazy.class;
		Constructor<Lazy> constructor = lazyClass.getDeclaredConstructor();
		constructor.setAccessible(true);

		Lazy lazy = (Lazy) constructor.newInstance();
		System.out.println(lazy);
		lazy = (Lazy) constructor.newInstance();
		System.out.println(lazy);
	}
}

运行结果

new lazy.
com.csdn.设计模式.单例.Lazy@15db9742
new lazy.
com.csdn.设计模式.单例.Lazy@6d06d69c

无解!

  • 漏洞二:克隆,当然需要单例类实现Cloneable接口,否则会抛异常。不实现就没有这个漏洞了。
	public static void main(String[] args) throws CloneNotSupportedException {
		Lazy lazy = getInstance();
		Lazy clone = (Lazy) lazy.clone();
		System.out.println(lazy);
		System.out.println(clone);
	}

运行结果

new lazy.
com.csdn.设计模式.单例.Lazy@70dea4e
com.csdn.设计模式.单例.Lazy@119d7047

解决方案:重写clone()方法,返回单例实例。

	@Override
	protected Object clone() throws CloneNotSupportedException {
		return getInstance();
	}
  • 漏洞三:序列化,需要单例类实现Serializable接口,否则序列化失败。不实现也就没有这个漏洞了。
public static void main(String[] args) throws CloneNotSupportedException, IOException, ClassNotFoundException {
		Lazy lazy = getInstance();

		ObjectInputStream in = null;
		try (ByteArrayOutputStream arrayOut = new ByteArrayOutputStream();
				ObjectOutputStream out = new ObjectOutputStream(arrayOut)) {
			out.writeObject(lazy);// 将单例对象写入字节数组
			in = new ObjectInputStream(new ByteArrayInputStream(arrayOut.toByteArray()));
			Lazy readLazy = (Lazy) in.readObject();// 从字节数组读取对象
			System.out.println(lazy);
			System.out.println(readLazy);
		} catch (Exception e) {
			// TODO: handle exception
		} finally {
			if (in != null) {
				in.close();
			}
		}
	}

运行结果

new lazy.
com.csdn.设计模式.单例.Lazy@70dea4e
com.csdn.设计模式.单例.Lazy@119d7047

解决方案:创建一个readResolve()方法,返回单例实例。

private Object readResolve() {
	return getInstance();
}

通过以上方式我们可以对单例模式进行破坏,并且提供了解决方案。对于反射漏铜无法提供可行的方案,无论加标志位还是其他校验逻辑,都可以 通过反射来处理。
如果有解决方案,欢迎在评论区留言。
接下来我们看下枚举是如何实现单例的。天然的单例。

枚举

public enum EnumSingleton {

	SINGLETON;

	private EnumSingleton() {
		System.out.println("new Instance.");
	}

}

以下是枚举单例的反编译源码:使用jad工具进行反编译。

// 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:   EnumSingleton.java

package com.csdn;

import java.io.PrintStream;

public final class EnumSingleton extends Enum
{

    private EnumSingleton(String s, int i)
    {
        super(s, i);
        System.out.println("new Instance.");
    }

    public static EnumSingleton[] values()
    {
        EnumSingleton aenumsingleton[];
        int i;
        EnumSingleton aenumsingleton1[];
        System.arraycopy(aenumsingleton = ENUM$VALUES, 0, aenumsingleton1 = new EnumSingleton[i = aenumsingleton.length], 0, i);
        return aenumsingleton1;
    }

    public static EnumSingleton valueOf(String s)
    {
        return (EnumSingleton)Enum.valueOf(com/csdn/EnumSingleton, s);
    }

    public static final EnumSingleton SINGLETON;
    private static final EnumSingleton ENUM$VALUES[];

    static 
    {
        SINGLETON = new EnumSingleton("SINGLETON", 0);
        ENUM$VALUES = (new EnumSingleton[] {
            SINGLETON
        });
    }
}

从反编译源码可以发现,枚举继承了Enum类,并且动态生成了一些代码。构造方法都已经修改过。
我们再来看Enum类的源码

  • 重写克隆方法
    /**
     * Throws CloneNotSupportedException.  This guarantees that enums
     * are never cloned, which is necessary to preserve their "singleton"
     * status.
     * 翻译:抛出CloneNotSupportedException. 保证了枚举永远不会被克隆,这是保持枚举单例的必要条件。
     * @return (never returns)
     */
    protected final Object clone() throws CloneNotSupportedException {
        throw new CloneNotSupportedException();
    }
  • 重写序列化读取方法
    /**
     * prevent default deserialization
     * 防止违约反序列化,保证了枚举的单例。
     */
    private void readObject(ObjectInputStream in) throws IOException,
        ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }

    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }

我们再来看反射源码:java.lang.reflect.Constructor

    @CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        // 此处进行判断,如果是枚举修饰,抛出异常:不能反射地创建枚举对象
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

综上所述:我们就看到了枚举是如何处理单例中漏洞的。
所以单例模式直接用枚举最省事、放心。
自己写需要考虑延迟加载、并发、性能、防反射、防克隆,防序列化。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值