单例模式_反射破坏单例模式_枚举类_枚举类实现单例_枚举类解决单例模式破坏
转:狂神说Java之彻底玩转单例设计模式
彻底玩转单例模式
参考文章:
单例模式:
简介:
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。
适用场景:
1.需要生成唯一序列的环境 2.需要频繁实例化然后销毁的对象。 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 4.方便资源相互通信的环境
构建步骤:
- 将该类的构造方法定义为私有方法
- 这样其他处的代码就无法通过调用该类的构造方法来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例;
- 在该类内提供一个静态实例化方法
- 当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,
- 如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用。
优缺点:
优点:
- 在内存中只有一个对象,节省内存空间;
- 避免频繁的创建销毁对象,可以提高性能;
- 避免对共享资源的多重占用,简化访问;
- 为整个系统提供一个全局访问点。
缺点:
- 不适用于变化频繁的对象;
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;
- 如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;
饿汉式单例模式:
饿汉式:饥饿的人,看到食物就上来抢。对应在代码中,在类加载时就立刻实例化。
public class HungrySingleton { //假如类中存在这样的空间开辟的操作: //使用饿汉式时,不管用不用,上来就给你开了再说,造成空间浪费。 private byte[] data1 = new byte[1024*1024]; private byte[] data2 = new byte[1024*1024]; private byte[] data3 = new byte[1024*1024]; private byte[] data4 = new byte[1024*1024]; //1、私有构造器 private HungrySingleton(){ } //2、类的内部创建对象 private final static HungrySingleton HUNGRYSINGLE = new HungrySingleton(); //3、向外暴露一个静态的公共方法。 getInstance public static HungrySingleton getInstance(){ return HUNGRYSINGLE; } public static void main(String[] args) { //单线程: HungrySingleton instance1 = HungrySingleton.getInstance(); HungrySingleton instance2 = HungrySingleton.getInstance(); System.out.println(instance1 == instance2);//true System.out.println("------------"); //多线程: new Thread(()->{ HungrySingleton instance_A = HungrySingleton.getInstance(); System.out.println(instance_A); //com.kuangstudy.Singleton.HungrySingleton@626213bf }).start(); new Thread(()->{ HungrySingleton instance_B = HungrySingleton.getInstance(); System.out.println(instance_B); //com.kuangstudy.Singleton.HungrySingleton@626213bf }).start(); } }
小结:
优点:
- 基于 classloader 机制避免了多线程的同步问题,在类装载的时候就完成实例化。避免了线程同步问题==>线程安全。
- 没有加锁,执行效率会提高。
- 简单好用。
缺点:
在类装载的时候就完成实例化,没有达到Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。
懒汉式单例模式:
非线程安全:
public class LazySingleton { //1.私有化构造函数 private LazySingleton() { System.out.println(Thread.currentThread().getName()+" ->OK"); } //2.创建对象(容器) private static LazySingleton lazyMan ; //3.对外提供静态实例化方法,判断在对象为空的时候创建 public static LazySingleton getInstance(){ //用的时候再加载 if (lazyMan == null) { lazyMan = new LazySingleton(); } return lazyMan; } //测试单线程下懒汉式单例 public static void main(String[] args) { LazySingleton instance1 = LazySingleton.getInstance(); LazySingleton instance2 = LazySingleton.getInstance(); //实例化两个只出现:main ->OK System.out.println(instance1);//com.kuangstudy.Singleton.LazySingleton@61bbe9ba System.out.println(instance2);//com.kuangstudy.Singleton.LazySingleton@61bbe9ba } }
小结
这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。
public class LazySingleton { //1.私有化构造函数 private LazySingleton() { System.out.println(Thread.currentThread().getName()+" ->OK"); } //2.创建对象(容器) private static LazySingleton lazyMan ; //3.对外提供静态实例化方法,判断在对象为空的时候创建 public static LazySingleton getInstance(){ //用的时候再加载 if (lazyMan == null) { lazyMan = new LazySingleton(); } return lazyMan; } //开启多条线程实例化LazySingleton public static void main(String[] args) { for (int i = 0; i < 100; i++) { // new Thread(()->{LazySingleton.getInstance();}).start(); new Thread(LazySingleton::getInstance).start(); } } //结果实例化超过1个对象:(不唯一) /* Thread-0 ->OK Thread-3 ->OK Thread-2 ->OK Thread-1 ->OK */ }
线程安全:
public class LazySingletonWithDCL { //1.私有化构造函数 private LazySingletonWithDCL() { System.out.println(Thread.currentThread().getName() + " ->OK"); } //2.创建对象(容器) // private static LazySingletonWithDCL lazyMan ; //5.new 不是一个原子性操作: private volatile static LazySingletonWithDCL lazyMan; //3.对外提供静态实例化方法 //4.为保证线程安全,需要上锁 public static LazySingletonWithDCL getInstance() { if (lazyMan == null) { synchronized (LazySingletonWithDCL.class) { if (lazyMan == null) { lazyMan = new LazySingletonWithDCL(); } } } return lazyMan; } //开启多条线程实例化LazySingletonWithDCL public static void main(String[] args) { for (int i = 0; i < 100; i++) { // new Thread(()->{LazySingletonWithDCL.getInstance();}).start(); new Thread(LazySingletonWithDCL::getInstance).start(); } } }
问题:
为什么要synchronized (LazySingletonWithDCL.class)而不是对方法加锁?
相关博客:Synchronized方法锁、对象锁、类锁区别 (精)
简单回答:
synchronized 重量级锁,锁的范围越小越好,class只有一个,而方法会每次都执行,因此为了提高效率,锁对象而不是锁方法。
new对象的过程为什么不是一个原子性操作?
相关博客:new一个对象竟然不是原子操作?
实锤 new 对象不是原子性操作,会执行以下操作:
- 分配内存空间
- 执行构造方法,
- 初始化对象把对象指向空间实践出真知:
这是原子类AtomicInteger.getAndIncrement()方法,反编译后:
public class TestNew { private static int num = 0; public static void main(String[] args) { TestNew testNew = new TestNew(); } public void add(){ num++; }; }
反编译后:
小结:
- 双检锁/双重校验锁(DCL,即 double-checked locking)
- 这种方式采用双锁机制,安全且在多线程情况下能保持高性能。getInstance() 的性能对应用程序很关键。
- 这样,实例化代码只用执行一次,后面再次访问时,判断 if(lazyMan == null),直接return实例化对象,也避免的反复进行方法同步.
- 线程安全;延迟加载;效率较高
反射破坏单例:
在Java进阶中学习过非常暴力的获取类的方式:反射(详见:注解与反射)
因此对于在单例模式中私有的方法,我们可以通过反射进行破解:
对DCL饿汉式单例进行破解:
实力诠释再多的🔒也抵不住反射的暴力破解
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { LazySingletonWithDCL instance = LazySingletonWithDCL.getInstance(); //利用反射创建对象 //获取无参构造 Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null); //开放权限 declaredConstructor.setAccessible(true); LazySingletonWithDCL instance_withReflect = declaredConstructor.newInstance(); System.out.println(instance); //LazySingletonWithDCL@61bbe9ba System.out.println(instance_withReflect); //LazySingletonWithDCL@610455d6 System.out.println(instance == instance_withReflect); //false }
单例模式增加校验反击:
因为反射走的是无参构造,可以在构造函数中进行判断
public class LazySingletonWithDCL { //1.私有化构造函数 //6.增加对反射的判断 private LazySingletonWithDCL() { synchronized (LazySingletonWithDCL.class){ //6.1如果此时已经有实例,阻止反射创建 if (lazyMan != null){ throw new RuntimeException("不要试图通过反射破解单例"); } } System.out.println(Thread.currentThread().getName() + " ->OK"); } //2.创建对象(容器) // private static LazySingletonWithDCL lazyMan ; //5.new 不是一个原子性操作: private volatile static LazySingletonWithDCL lazyMan; //3.对外提供静态实例化方法 //4.为保证线程安全,需要上锁 public static LazySingletonWithDCL getInstance() { if (lazyMan == null) { synchronized (LazySingletonWithDCL.class) { if (lazyMan == null) { lazyMan = new LazySingletonWithDCL(); } } } return lazyMan; } public static void main(String[] args) throws Exception { LazySingletonWithDCL instance = LazySingletonWithDCL.getInstance(); System.out.println(instance); //创建成功: LazySingletonWithDCL@61bbe9ba //利用反射创建对象 //获取无参构造 Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null); //开放权限 declaredConstructor.setAccessible(true); LazySingletonWithDCL instance_withReflect = declaredConstructor.newInstance(); System.out.println(instance_withReflect); //LazySingletonWithDCL@610455d6 System.out.println(instance == instance_withReflect); //false } }
main ->OK com.kuangstudy.Singleton.LazySingletonWithDCL@61bbe9ba 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.kuangstudy.Singleton.LazySingletonWithDCL.main(LazySingletonWithDCL.java:60) Caused by: java.lang.RuntimeException: 不要试图通过反射破解单例 at com.kuangstudy.Singleton.LazySingletonWithDCL.<init>(LazySingletonWithDCL.java:20) ... 5 more
只创建出了一个实例,并成功拦截了 通过反射创建对象的行为
反射再扳回一城
上述例子2中是用一个饿汉+反射创建,但如果两个对象都是用反射创建呢?
public static void main(String[] args) throws Exception { //获取类模板class Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null); //开放权限 declaredConstructor.setAccessible(true); //通过空参创建实例 LazySingletonWithDCL instance_withReflect1 = declaredConstructor.newInstance(); LazySingletonWithDCL instance_withReflect2 = declaredConstructor.newInstance(); System.out.println(instance_withReflect1);//LazySingletonWithDCL@61bbe9ba System.out.println(instance_withReflect2);//LazySingletonWithDCL@610455d6 System.out.println(instance_withReflect1 == instance_withReflect2);//false }
结果非常明显:再一次破坏了单例模式
懒汉单例再度防守:
(加入标志位)
在空参方法里运用标志位,因为实例化对象需要获取类模板:
- 当获取了一次类模板之后,就把标志位flag置反。
- 等一次通过反射获取类模板创建对象的时候便能抛异常
public class LazySingletonWithDCL { //7.加入标志位,防止多个反射破坏单例 private static boolean flag = true; //1.私有化构造函数 //6.增加对反射的判断 private LazySingletonWithDCL() { synchronized (LazySingletonWithDCL.class){ if (flag){ flag = false; }else { // //6.1如果此时已经有实例,阻止反射创建 // if (lazyMan != null){ throw new RuntimeException("不要试图通过反射破解单例"); // } } } System.out.println(Thread.currentThread().getName() + " ->OK"); }
//2.创建对象(容器)
// private static LazySingletonWithDCL lazyMan ; //5.new 不是一个原子性操作: private volatile static LazySingletonWithDCL lazyMan; //3.对外提供静态实例化方法 //4.为保证线程安全,需要上锁 public static LazySingletonWithDCL getInstance() { if (lazyMan == null) { synchronized (LazySingletonWithDCL.class) { if (lazyMan == null) { lazyMan = new LazySingletonWithDCL(); } } } return lazyMan; } public static void main(String[] args) throws Exception { //获取类模板class Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null); //开放权限 declaredConstructor.setAccessible(true); //通过空参创建实例 LazySingletonWithDCL instance_withReflect1 = declaredConstructor.newInstance(); System.out.println(instance_withReflect1);//LazySingletonWithDCL@61bbe9ba LazySingletonWithDCL instance_withReflect2 = declaredConstructor.newInstance(); System.out.println(instance_withReflect2); //报错:java.lang.RuntimeException: 不要试图通过反射破解单例 System.out.println(instance_withReflect1 == instance_withReflect2); } }
由运行结果可见,保住了单例模式。
至此,用过反射调用空参实例化的方法被标志位掐断
反射再度进攻
(你既然设置了标志位,那我就来破坏标志位)
public static void main(String[] args) throws Exception{ //获取标志位 Field flag = LazySingletonWithDCL.class.getDeclaredField("flag"); flag.setAccessible(true); //获取类模板class Constructor<LazySingletonWithDCL> declaredConstructor = LazySingletonWithDCL.class.getDeclaredConstructor(null); //开放权限 declaredConstructor.setAccessible(true); //通过空参创建实例 LazySingletonWithDCL instance_withReflect1 = declaredConstructor.newInstance(); System.out.println(instance_withReflect1);//LazySingletonWithDCL@61bbe9ba //在获取并使用完类模板后,重新设置flag值: flag.set(instance_withReflect1,true); LazySingletonWithDCL instance_withReflect2 = declaredConstructor.newInstance(); System.out.println(instance_withReflect2); }
此时:
main ->OK
com.kuangstudy.Singleton.LazySingletonWithDCL@61bbe9ba
main ->OK
com.kuangstudy.Singleton.LazySingletonWithDCL@511d50c0
可以看到单例模式再一次被破坏了
该如何真正确保单例?
解铃还须系铃人,那我们就从单例入手分析一波:
查看反射中newInstance()方法
否则会报异常:
IllegalArgumentException("Cannot reflectively create enum objects")
1
枚举保证单例:
构造枚举尝试单例模式:
public enum EnumSingleton { INSTANCE; public EnumSingleton getInstance(){ return INSTANCE; } } class Test{ public static void main(String[] args) { EnumSingleton instance1 = EnumSingleton.INSTANCE; EnumSingleton instance2 = EnumSingleton.INSTANCE; System.out.println(instance1.hashCode());//1639705018 System.out.println(instance2.hashCode());//1639705018 } }
简单代码创建一个实例,针不戳
尝试用反射破坏枚举的单例模式
public static void main(String[] args) throws Exception { //instance1正常获取 EnumSingleton instance1 = EnumSingleton.INSTANCE; System.out.println(instance1); //INSTANCE //instance2通过反射获取: //1.获取其空参 Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(null); //2.打开权限: constructor.setAccessible(true); //3.实例化对象: EnumSingleton instance2 = constructor.newInstance(); System.out.println(instance2); }
结果:
- instance1 正常实例化
- 但是在通过反射的时候报了错:
找不到EnumSingleton 的空参构造?
这就奇怪了。
idea查看class文件也是只有空参构造:
用javap -p -c .\EnumSingleton.class口令也是看到空参:
探究枚举类的构造函数:
上述方法行不通后得运用更加专业的工具进行反编译:
使用jad工具
使用命令:
.\jad.exe -sjava .\EnumSingleton.class
将class成功反编译为Java文件:
// Decompiled by Jad v1.5.8e2. Copyright 2001 Pavel Kouznetsov. // Jad home page: http://kpdus.tripod.com/jad.html // Decompiler options: packimports(3) // Source File Name: EnumSingleton.java package com.kuangstudy.Singleton; public final class EnumSingleton extends Enum { public static EnumSingleton[] values() { return (EnumSingleton[])$VALUES.clone(); } public static EnumSingleton valueOf(String name) { return (EnumSingleton)Enum.valueOf(com/kuangstudy/Singleton/EnumSingleton, name); } private EnumSingleton(String s, int i) { super(s, i); } public EnumSingleton getInstance() { return INSTANCE; } public static final EnumSingleton INSTANCE; private static final EnumSingleton $VALUES[]; static { INSTANCE = new EnumSingleton("INSTANCE", 0); $VALUES = (new EnumSingleton[] { INSTANCE }); } }
此时我们发现枚举类内部其实是一个有参的构造函数。
此时我们修改代码:
//1.获取其有参构造: Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);
这时候我们就能如愿得到想要的报错了 (?怎么听起来怪怪的)
INSTANCE Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417) at com.kuangstudy.Singleton.Test.main(EnumSingleton.java:40)
小结:
用枚举构建单例模式:
单例模式在JDK中的应用:
在Runtime类中使用了单例模式(不过是俄汉式)上代码:
public class Runtime { //2.类的内部创建对象 private static Runtime currentRuntime = new Runtime(); //3.开发对外的实例化方法 public static Runtime getRuntime() { return currentRuntime; } //1.私有化构造方法 private Runtime() {} .... }
java中枚举类型的使用
Java 枚举(enum) 详解7种常见的用法
JDK1.5引入了新的类型——枚举。在 Java 中它虽然算个“小”功能,却给我的开发带来了“大”方便。
web项目里实体类使用枚举类型:
一般在该实体类的包下在新建一个enumeration包,把枚举类型的类写在enumeration包下,例如:
1 public enum Color { 2 RED, //红色 3 BLUE, //蓝色 4 GREEN //绿色 5 }
然后在实体类里引用这个枚举类。
1 @Enumerated(value = EnumType.STRING) 2 @Column(name = "color") 3 @NotNull 4 private Color color;
注意:
(1)@Enumerated(value=EnumType.ORDINAL)采用枚举类型的序号值与数据库进行交互,
此时数据库的数据类型需要是数值类型,例如在实际操作中
CatTest ct = new CatTest(); ct.setColor(Color.BLUE);
当我们将对象ct保存到数据库中的时候,数据库中存储的数值是BLUE在Color枚举
定义中的序号1(序号从零开始);
(2)@Enumerated(value=EnumType.STRING)采用枚举类型与数据库进行交互,
此时数据库的数据类型需要是NVACHAR2等字符串类型,例如在实际操作中
CatTest ct = new CatTest(); ct.setColor(Color.BLUR);
数据库中存储的数值是BLUE字符串。
枚举类型对象之间的值比较,是可以使用==,直接来比较值,是否相等的,不是必须使用equals方法的哟。
用法一:常量
在JDK1.5 之前,我们定义常量都是: public static fianl.... 。现在好了,有了枚举,可以把相关的常量分组到一个枚举类型里,而且枚举提供了比常量更多的方法。
1 public enum Color { 2 RED, GREEN, BLANK, YELLOW 3 }
用法二:switch
1 enum Signal { 2 GREEN, YELLOW, RED 3 } 4 public class TrafficLight { 5 Signal color = Signal.RED; 6 public void change() { 7 switch (color) { 8 case RED: 9 color = Signal.GREEN; 10 break; 11 case YELLOW: 12 color = Signal.RED; 13 break; 14 case GREEN: 15 color = Signal.YELLOW; 16 break; 17 } 18 } 19 }
用法三:向枚举中添加新方法
1 public enum Color { 2 RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4); 3 // 成员变量 4 private String name; 5 private int index; 6 // 构造方法 7 private Color(String name, int index) { 8 this.name = name; 9 this.index = index; 10 } 11 // 普通方法 12 public static String getName(int index) { 13 for (Color c : Color.values()) { 14 if (c.getIndex() == index) { 15 return c.name; 16 } 17 } 18 return null; 19 } 20 // get set 方法 21 public String getName() { 22 return name; 23 } 24 public void setName(String name) { 25 this.name = name; 26 } 27 public int getIndex() { 28 return index; 29 } 30 public void setIndex(int index) { 31 this.index = index; 32 } 33 }
用法四:覆盖枚举的方法
下面给出一个toString()方法覆盖的例子。
1 public enum Color { 2 RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4); 3 // 成员变量 4 private String name; 5 private int index; 6 // 构造方法 7 private Color(String name, int index) { 8 this.name = name; 9 this.index = index; 10 } 11 //覆盖方法 12 @Override 13 public String toString() { 14 return this.index+"_"+this.name; 15 } 16 }
用法五:实现接口
所有的枚举都继承自java.lang.Enum类。由于Java 不支持多继承,所以枚举对象不能再继承其他类。
1 public interface Behaviour { 2 void print(); 3 String getInfo(); 4 } 5 public enum Color implements Behaviour{ 6 RED("红色", 1), GREEN("绿色", 2), BLANK("白色", 3), YELLO("黄色", 4); 7 // 成员变量 8 private String name; 9 private int index; 10 // 构造方法 11 private Color(String name, int index) { 12 this.name = name; 13 this.index = index; 14 } 15 //接口方法 16 @Override 17 public String getInfo() { 18 return this.name; 19 } 20 //接口方法 21 @Override 22 public void print() { 23 System.out.println(this.index+":"+this.name); 24 } 25 }
用法六:使用接口组织枚举
1 public interface Food { 2 enum Coffee implements Food{ 3 BLACK_COFFEE,DECAF_COFFEE,LATTE,CAPPUCCINO 4 } 5 enum Dessert implements Food{ 6 FRUIT, CAKE, GELATO 7 } 8 }
用法七:关于枚举集合的使用
java.util.EnumSet和java.util.EnumMap是两个枚举集合。EnumSet保证集合中的元素不重复;EnumMap中的 key是enum类型,而value则可以是任意类型。关于这个两个集合的使用就不在这里赘述,可以参考JDK文档。
关于枚举的实现细节和原理请参考:
参考资料:《ThinkingInJava》第四版 http://softbeta.iteye.com/blog/1185573
枚举类实现单例
enum SomeThing { INSTANCE; private Resource instance; SomeThing() { instance = new Resource(); } public Resource getInstance() { return instance; } public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //单例实现了安全 Resource instance = SomeThing.INSTANCE.getInstance(); Resource instance2 = SomeThing.INSTANCE.getInstance(); System.out.println(instance2.equals(instance2)); //反射获取实例,失败 Constructor<SomeThing> declaredConstructor = SomeThing.class.getDeclaredConstructor(String.class,int.class); declaredConstructor.setAccessible(true); SomeThing someThing = declaredConstructor.newInstance(); System.out.println(someThing); /* Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects at java.lang.reflect.Constructor.newInstance(Constructor.java:417) at com.bupt.SomeThing.main(TestEnum.java:141) */ } }