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