目的:
一个类的实例在系统中只有一个,从而实现减少垃圾对象的产生,从而提高效率。
应用场景:
工具类
实现方式:
说在开头,下述的所有实现方式我们按如下思路推进。
首先最简单的实现方式饿汉方式,但饿汉方式在类加载的时候就创建了单例对象,这样有点浪费内存;
进而改造为懒汉模式一,懒汉模式一是在第一次使用单例对象时才创建,解决了饿汉模式的缺点(浪费内存),但是懒汉模式一没有实现获取单例对象函数的线程安全性,所以有了懒汉模式二,其对获取单例对象的函数使用synchronize进行加锁,这样虽然解决了懒汉模式一的线程不安全问题,但是synchronize锁太重造成每次调用获取单例对象函数都是同步的,效率低下;
再进而通过双检查锁解决懒汉模式二锁太重的问题,双检查锁实现的方式是只有在单例对象为null的时候才会锁,如果已经创建好了单例对象则不需要锁,双检查锁有个指令重排的问题,所以引入了关键字volatile来避免指令重排,解决单例对象的引用已经不是null,但指向的单例对象还没创建好而造成的问题,更详细的说明见代码注释。
创建一个 Java 对象一般有 4 种方式:new 、克隆、序列化、反射,将构造函数用private修饰后,new关键字就不能用来创建对象了,那么还剩下另外三种,所以单例模式还存在三种攻击:①克隆和②序列化攻击及③反射攻击;枚举单例很好的解决了这三种攻击问题。所以目前来说枚举是单例的最好实践。
饿汉模式
package com.cld.designpattern.creation.singleton.hungry;
import java.io.Serializable;
/**
* 饿汉式
* 是否 Lazy 初始化:否
* <p>
* 是否多线程安全:是
* <p>
* 实现难度:易
* <p>
* 描述:这种方式比较常用,但容易产生垃圾对象。
* 优点:没有加锁,执行效率会提高。
* 缺点:类加载时就初始化,浪费内存。
* 它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
*
* @author 休克柏
*/
public class HungrySingleton implements Serializable,Cloneable {
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton() {
if (INSTANCE != null) {
throw new RuntimeException("单例构造器禁止反射调用!");
}
}
public static HungrySingleton getInstance() {
return INSTANCE;
}
@Override
public HungrySingleton clone() {
//避免克隆攻击
// try {
// HungrySingleton clone = (HungrySingleton) super.clone();
// // TODO: copy mutable state here, so the clone can't change the internals of the original
// return clone;
// } catch (CloneNotSupportedException e) {
// throw new AssertionError();
// }
return getInstance();
}
}
懒汉模式一
import java.io.ObjectStreamException;
import java.io.Serializable;
/**
* (线程不安全)懒汉式
* 是否 Lazy 初始化:是
*
* 是否多线程安全:否
*
* 实现难度:易
*
* 描述:这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加
* 锁synchronized,所以严格意义上它并不算单例模式。
* 这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
*
* @author TNT_LEE
*/
public class Lazy1Singleton implements Serializable {
private static Lazy1Singleton instance;
private Lazy1Singleton() {
}
/**
* 线程不安全
*
* @return Lazy1Singleton
*/
public static Lazy1Singleton getInstance() {
if (instance == null) {
instance = new Lazy1Singleton();
}
return instance;
}
/**
* 反序列化的时候,会调用该方法,从而避免反序列化对单例的破坏
* @return Lazy1Singleton
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException {
return getInstance();
}
}
懒汉模式二
import java.io.Serializable;
/**
* (线程安全)懒汉式
* 是否 Lazy 初始化:是
*
* 是否多线程安全:是
*
* 实现难度:易
*
* 描述:这种方式具备很好的 lazy loading,能够在多线程中很好的工作,
* 但是,效率很低,99% 情况下不需要同步。
* 优点:第一次调用才初始化,避免内存浪费。
* 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
* getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
*
* @author 休克柏
*/
public class Lazy2Singleton implements Serializable {
private static Lazy2Singleton instance;
private Lazy2Singleton() {}
/**
* 线程安全,但是synchronized锁比较重
*
* @return Lazy2Singleton
*/
public static synchronized Lazy2Singleton getInstance() {
if (instance == null) {
instance = new Lazy2Singleton();
}
return instance;
}
}
Double Check
/**
* JDK 版本:JDK1.5 起
*
* 是否 Lazy 初始化:是
*
* 是否多线程安全:是
*
* 实现难度:较复杂
*
* 描述:这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
* getInstance() 的性能对应用程序很关键。
*
* Lazy2Singleton相对于Lazy1Singleton的效率问题,其实是为了解决1%几率的问题,
* 而使用了一个100%出现的防护盾。
* 那有一个优化的思路,就是把100%出现的防护盾,也改为1%的几率出现,使之只出现在可能会导致多个实例出现的地方。
*
* @author 休克柏
*/
public class DclSingleton implements Serializable {
/**
* volatile 的作用是对dclSingleton的写操作有一个内存屏障,这样,在它的赋值完成之前,就不用会调用读操作。
*
* volatile关键字通过提供“内存屏障”的方式来防止指令被重排序,为了实现volatile的内存语义,
* 编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。
*
* 在java内存模型中,volatile 关键字作用可以是保证可见性或者禁止指令重排。
* 这里是因为 dclSingleton = new DclSingleton() ,它并非是一个原子操作,事实上:
* 在 JVM 中上述语句至少做了以下这 3 件事:
*
* 第一步是给 dclSingleton 分配内存空间;
*
* 第二步开始调用 DclSingleton 的构造函数等,来初始化 dclSingleton;
*
* 第三步,将 dclSingleton 对象指向分配的内存空间(执行完这步 dclSingleton 就不是 null 了)。
*
* 这里需要留意一下 1-2-3 的顺序,因为存在指令重排序的优化,也就是说第 2 步和第 3 步的顺序是
* 不能保证的,最终的执行顺序,可能是 1-2-3,也有可能是 1-3-2。
* 如果是 1-3-2,那么在第 3 步执行完以后,dclSingleton 就不是 null 了,可是这时第 2 步并没
* 有执行,singleton 对象未完成初始化,它的属性的值可能不是我们所预期的值。
* 假设此时线程 2 进入 getInstance 方法,由于 dclSingleton 已经不是 null 了,
*
* 所以会通过第一重检查并直接返回,但其实这时的 singleton 并没有完成初始化,所以使用这个实例的时候会报错。
*/
private volatile static DclSingleton dclSingleton;
private DclSingleton() {}
public static DclSingleton getDlcSingleton() {
if (dclSingleton == null) {
synchronized (DclSingleton.class) {
if (dclSingleton == null) {
//1. 给 dclSingleton 分配内存
//2. 调用 dclSingleton 的构造函数来初始化成员变量,形成实例
//3. 将dclSingleton对象指向分配的内存空间(执行完这步 singleton才是非 null了)
//上述3步是dclSingleton = new DclSingleton()的指令执行顺序,
dclSingleton = new DclSingleton();
}
}
}
return dclSingleton;
}
/**
* 反序列化的时候,会调用该方法,从而避免反序列化对单例的破坏
* @return Lazy1Singleton
* @throws ObjectStreamException
*/
private Object readResolve() throws ObjectStreamException {
return getDlcSingleton();
}
}
枚举方式
import java.io.Serializable;
/**
* 1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
* <p>
* 2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
* <p>
* 而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。
* 因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。
* 解决了反射攻击和反序列化攻击:
* 1-防止序列化:通过name和class类型获取枚举常量,因此枚举中的name是唯一的,对应一个枚举常量。
* 所以序列化和反序列化对枚举破坏没有影响。
* 2-反射攻击:Enum没有无参构造器,只有一个有参构造器。反射攻击无效,源码中会判断是不是枚举类型,是的话抛出非法参数异常,不能反射创建枚举对象。
* <p>
* https://blog.csdn.net/hskw444273663/article/details/85223491
* In software engineering, a software design pattern is a general, reusable solution to a commonly occurring problem within a given context in software design.
* <p>
* It is not a finished design that can be transformed directly into source or machine code.
* <p>
* Rather, it is a description or template for how to solve a problem that can be used in many different situations.
* <p>
* Design patterns are formalized best practices that the programmer can use to solve common problems when designing an application or system.
*
* @author TNT_LEE
*/
public enum EnumSingleton implements Serializable {
/**
* 实例
*/
INSTANCE;
public void whateverMethod() {}
}
攻击方式
创建一个 Java 对象一般有 4 种方式:new 、克隆、序列化、反射,将构造函数用private修饰后,new关键字就不能用来创建对象了,那么还剩下另外三种,所以单例模式还存在三种攻击:①克隆攻击和②序列化攻击及③反射攻击;枚举单例很好的解决了这三种攻击问题。所以目前来说枚举是单例的最好实践。
克隆攻击
java提供了一个Cloneable接口来标示这个对象是可拷贝的。当一个类实现了Cloneable接口后,就表明该类从Object继承的clone()方法合法。就可以通过调用改方法返回一个Object对象,该对象的构建过程不需要调用构造函数,因为对象的构建过程是直接从内存中进行的二进制拷贝,对一些需要深拷贝的成员需要程序员做额外的处理。所以避免克隆攻击的方法就是不要实现Clone接口,如果实现了就覆盖clone()方法。java的原型模式实战就是通过上述逻辑来实现的。
package com.cld.designpattern.creation.singleton.attack.cloneattack;
public class CloneAttackDemo implements Cloneable{
private static final CloneAttackDemo INSTANCE = new CloneAttackDemo();
private CloneAttackDemo() {
if (INSTANCE != null) {
throw new RuntimeException("单例构造器禁止反射调用!");
}
}
public static CloneAttackDemo getInstance() {
return INSTANCE;
}
/**
* Creates and returns a copy of this object. The precise meaning
* of "copy" may depend on the class of the object. The general
* intent is that, for any object {@code x}, the expression:
* <blockquote>
* <pre>
* x.clone() != x</pre></blockquote>
* will be true, and that the expression:
* <blockquote>
* <pre>
* x.clone().getClass() == x.getClass()</pre></blockquote>
* will be {@code true}, but these are not absolute requirements.
* While it is typically the case that:
* <blockquote>
* <pre>
* x.clone().equals(x)</pre></blockquote>
* will be {@code true}, this is not an absolute requirement.
* <p>
* By convention, the returned object should be obtained by calling
* {@code super.clone}. If a class and all of its superclasses (except
* {@code Object}) obey this convention, it will be the case that
* {@code x.clone().getClass() == x.getClass()}.
* <p>
* By convention, the object returned by this method should be independent
* of this object (which is being cloned). To achieve this independence,
* it may be necessary to modify one or more fields of the object returned
* by {@code super.clone} before returning it. Typically, this means
* copying any mutable objects that comprise the internal "deep structure"
* of the object being cloned and replacing the references to these
* objects with references to the copies. If a class contains only
* primitive fields or references to immutable objects, then it is usually
* the case that no fields in the object returned by {@code super.clone}
* need to be modified.
* <p>
* The method {@code clone} for class {@code Object} performs a
* specific cloning operation. First, if the class of this object does
* not implement the interface {@code Cloneable}, then a
* {@code CloneNotSupportedException} is thrown. Note that all arrays
* are considered to implement the interface {@code Cloneable} and that
* the return type of the {@code clone} method of an array type {@code T[]}
* is {@code T[]} where T is any reference or primitive type.
* Otherwise, this method creates a new instance of the class of this
* object and initializes all its fields with exactly the contents of
* the corresponding fields of this object, as if by assignment; the
* contents of the fields are not themselves cloned. Thus, this method
* performs a "shallow copy" of this object, not a "deep copy" operation.
* <p>
* The class {@code Object} does not itself implement the interface
* {@code Cloneable}, so calling the {@code clone} method on an object
* whose class is {@code Object} will result in throwing an
* exception at run time.
*
* @return a clone of this instance.
* @throws CloneNotSupportedException if the object's class does not
* support the {@code Cloneable} interface. Subclasses
* that override the {@code clone} method can also
* throw this exception to indicate that an instance cannot
* be cloned.
* @see Cloneable
*/
@Override
protected CloneAttackDemo clone() throws CloneNotSupportedException {
try {
// TODO: copy mutable state here, so the clone can't change the internals of the original
return (CloneAttackDemo) super.clone();
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
public static void main(String[] args) throws CloneNotSupportedException {
CloneAttackDemo original = CloneAttackDemo.getInstance();
CloneAttackDemo clonedFromOriginal = original.clone();
if(original != clonedFromOriginal){
System.out.println("克隆攻击成功");
}else{
System.out.println("克隆攻击失败");
}
}
}
序列化攻击
反射攻击
Reflection is commonly used by programs which require the ability to examine or modify the runtime behavior of applications running in the Java virtual machine. This is a relatively advanced feature and should be used only by developers who have a strong grasp of the fundamentals of the language. With that caveat(警告; 告诫;) in mind, reflection is a powerful technique and can enable applications to perform operations which would otherwise be impossible.
/**
* 饿汉单例模式,通过修改私有构造函数可以解决反射攻击,但是对懒汉单例模式无效
* 饿汉单例模式对象是静态初始化的,不需要再次使用构造函数,所以可以对只要使用到构造函数就报错处理。
*/
@Slf4j
public class ReflectionAttackDemo {
public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
Class<HungrySingleton> object = HungrySingleton.class;
Constructor<HungrySingleton> constructor = object.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton instance = HungrySingleton.getInstance();
HungrySingleton newInstance = constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
if (instance == newInstance) {
log.info("反射攻击成功!");
} else {
log.info("反射攻击失败!");
}
}
}