什么是单例模式?
保证整个系统中一个类只有一个对象的实例,实现这种功能的方式就叫单例模式。
饿汉式
特点:无论需不需要,都会加载对象
package com.wyh.single; /** * @program: JUC * @description: 单例模式--饿汉模式 * @author: 魏一鹤 * @createDate: 2022-03-12 22:17 **/ //饿汉式单例 一上来就能把东西接受加载 有可能会浪费内存 //单例模式最重要的思想:构造器私有 一旦构造器私有了,那么别人就无法new这个对象了 就保证内存种只有一个对象 public class Hungry { //一上来就接受 可能浪费空间 //正确的思想是:应该判断是否需要使用资源,需要使用的话才进行接受加载避免资源浪费,也就是懒汉模式 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]; //构造器私有 保证别人无法new这个对象,间接保证内存种只有一个实例 private Hungry(){ } //饿汉式单例不管怎么样,一上来先加载对象 也是唯一的对象 private final static Hungry hungry=new Hungry(); //对外方法 获取对象实例 public static Hungry getInstance(){ return hungry; } }
DCL懒汉式
特点:会通过双重锁判断对象是否需要加载
package com.wyh.single; /** * @program: JUC * @description: 单例模式--懒汉模式 * @author: 魏一鹤 * @createDate: 2022-03-12 22:29 **/ //懒汉模式相对于饿汉模式更加安全,会先判断是否需要使用资源,需要使用的话才进行加载,避免了资源浪费 public class Lazy { //构造器私有 private Lazy() { System.out.println(Thread.currentThread().getName() + "ok"); } //加载对象 不会直接一上来就加载对象 而是会进行判断是否需要进行使用 private static Lazy lazy; //获取实例方法 public static Lazy getInstance(){ //双重检测锁模式的懒汉式单例 DCL懒汉式单例 //判断对象不为空 不为空才会创建 if(lazy==null){ //保证Lazy只有一个 锁的是class synchronized (Lazy.class){ if(lazy==null){ lazy=new Lazy(); } } } return lazy; } //问题: 单线程下这个懒汉单例确实OK的,但是多线程并发就不行了 //以下为代码测试 多线程并发操作 public static void main(String[] args){ //10条线程并发测试 for (int i = 1; i <= 10; i++) { new Thread(()->{ Lazy.getInstance(); }).start(); } //如何解决这个问题呢? //获取对象实例的时候加synchronized锁 } }
那么,以上代码没有问题吗?
有的,因为创建实例对象并不是一个安全操作,在底层它分为三个步骤
1 分配内存空间
2 执行构造方法,初始化对象
3 把这个对象分配个这个内存空间,但是可能会被指令重排,所以必须使用volatile修饰懒汉实例对象,防止指令重排
//为了防止指令重排 懒汉对象必须使用volatile进行修饰防止指令重排! private static volatile Lazy lazy;
懒汉单例安全完整代码如下
package com.wyh.single; /** * @program: JUC * @description: 单例模式--懒汉模式 * @author: 魏一鹤 * @createDate: 2022-03-12 22:29 **/ //懒汉模式相对于饿汉模式更加安全,会先判断是否需要使用资源,需要使用的话才进行加载,避免了资源浪费 public class Lazy { //构造器私有 private Lazy() { } //加载对象 不会直接一上来就加载对象 而是会进行判断是否需要进行使用 //为了防止指令重排 懒汉对象必须使用volatile进行修饰防止指令重排! private static volatile Lazy lazy; //获取实例方法 public static Lazy getInstance(){ //双重检测锁模式的懒汉式单例 DCL懒汉式单例 //判断对象不为空 不为空才会创建 if(lazy==null){ //保证Lazy只有一个 锁的是class synchronized (Lazy.class){ if(lazy==null){ lazy=new Lazy(); //创建对象不是一个原子性操作 //创建分为三步 有可能会被指令重排 需要volatile修饰对象 //1 分配内存空间 2执行构造方法,初始化对象 3把这个对象指向这个空间 } } } return lazy; } }
静态内部类创建单例对象(不建议使用)
package com.wyh.single; /** * @program: JUC * @description: 单例模式--静态内部类 * @author: 魏一鹤 * @createDate: 2022-03-12 22:53 **/ //使用静态内部类实现单例模式 //把单例模式放在内部类种创建,创建的时候调用内部类创建对象 public class Holder { //只要是单例模型 必须构造器私有 private Holder() { } public static Holder getInstance(){ //调用内部类获取单例对象 return InnerClass.holder; } //内部类 public static class InnerClass{ //单例对象 private static final Holder holder=new Holder(); } }
通过反射破坏懒汉单例模式
1 1个反射对象
package com.wyh.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * @program: JUC * @description: 单例模式--懒汉模式 * @author: 魏一鹤 * @createDate: 2022-03-12 22:29 **/ //懒汉模式相对于饿汉模式更加安全,会先判断是否需要使用资源,需要使用的话才进行加载,避免了资源浪费 public class Lazy { //构造器私有 private Lazy() { System.out.println(Thread.currentThread().getName() + "ok"); } //加载对象 不会直接一上来就加载对象 而是会进行判断是否需要进行使用 //为了防止指令重排 懒汉对象必须使用volatile进行修饰防止指令重排! private static volatile Lazy lazy; //获取实例方法 public static Lazy getInstance(){ //双重检测锁模式的懒汉式单例 DCL懒汉式单例 //判断对象不为空 不为空才会创建 if(lazy==null){ //保证Lazy只有一个 锁的是class synchronized (Lazy.class){ if(lazy==null){ lazy=new Lazy(); //创建对象不是一个原子性操作 //创建分为三步 //1 分配内存空间 //2执行构造方法,初始化对象 //3把这个对象指向这个空间 需要volatile修饰对象 } } } return lazy; } //问题: 单线程下这个懒汉单例确实OK的,但是多线程并发就不行了 //以下为代码测试 多线程并发操作 //public static void main(String[] args){ // //10条线程并发测试 // for (int i = 1; i <= 10; i++) { // new Thread(()->{ // Lazy.getInstance(); // }).start(); // } // //如何解决这个问题呢? // //获取对象实例的时候加synchronized锁 //} //通过反射去破坏单例 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //获取懒汉单例实例 Lazy lazy1 = Lazy.getInstance(); //空参构造器 Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null); //无视私有构造器 declaredConstructor.setAccessible(true); //通过反射创建对象 Lazy lazy2 = declaredConstructor.newInstance(); //现在再测试懒汉单例模式是否被破坏 System.out.println(lazy1);//com.wyh.single.Lazy@74a14482 System.out.println(lazy2);//com.wyh.single.Lazy@1540e19d //很明显结果是不一样的 说明单例模式被反射破坏了 } }
mainok
mainok
com.wyh.single.Lazy@74a14482
com.wyh.single.Lazy@1540e19d
很明显值是不一样的 说明单例模式被反射破坏了,那么如何解决呢?
既然是通过破坏无参构造实现破坏单例模式
那么就可以再构造方法加锁并且判断对象是否已经被创建,如果对象已经被创建了,就进行错误提示
package com.wyh.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * @program: JUC * @description: 单例模式--懒汉模式 * @author: 魏一鹤 * @createDate: 2022-03-12 22:29 **/ //懒汉模式相对于饿汉模式更加安全,会先判断是否需要使用资源,需要使用的话才进行加载,避免了资源浪费 public class Lazy { //构造器私有 private Lazy() { //加锁 保证Lazy对象只能被创建一次 synchronized (Lazy.class){ //如果单例对象不等于空,那么说明已 经被创建了 if(lazy!=null){ //错误提示 throw new RuntimeException("不要视图使用反射破坏单例模式"); } } System.out.println(Thread.currentThread().getName() + "ok"); } //加载对象 不会直接一上来就加载对象 而是会进行判断是否需要进行使用 //为了防止指令重排 懒汉对象必须使用volatile进行修饰防止指令重排! private static volatile Lazy lazy; //获取实例方法 public static Lazy getInstance(){ //双重检测锁模式的懒汉式单例 DCL懒汉式单例 //判断对象不为空 不为空才会创建 if(lazy==null){ //保证Lazy只有一个 锁的是class synchronized (Lazy.class){ if(lazy==null){ lazy=new Lazy(); //创建对象不是一个原子性操作 //创建分为三步 //1 分配内存空间 //2执行构造方法,初始化对象 //3把这个对象指向这个空间 需要volatile修饰对象 } } } return lazy; } //通过反射去破坏单例 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //获取懒汉单例实例 Lazy lazy1 = Lazy.getInstance(); //空参构造器 Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null); //无视私有构造器 declaredConstructor.setAccessible(true); //通过反射创建对象 Lazy lazy2 = declaredConstructor.newInstance(); //现在再测试懒汉单例模式是否被破坏 System.out.println(lazy1);//com.wyh.single.Lazy@74a14482 System.out.println(lazy2);//com.wyh.single.Lazy@1540e19d //很明显值是不一样的 说明单例模式被反射破坏了 //如何解决呢? //既然是通过破坏无参构造实现破坏单例模式 //那么就可以再构造方法加锁并且判断对象是否已经被创建,如果对象已经被创建了,就进行错误提示 } }
mainok
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.wyh.single.Lazy.main(Lazy.java:72)
Caused by: java.lang.RuntimeException: 不要视图使用反射破坏单例模式
at com.wyh.single.Lazy.<init>(Lazy.java:23)
... 5 more
2 2个反射对象
我们只是通过加锁构造判断对象来结果反射带来的无参构造问题,现在两个对象对象都是通过反射来new的,该如何解决呢?
package com.wyh.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * @program: JUC * @description: 单例模式--懒汉模式 * @author: 魏一鹤 * @createDate: 2022-03-12 22:29 **/ //懒汉模式相对于饿汉模式更加安全,会先判断是否需要使用资源,需要使用的话才进行加载,避免了资源浪费 public class Lazy { //构造器私有 private Lazy() { //加锁 保证Lazy对象只能被创建一次 synchronized (Lazy.class){ //如果单例对象不等于空,那么说明已 经被创建了 if(lazy!=null){ //错误提示 throw new RuntimeException("不要视图使用反射破坏单例模式"); } } System.out.println(Thread.currentThread().getName() + "ok"); } //加载对象 不会直接一上来就加载对象 而是会进行判断是否需要进行使用 //为了防止指令重排 懒汉对象必须使用volatile进行修饰防止指令重排! private static volatile Lazy lazy; //获取实例方法 public static Lazy getInstance(){ //双重检测锁模式的懒汉式单例 DCL懒汉式单例 //判断对象不为空 不为空才会创建 if(lazy==null){ //保证Lazy只有一个 锁的是class synchronized (Lazy.class){ if(lazy==null){ lazy=new Lazy(); //创建对象不是一个原子性操作 //创建分为三步 //1 分配内存空间 //2执行构造方法,初始化对象 //3把这个对象指向这个空间 需要volatile修饰对象 } } } return lazy; } //通过反射去破坏单例 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //获取懒汉单例实例 //Lazy lazy1 = Lazy.getInstance(); //空参构造器 Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null); //无视私有构造器 declaredConstructor.setAccessible(true); //通过反射创建对象 Lazy lazy1 = declaredConstructor.newInstance(); Lazy lazy2 = declaredConstructor.newInstance(); //现在再测试懒汉单例模式是否被破坏 System.out.println(lazy1);//com.wyh.single.Lazy@74a14482 System.out.println(lazy2);//com.wyh.single.Lazy@1540e19d //很明显值是不一样的 说明单例模式被反射破坏了 //如何解决呢? //既然是通过破坏无参构造实现破坏单例模式 //那么就可以再构造方法加锁并且判断对象是否已经被创建,如果对象已经被创建了,就进行错误提示 } }
mainok
mainok
com.wyh.single.Lazy@74a14482
com.wyh.single.Lazy@1540e19d
可以通过定义标识的方法,判断是否非当前对象
package com.wyh.single; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; /** * @program: JUC * @description: 单例模式--懒汉模式 * @author: 魏一鹤 * @createDate: 2022-03-12 22:29 **/ //懒汉模式相对于饿汉模式更加安全,会先判断是否需要使用资源,需要使用的话才进行加载,避免了资源浪费 public class Lazy { //标识符 判断是否非当前对象 private static boolean flag=false; //构造器私有 private Lazy() { //加锁 保证Lazy对象只能被创建一次 synchronized (Lazy.class){ //如果标识符为false if(flag==false){ //把flag变为true flag = true; }else{ throw new RuntimeException("不要视图使用反射破坏单例模式"); } } System.out.println(Thread.currentThread().getName() + "ok"); } //加载对象 不会直接一上来就加载对象 而是会进行判断是否需要进行使用 //为了防止指令重排 懒汉对象必须使用volatile进行修饰防止指令重排! private static volatile Lazy lazy; //获取实例方法 public static Lazy getInstance(){ //双重检测锁模式的懒汉式单例 DCL懒汉式单例 //判断对象不为空 不为空才会创建 if(lazy==null){ //保证Lazy只有一个 锁的是class synchronized (Lazy.class){ if(lazy==null){ lazy=new Lazy(); //创建对象不是一个原子性操作 //创建分为三步 //1 分配内存空间 //2执行构造方法,初始化对象 //3把这个对象指向这个空间 需要volatile修饰对象 } } } return lazy; } //问题: 单线程下这个懒汉单例确实OK的,但是多线程并发就不行了 //以下为代码测试 多线程并发操作 //public static void main(String[] args){ // //10条线程并发测试 // for (int i = 1; i <= 10; i++) { // new Thread(()->{ // Lazy.getInstance(); // }).start(); // } // //如何解决这个问题呢? // //获取对象实例的时候加synchronized锁 //} //通过反射去破坏单例 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { //获取懒汉单例实例 //Lazy lazy1 = Lazy.getInstance(); //空参构造器 Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null); //无视私有构造器 declaredConstructor.setAccessible(true); //通过反射创建对象 Lazy lazy1 = declaredConstructor.newInstance(); Lazy lazy2 = declaredConstructor.newInstance(); //现在再测试懒汉单例模式是否被破坏 System.out.println(lazy1);//com.wyh.single.Lazy@74a14482 System.out.println(lazy2);//com.wyh.single.Lazy@1540e19d //很明显值是不一样的 说明单例模式被反射破坏了 //如何解决呢? //既然是通过破坏无参构造实现破坏单例模式 //那么就可以再构造方法加锁并且判断对象是否已经被创建,如果对象已经被创建了,就进行错误提示 } }
mainok
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.wyh.single.Lazy.main(Lazy.java:78)
Caused by: java.lang.RuntimeException: 不要视图使用反射破坏单例模式
at com.wyh.single.Lazy.<init>(Lazy.java:28)
... 5 more
3 破坏标识符 恶意修改
通过反射查到标识符,恶意进行修改,破坏单例
package com.wyh.single; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; /** * @program: JUC * @description: 单例模式--懒汉模式 * @author: 魏一鹤 * @createDate: 2022-03-12 22:29 **/ //懒汉模式相对于饿汉模式更加安全,会先判断是否需要使用资源,需要使用的话才进行加载,避免了资源浪费 public class Lazy { //标识符 判断是否非当前对象 private static boolean flag=false; //构造器私有 private Lazy() { //加锁 保证Lazy对象只能被创建一次 synchronized (Lazy.class){ //如果标识符为false if(flag==false){ //把flag变为true flag = true; }else{ throw new RuntimeException("不要试图使用反射破坏单例模式"); } } System.out.println(Thread.currentThread().getName() + "ok"); } //加载对象 不会直接一上来就加载对象 而是会进行判断是否需要进行使用 //为了防止指令重排 懒汉对象必须使用volatile进行修饰防止指令重排! private static volatile Lazy lazy; //获取实例方法 public static Lazy getInstance(){ //双重检测锁模式的懒汉式单例 DCL懒汉式单例 //判断对象不为空 不为空才会创建 if(lazy==null){ //保证Lazy只有一个 锁的是class synchronized (Lazy.class){ if(lazy==null){ lazy=new Lazy(); //创建对象不是一个原子性操作 //创建分为三步 //1 分配内存空间 //2执行构造方法,初始化对象 //3把这个对象指向这个空间 需要volatile修饰对象 } } } return lazy; } //问题: 单线程下这个懒汉单例确实OK的,但是多线程并发就不行了 //以下为代码测试 多线程并发操作 //public static void main(String[] args){ // //10条线程并发测试 // for (int i = 1; i <= 10; i++) { // new Thread(()->{ // Lazy.getInstance(); // }).start(); // } // //如何解决这个问题呢? // //获取对象实例的时候加synchronized锁 //} //通过反射去破坏单例 public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchFieldException { //获取懒汉单例实例 //Lazy lazy1 = Lazy.getInstance(); //通过反射找到标识符 Field flag = Lazy.class.getDeclaredField("flag"); //空参构造 Constructor<Lazy> declaredConstructor = Lazy.class.getDeclaredConstructor(null); //无视私有构造器 declaredConstructor.setAccessible(true); //通过反射创建对象 Lazy lazy1 = declaredConstructor.newInstance(); //第一次创建完之后,恶意修改flag 破坏反射 flag.set(lazy1,false); Lazy lazy2 = declaredConstructor.newInstance(); //现在再测试懒汉单例模式是否被破坏 System.out.println(lazy1);//com.wyh.single.Lazy@1540e19d System.out.println(lazy2); //com.wyh.single.Lazy@677327b6 //很明显值是不一样的 说明单例模式被反射破坏了 //如何解决呢? //既然是通过破坏无参构造实现破坏单例模式 //那么就可以再构造方法加锁并且判断对象是否已经被创建,如果对象已经被创建了,就进行错误提示 } }