文章目录
定义
- 保证一个类仅有一个实例,并提供一个全局访问点
类型
- 创建型
适用场景
- 想确保任何情况下都绝对只有一个实例
优点
- 在内存里只有一个实例,减少了内存的开销。特别是一个对象需要频繁的创建和销毁时,而且创建销毁时的性能又无法优化
- 可以避免对资源的多重占用。例如我们对一个文件进行写操作,由于只有一个实例在内存中存在,可以避免对同一个文件进行写操作
- 设置全局访问点,严格控制访问。对外不让new出来,只能通过我们的方法创建单例对象
缺点
- 没有接口,扩展困难。如果想要扩展就得修改代码,基本上没有其他途径可以实现
重点
- 私有构造器。为了禁止从单例类外部调用构造函数,来创建对象。为了达到这个目的,必须设置构造函数的权限为private。
- 线程安全。必须保证
- 延迟加载(lazy load)。我们想使用的时候再创建,就需要延迟加载了
- 序列化和反序列化安全。如果需要对对象进行序列化反序列化,就必须保证其安全,否则就会对单例造成破坏
- 反射。防止反射攻击
代码实例
懒汉式
- 说明:字面意思就是比较懒。在初始化的时候没有被创建的,而是做一个延迟加载,并且私有化构造器。
- LazySingleton code
/**
* 懒汉式单例
*/
public class LazySingleton {
//声明一个静态的要被单例的对象
private static LazySingleton lazySingleton = null;
//私有化构造器
private LazySingleton() {
}
//获取单例对象的方法
public static LazySingleton getInstance(){
//做一个空判断如果为null创建对象,反之返回lazySingleton对象
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
这种懒汉式单例在单线程的时候是ok的,但是多线程的情况下是线程不安全的。
- Test
- 模拟单线程情况
public class Test {
public static void main(String[] args) {
LazySingleton lazySingleton = LazySingleton.getInstance();
System.out.println(lazySingleton);
}
}
执行结果:
LazySingleton@45ee12a7
- 模拟多线程情况
为了更好的看出效果我们修改一下getInstance()
//获取单例对象的方法
public static LazySingleton getInstance(){
//做一个空判断如果为null创建对象,反之返回lazySingleton对象
if (lazySingleton == null){
try {
//问题放大
Thread.sleep(2);
}catch (Exception e){
e.printStackTrace();
}
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
执行测试代码
public class Test {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i< 2; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+LazySingleton.getInstance());
}
});
}
threadPool.shutdown();
}
}
执行结果
pool-1-thread-2:com.test.principle.pattern.creational.singleton.LazySingleton@23eff874
pool-1-thread-1:com.test.principle.pattern.creational.singleton.LazySingleton@a49a412
从结果来看,创建两个不同的实例。这就是所谓的线程安全问题。
原因我们也说过了。线程1执行到if (lazySingleton == null),读取了lazySingleton 为null,然后cpu就被线程2抢去了,此时,线程1还没有对lazySingleton 进行实例化。因此,线程2读取lazySingleton 时仍然为null,于是,它对lazySingleton 进行实例化了。然后,cpu就被线程1抢去了。此时,线程1由于已经读取了lazySingleton 的值并且认为它为null,所以,再次对lazySingleton 进行实例化。所以,线程1和线程2返回的不是同一个实例。
改进方法
对于懒汉式线程安全的问题我们有几种改进方案
synchronized改进方式
在我们的getInstance()上 添加 synchronized关键字修饰,使这个方法变成同步方法
简单提一下synchronized关键字:synchronized添加到静态方法上,相当于锁的是这个方法所在的类的class文件,如果不是static修饰的方法,则锁的是在堆内存中生成的对象。
代码
/**
* 懒汉式单例(synchronized修饰getInstance())
*/
public class LazySingleton {
//声明一个静态的要被单例的对象
private static LazySingleton lazySingleton = null;
//私有化构造器
private LazySingleton() {
}
public synchronized static LazySingleton getInstance(){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
重新执行我们模拟多线程代码,执行结果
pool-1-thread-2:com.test.principle.pattern.creational.singleton.LazySingleton@f710c85
pool-1-thread-1:com.test.principle.pattern.creational.singleton.LazySingleton@f710c85
该方法虽然解决了线程安全问题。但是同步锁呢 比较消耗资源,有一个加锁解锁的开销,而且这里synchronized修饰static方法的时候,锁的是这个class,锁的范围非常大,对性能也会有影响。还有就是,每个线程去执行getInstance()时都要先获得锁再去执行方法体,没有锁线程就是一个阻塞状态的,这样变得有点像串行了,所以弊端还是非常大的。因此我们还需要改进。
Double Check (双重检查)方式
代码
/**
*懒汉式单例(Double Check双重检查方式)
*/
public class LazyDoubleCheckSingleton {
//声明一个静态的要被单例的对象
private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
//私有化构造器
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance(){
if (lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
这样做呢,就是所有的线程都会进到getInstance()的方法体中,如果不为null就直接返回实例,如果为null那也会只有一个线程进入到同步代码块中,这样可以大幅的降低synchronized修饰在getInstance()带来的性能开销。
在同步代码块中lazyDoubleCheckSingleton 实例化之前进行判断是否为空 ,是为了返回同一个实例对象。如果不加,例如有线程1和线程2,线程1读取lazyDoubleCheckSingleton 值为null,此时cpu被线程2抢去了,线程2再来判断lazyDoubleCheckSingleton 值为null,于是,它开始执行同步代码块中的代码,对lazyDoubleCheckSingleton 进行实例化。此时,线程2获得cpu,由于线程1之前已经判断过lazyDoubleCheckSingleton 值为null了,于是开始执行它后面的同步代码块代码。它也会去对lazyDoubleCheckSingleton 进行实例化。
看上去这个实现方式是非常完美的,当多个线程的时候我们通过方法体内的加锁,来保证只有一个线程,能创建对象,当创建好后,再调用getInstance()的时候都不会在需要加锁,直接返回已创建好的实例对象。但是这个过程中还是存在安全隐患。因为,这里会涉及到一个指令重排序问题。
- 首先我们先分析一下 lazyDoubleCheckSingleton = new lazyDoubleCheckSingleton();的步骤
-
申请一块内存空间
-
在这块空间里实例化对象
-
设置lazyDoubleCheckSingleton 指向刚分配的内存地址
这是我们理想的步骤。但是在java 语言规范中说所有的线程在执行java程序时必须遵从,intra-thread semantics 保证重排序不会改变单线程内的程序执行结果。在我们的这个程序中步骤2和3互换,并没有改变单线程内的程序结果,所以符合规范,在真正执行的时候步骤就有可能变成1->3->2
假设按1->3->2执行,这样存在的问题就是,假设线程1 执行完分配内存地址还没有在这块空间里实例化对象的时候,线程2判断lazyDoubleCheckSingleton为null的条件就出问题了,因为已经不是空了,因此,线程2也就不会实例化对象了。
- 解决这个问题的方式有两种方式
- 不允许步骤2和3重排序
我们在声明lazyDoubleCheckSingleton 的时候 添加volatile关键字 来禁止重排序
简单提一下volatile 关键字,在多线程的时候是有共享内存的,在添加volatile关键字后所有的线程就都可以看到共享内存的最新状态,在进行写操作的时候,将当前缓存行的数据写回到内存,这时候,会使得其他内存里缓存了该内存地址的数据无效,重新从共享内存同步数据这样就保证了内存的可见性。这里主要是用的java的缓存一致性协议。当处理器发现我的缓存无效了,所以在进行操作的时候,会重新从系统内存中,把数据读到处理器的缓存里
public class LazyDoubleCheckSingleton {
//声明一个静态的要被单例的对象
private volatile static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
//私有化构造器
private LazyDoubleCheckSingleton() {
}
public static LazyDoubleCheckSingleton getInstance(){
if (lazyDoubleCheckSingleton == null){
synchronized (LazyDoubleCheckSingleton.class){
if (lazyDoubleCheckSingleton == null) {
lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazyDoubleCheckSingleton;
}
}
- 运行当前线程重排序,但是运行其他线程看到这个重排序
通过静态内部类解决
静态内部类方式
/**
* 静态内部类-单例 写法1
*/
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton(){
}
private static class InnerClass{
private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
}
public static StaticInnerClassSingleton getInstance(){
return InnerClass.staticInnerClassSingleton;
}
}
原理:JVM在类的初始化阶段,也就是class加载后,并且在线程使用之前,都是类的初始化阶段,在执行类的初始化期间,JVM会获取一个Class对象的初始化锁,这个锁会同步,多个线程对一个类的初始化,基于这个特性,我们可以实现基于静态内部类,线程安全并且延迟加载的初始化方案。
那他是如何保证线程安全的呢,这里我们就要说一下类加载时机。
类加载时机根据java语言规范主要分为5中情况
- 有一个A类型的实例被创建
- A类中有一个声明的静态方法被调用
- A类中声明的一个静态成员被赋值
- A类中声明的一个静态成员被使用,并且该成员不是一个常量成员
- A类如果是一个顶级类,并且在这个类中有嵌套的断言语句
举个栗子:
假设有线程A和线程B。他们两个线程在首次试图获取Class对象初始化锁的时候,这个时候必然只有一个线程能获取到,假设线程A拿到了,并且执行静态内部类的初始化,对于静态内部类中staticInnerClassSingleton = new StaticInnerClassSingleton()的分配内存地址和在这块空间里实例化对象存在重排序, 但是线程1是无法感知的,因为线程1还处于一个等待状态。
静态内部类核心在于,InnerClass这个静态内部类的初始化锁,看哪个线程拿到,哪个线程就去初始化它
饿汉式
在类加载的时候就完成实例化
代码
/**
* 饿汉式-单例写法1 直接初始化
*/
public class HungrySingleton {
private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return HUNGRY_SINGLETON;
}
}
/**
* 饿汉式-单例写法2 放到静态代码块中初始化
*/
public class HungrySingleton {
private final static HungrySingleton HUNGRY_SINGLETON;
static {
HUNGRY_SINGLETON = new HungrySingleton();
}
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return HUNGRY_SINGLETON;
}
}
这个就是非常简单的饿汉式单例,优点呢就是写法简单,类加载的时候就完成了初始化,避免了线程同步问题,缺点就是,因为在类加载的时候就完成了初始化,没有延迟加载的效果。如果这个类从始至终我们的系统都没用过,还会造成内存的浪费。
反序列化破坏单例模式
验证
我们以饿汉式单例为例。
- 首先我们要让我们的HungrySingleton 实现Serializable接口
/**
* 饿汉式-单例
*/
public class HungrySingleton implements Serializable{
private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return HUNGRY_SINGLETON;
}
}
- 编写测试类
public class Test {
public static void main(String[] args) throws Exception{
//获取饿汉式单例对象
HungrySingleton instance = HungrySingleton.getInstance();
//假设我们把instance这个对象 序列化到一个文件中,再从文件中把这个对象取出来。取出来的对象还是原来的那个对象吗?
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
HungrySingleton newInstance = (HungrySingleton)ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
执行结果
com.test.principle.pattern.creational.singleton.HungrySingleton@12a3a380
com.test.principle.pattern.creational.singleton.HungrySingleton@27d6c5e0
false
从结果可以看出,我们已经违背了单例模式的一个初衷,通过序列化和反序列化拿到了不一样的对象。而我们希望的是同一个对象
解决方法
修改我们的单例类
/**
* 饿汉式-单例
*/
public class HungrySingleton implements Serializable{
private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
private HungrySingleton(){
}
public static HungrySingleton getInstance(){
return HUNGRY_SINGLETON;
}
//添加readResolve方法(强制,换成其他方法名不生效)
public Object readResolve(){
return HUNGRY_SINGLETON;
}
}
再执行我们的测试类,得出的执行结果
com.test.principle.pattern.creational.singleton.HungrySingleton@12a3a380
com.test.principle.pattern.creational.singleton.HungrySingleton@12a3a380
true
就此反序列化破坏单例模式的问题解决。其他单例的模式也是相同的。
原因
我们很好奇为什么定义一个**readResolve()**就解决的了这个问题。方法名必须是readResolve,别的就不生效。
首先我们得知道在反序列化的过程中到底发生了什么。
根据我们的测试代码对象的序列化过程通过ObjectOutputStream和ObjectInputputStream来实现的,那么带着刚刚的问题,分析一下ObjectInputputStream 的readObject 方法执行情况到底是怎样的。
首先我们DEBUG看一下readObject调用栈:
readObject—>readObject0—>readOrdinaryObject—>checkResolve、
接下来划重点
主要看readOrdinaryObject(boolean unshared)这个方法
public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants {
//省略部分代码...
private Object readOrdinaryObject(boolean unshared)
throws IOException {
//省略部分代码.....
Object obj;
try {
//1.部分
//首先isInstantiable()判断是否可以初始化
//如果为true,则调用newInstance()方法创建对象,这时创建的对象是不走构造函数的,是一个新的对象
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}
//省略部分代码....
//2.部分
//hasReadResolveMethod()会去判断,我们的InnerClassSingleton对象中是否有readResolve()方法
if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod()){
//3.部分
//如果为true,则执行readResolve()方法,而我们在自己的readResolve()方法中 直接retrun InnerClassHelper.INSTANCE,所以还是返回的同一个对象,保证了单例
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}
return obj;
}
//省略部分代码...
}
- 第一部分代码
obj = desc.isInstantiable() ? desc.newInstance() : null;
这里创建的这个obj对象,就是readOrdinaryObject()要返回的对象,也可以暂时理解为是ObjectInputStream的readObject返回的对象。
而调用的两个方法。
desc.isInstantiable():如果一个serializable/externalizable的类可以在运行时被实例化,那么该方法就返回true。
desc.newInstance():该方法通过反射的方式调用无参构造方法新建一个对象。
至此我们找到了,反序列化会破坏单例的原因 序列化会通过反射调用无参数的构造方法创建一个新的对象。
- 第二部分代码和第三部分代码
desc.hasReadResolveMethod()
Object rep = desc.invokeReadResolve(obj);
hasReadResolveMethod:如果实现了serializable 或者 externalizable接口的类中包含readResolve方法名则返回true、
invokeReadResolve:通过反射的方式调用要被反序列化的类的readResolve方法。
至此我们了解了为什么在单例类中中定义readResolve方法,并在该方法中指定要返回的对象的生成策略,就可以防止单例被破坏的原因。
在这个过程中 第一部分的代码一定会走的。只是在单例类中定义readResolve方法后没有返回第一部分实例化的对象。这个还是要注意一下的。
在我们的业务场景中如果有需要序列化反序列的单例,一定考虑一下反序列化对单例的影响。
反射攻击
验证
我们还是以饿汉式单例为例。
编写测试类
public static void main(String[] args) throws Exception{
//已正常的方式获取饿汉式单例对象
HungrySingleton instance = HungrySingleton.getInstance();
System.out.println(instance);
//已反射的方式获取饿汉式单例对象
Constructor constructor = HungrySingleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
HungrySingleton newInstance = (HungrySingleton)constructor.newInstance();
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
执行结果
com.test.principle.pattern.creational.singleton.HungrySingleton@45ee12a7
com.test.principle.pattern.creational.singleton.HungrySingleton@330bedb4
false
从结果来看我们用反射,也破坏了单例的初衷
解决方法
在我们的构造器中添加反射防御的代码
/**
* 饿汉式-单例
*/
public class HungrySingleton implements Serializable{
private final static HungrySingleton HUNGRY_SINGLETON = new HungrySingleton();
private HungrySingleton(){
if (HUNGRY_SINGLETON != null){
throw new RuntimeException("duplicate instance create error!" + HungrySingleton.class.getName());
}
}
public static HungrySingleton getInstance(){
return HUNGRY_SINGLETON;
}
public Object readResolve(){
return HUNGRY_SINGLETON;
}
}
再执行测试代码,执行结果
com.test.principle.pattern.creational.singleton.HungrySingleton@45ee12a7
Exception in thread "main" java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
at com.test.principle.pattern.creational.singleton.Test.main(Test.java:20)
Caused by: java.lang.RuntimeException: duplicate instance create error!com.test.principle.pattern.creational.singleton.HungrySingleton
at com.test.principle.pattern.creational.singleton.HungrySingleton.<init>(HungrySingleton.java:12)
... 5 more
我们发现 这样解决反射问题。这种方法只能解决类加载时初始化实例的单例实现方式(静态内部类的方式),延迟加载的实现方式并不能防止反射破坏
在我们真正的业务中,尽量不要用反射来创建实例
Enum 枚举单例
枚举类天然的可序列化机制,能够强有力的保证不会出现多种实例化的情况,即使在复杂的序列化情况下或者反射的攻击下,枚举类型的单例模式都没有问题。
code
/**
* 枚举类单例模式
*/
public enum EnumInstance {
INSTANCE;
private Object data;
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public static EnumInstance getInstance(){
return INSTANCE;
}
}
测试序列化反序列化
测试代码
public class Test {
public static void main(String[] args) throws Exception{
//获取枚举单例对象
EnumInstance instance = EnumInstance.getInstance();
//设置枚举单例里的data
instance.setData(new Object());
//序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
oos.writeObject(instance);
//反序列化
File file = new File("singleton_file");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
EnumInstance newInstance = (EnumInstance)ois.readObject();
//data是否是同一个对象
System.out.println(instance.getData());
System.out.println(newInstance.getData());
System.out.println(instance.getData() == newInstance.getData());
}
}
执行结果
java.lang.Object@4f3f5b24
java.lang.Object@4f3f5b24
true
通过结果我们可以看出,序列化反序列化后还是同一个对象。单例没有遭到破坏
跟之前的方式一样我们先看一下ObjectInputputStream 的readObject 方法的调用栈
readObject —> readObject0 —> readEnum —> checkResolve
接下来我们划重点,主要在readEnum(boolean unshared) 这个方法中,上代码
public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants {
//省略部分代码...
private Enum<?> readEnum(boolean unshared) throws IOException {
//省略部分代码...
//1. 通过readString这个方法获取到枚举对象的名称
String name = readString(false);
//2.声明一个为null的Enum
Enum<?> result = null;
Class<?> cl = desc.forClass();
if (cl != null) {
try {
//3.通过valueOf方法获取Enum,参数为class和name
@SuppressWarnings("unchecked")
Enum<?> en = Enum.valueOf((Class) cl, name);
//4. 赋值
result = en;
} catch (IllegalArgumentException ex) {
throw (IOException) new InvalidObjectException(
"enum constant " + name + " does not exist in " +
cl).initCause(ex);
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
//省略部分代码...
}
通过以上代码我们可以得知Enum单例模式 序列化的时候只将 INSTANCE 这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。
测试反射
测试代码
public class Test {
public static void main(String[] args) throws Exception {
Constructor constructor = EnumInstance.class.getDeclaredConstructor();
constructor.setAccessible(true);
EnumInstance newInstance = (EnumInstance)constructor.newInstance();
System.out.println(newInstance);
}
}
执行结果
Exception in thread "main" java.lang.NoSuchMethodException: com.test.principle.pattern.creational.singleton.EnumInstance.<init>()
at java.lang.Class.getConstructor0(Class.java:3082)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at com.test.principle.pattern.creational.singleton.Test.main(Test.java:16)
通过执行结果我们可以看出,当我们用想要反射初始化的时候,EnumInstance.class.getDeclaredConstructor();报了一个NoSuchMethodException,也就是说在过获取构造器的时候并没有获得无参的构造器。为什么会这样呢。
我们首先看一下java.lang.Enum
//首先这是一个抽象类
public abstract class Enum<E extends Enum<E>>
implements Comparable<E>, Serializable {
//省略部分代码...
//重点,Enum只有一个构造方法并没有无参构造器
protected Enum(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
//省略部分代码...
}
既然他没有无参构造器,我们就尝试用它有参的构造器反射试一下
public class Test {
public static void main(String[] args) throws Exception {
Constructor constructor = EnumInstance.class.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumInstance newInstance = (EnumInstance)constructor.newInstance("oiobee",666);
System.out.println(newInstance);
}
}
执行结果
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
at com.test.principle.pattern.creational.singleton.Test.main(Test.java:15)
我们发现虽然我们获取到了他的构造器,但是我们真正创建对象的时候,还是抛了一个IllegalArgumentException的异常出来,并且提示很明确的说不能反射创建枚举对象。我们可以看到这个异常是从Constructor这个类里抛出来的。那我们就进去看一下
public final class Constructor<T> extends Executable {
//省略部分代码....
@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);
}
}
//重点:这行判断就是看一下我们使用newInstance的时候目标类型是不是一个枚举类,是就直接抛异常
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;
}
//省略部分代码....
}
现在我们就知道为什么无法对枚举类进行反射攻击了,因为人家JDK把枚举类当儿子不让它受到一点点的伤害
我们在看看java.lang.Enum的一部分代码,你就知道啥叫亲儿子了,你想克隆或者序列化Enum对象不存在的,人家直接给你抛异常
protected final Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
//
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");
}
反编译
我们来反编译EnumInstance.class看一下
// 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: EnumInstance.java
package com.test.principle.pattern.creational.singleton;
//final代表这个类不可被继承
public final class EnumInstance extends Enum
{
public static EnumInstance[] values()
{
return (EnumInstance[])$VALUES.clone();
}
public static EnumInstance valueOf(String name)
{
return (EnumInstance)Enum.valueOf(com/test/principle/pattern/creational/singleton/EnumInstance, name);
}
//符合我们单例模式的要求私有构造器,不允许外部实例化
private EnumInstance(String s, int i)
{
super(s, i);
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
this.data = data;
}
public static EnumInstance getInstance()
{
return INSTANCE;
}
//类变量是静态
public static final EnumInstance INSTANCE;
private Object data;
private static final EnumInstance $VALUES[];
//通过静态代码块的方式来实例化INSTANCE,所以这个类在加载的时候就把INSTANCE给初始化了,所以它是线程安全的。这点跟我的饿汉式单例是很像的
static
{
INSTANCE = new EnumInstance("INSTANCE", 0);
$VALUES = (new EnumInstance[] {
INSTANCE
});
}
}
容器单例
和享元模式类型。我们可以使用容器来管理多个单例模式对象
/**
* 容器单例类
*/
public class ContainerSingleton {
private ContainerSingleton(){
}
private static Map<String,Object> containerMap = new HashMap<String, Object>();
public static void putInstance(String key, Object instance){
if (StringUtils.isNotBlank(key) && instance != null){
if (!containerMap.containsKey(key)){
containerMap.put(key,instance);
}
}
}
public static Object getInstance(String key){
return containerMap.get(key);
}
}
这种写法很简单。但是这个单例模式有缺点,是一个平衡根据业务场景判断,假设,单例对象非常多,也可以考虑用一个容器来统一管理
优点:统一管理节省资源,map就相当于一个缓存
缺点:线程不安全
为什么是线程不安全的:本身hashmap就是线程不安全的,虽然用Hashtable,他就是线程安全的了,但是不建议使用,因为会影响性能,频繁去获取实例的时候,都会有同步锁。因为我们这个容器是静态的,而是会直接操作容器,所以用ConcurrentHashMap也并不是觉得的线程安全。
ThreadLocal"线程"单例
这个单例不是那么纯粹,它无法保证全局唯一,但是他可以线程唯一。
code
/**
* ThreadLocal线程单例
*/
public class ThreadLocalInstance {
private static ThreadLocal<ThreadLocalInstance> threadLocalinstanceThreadLocal
= new ThreadLocal<ThreadLocalInstance>(){
//重写ThreadLocal的初始化方法
@Override
protected ThreadLocalInstance initialValue() {
return new ThreadLocalInstance();
}
};
//私有化构造器
private ThreadLocalInstance(){
}
//获取实例
public static ThreadLocalInstance getInstance(){
return threadLocalinstanceThreadLocal.get();
}
}
测试代码
public class Test {
public static void main(String[] args) throws Exception {
System.out.println(Thread.currentThread().getName()+":"+ ThreadLocalInstance.getInstance());
System.out.println(Thread.currentThread().getName()+":"+ ThreadLocalInstance.getInstance());
System.out.println(Thread.currentThread().getName()+":"+ ThreadLocalInstance.getInstance());
ExecutorService threadPool = Executors.newFixedThreadPool(2);
for (int i = 0; i< 2; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+":"+ ThreadLocalInstance.getInstance());
}
});
}
threadPool.shutdown();
}
}
执行结果
main:com.test.principle.pattern.creational.singleton.ThreadLocalInstance@45ee12a7
main:com.test.principle.pattern.creational.singleton.ThreadLocalInstance@45ee12a7
main:com.test.principle.pattern.creational.singleton.ThreadLocalInstance@45ee12a7
pool-1-thread-1:com.test.principle.pattern.creational.singleton.ThreadLocalInstance@1a6270ed
pool-1-thread-2:com.test.principle.pattern.creational.singleton.ThreadLocalInstance@2988d273
从结果来看,main线程获取的是同一个实例,而pool-1-thread-1和pool-1-thread-2获取的则是两个完全不同的实例对象
讲一下原因:
ThreadLocal会为每个线程创建一个独立的变量副本。 ThreadLocal是基于它里面一个静态内部类ThreadLocalMap来实现的所有我们调用get()的时候默认走的就是ThreadLocalMap不用指定key,他维持了线程间的隔离,ThreadLocal隔离了多个线程对数据的访问冲突,
对于多线程资源共享的问题,如果使用同步锁就是拿时间换空间的方式,因为要排队,如果使用ThreadLocal就是以空间换时间的方式,他会创建很多对象。至少在一个线程里会创建一个,但是对于这个线程他获取这个对象是唯一的,正如我们的main线程一样。里面拿到的对象都是同一个。