• 设计模式之【单例模式】全解,单例模式实现方式,暴力打破单例模式与解决方案,你真的认识单例模式吗?



    全网最全最细的【设计模式】总目录,收藏起来慢慢啃,看完不懂砍我

    什么是单例模式

    单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

    单例模式中的“单例”概念其实有些笼统,很多博文中只介绍了一个进程内单例模式。其实单例模式有很多种,线程单例、进程单例、还是集群单例?

    接下来咱们一起来学习学习吧~

    单例模式的应用场景

    处理有线程冲突的资源

    
    public class Logger {
      private FileWriter writer;
      
      public Logger() {
        File file = new File("/Users/log.txt");
        writer = new FileWriter(file, true); //true表示追加写入
      }
      
      public void log(String message) {
        writer.write(message);
      }
    }
    
    // Logger类的应用示例:
    public class UserController {
      private Logger logger = new Logger();
      
      public void login(String username, String password) {
        // ...省略业务逻辑代码...
        logger.log(username + " logined!");
      }
    }
    
    public class OrderController {
      private Logger logger = new Logger();
      
      public void create(OrderVo order) {
        // ...省略业务逻辑代码...
        logger.log("Created an order: " + order.toString());
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32

    如上记录日志的方式,两个请求同时写同一个日志文件,完全有可能造成日志被覆盖的情况,log.txt应该是共享资源。

    对于线程不安全的问题,我们通常情况下都是加一把锁,但是此处加锁明显不是最优解,最好的办法就是将日志类定义为单例:

    
    public class Logger {
      private FileWriter writer;
      private static final Logger instance = new Logger();
    
      private Logger() {
        File file = new File("/Users/wangzheng/log.txt");
        writer = new FileWriter(file, true); //true表示追加写入
      }
      
      public static Logger getInstance() {
        return instance;
      }
      
      public void log(String message) {
        writer.write(mesasge);
      }
    }
    
    // Logger类的应用示例:
    public class UserController {
      public void login(String username, String password) {
        // ...省略业务逻辑代码...
        Logger.getInstance().log(username + " logined!");
      }
    }
    
    public class OrderController {  
      public void create(OrderVo order) {
        // ...省略业务逻辑代码...
        Logger.getInstance().log("Created a order: " + order.toString());
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    表示全局唯一类

    比如说java的Runtime类就是使用饿汉式实现的单例,表示全局唯一,一个进程中只能存在一个对象。

    public class Runtime {
        private static Runtime currentRuntime = new Runtime();
    
        /**
         * Returns the runtime object associated with the current Java application.
         * Most of the methods of class Runtime are instance
         * methods and must be invoked with respect to the current runtime object.
         *
         * @return  the Runtime object associated with the current
         *          Java application.
         */
        public static Runtime getRuntime() {
            return currentRuntime;
        }
    
        /** Don't let anyone else instantiate this class */
        private Runtime() {}
    
    	// other code...
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    单例模式的实现方式

    1、饿汉式之静态常量

    饿汉式就是在JVM加载这个类的时候就直接创建出该单例对象。

    /**
     * 饿汉式单例模式(静态变量)
     * 1.构造器私有化
     * 2.本类内部创建对象实例
     * 3.提供一个公有的静态方法,返回实例对象
     **/
    public class Hungry {
    
        private Hungry() {
    
        }
    
        private final static Hungry HUNGRY = new Hungry();
    
        public static Hungry getInstance() {
            return HUNGRY;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    2、饿汉式之静态代码块

    静态代码块的方式与静态常量的方式其实是一样的,都是在类加载的时候直接初始化。

    /**
     * 静态代码块饿汉式
     **/
    public class Hungry2 {
    
        private Hungry2() {
    
        }
    
        private static Hungry2 uniqueInstance;
    
        // 在静态代码块中创建单例对象
        static {
            uniqueInstance = new Hungry2();
        }
    
    
        public static Hungry2 getInstance() {
            return uniqueInstance;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    有人觉得这种实现方式不好,因为不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,我个人并不认同这样的观点。

    如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。

    如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

    3、懒汉式之线程不安全方式(不推荐)

    懒汉式其实就是懒加载的方式。

    /**
     **/
    public class LazyMan{
    
        // 创建一个静态变量来记录Singeleton类的唯一实例
        private static LazyMan uniqueInstance;
    
        // 私有化构造器,保证只有Singelton类内才可以调用
        private LazyMan() {}
    
        public static LazyMan getInstance() {
    
            if (uniqueInstance == null) {
                uniqueInstance = new LazyMan();
            }
    
            return uniqueInstance;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    上面的代码我们很明显的可以看出,单线程下似乎没有什么问题,但是多线程下,多个线程同时执行到if (uniqueInstance == null) ,就有可能创建出多个实例。

    4、懒汉式之加锁方式(不推荐)

    /**
     * 懒汉式单例模式,效率低
     **/
    public class LazyMan2 {
    
        private static LazyMan2 uniqueInstance;
    
        private LazyMan2 () { }
    
        // 通过synchronized在静态方法上加锁,使得每个线程在进入这个方法前都要等待其他线程的离开
        public static synchronized LazyMan2 getInstance() {
    
            if (uniqueInstance == null) {
    
                uniqueInstance = new LazyMan2();
            }
    
            return uniqueInstance;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    虽然多线程下安全了,但是加入了synchronized 锁,每次获取对象都要加一把锁,严重降低了性能。

    5、懒汉式之双重锁检查

    /**
     7. 懒汉式单例模式
     8. 双重检测锁: 效率高、线程安全且避免了内存浪费,但是不易理解(对新手不太友好)
     **/
    public class LazyMan4 {
    	
    	// volatile关键字可以确保uniqueInstance变量被初始化成LazyMan4实例时,多个线程正确处理uniqueInstance变量。
        private volatile static LazyMan4 uniqueInstance;
    
        private LazyMan4() {
            System.out.println(Thread.currentThread().getName() + " is ok");
        }
    
        public static LazyMan4 getInstance() {
    		
    		// 判断后续线程是否需要继续加锁
            if (uniqueInstance == null) {
    
                // 试图通过减少同步代码块的方式提高效率
                synchronized (LazyMan4.class) {
                    // 在给实例对象uniqueInstance赋值时,再判断一次是否为空
                    if (uniqueInstance == null) {
                        uniqueInstance = new LazyMan4();
                    }
                }
            }
    
            return uniqueInstance;
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31

    该方式引入了volatile轻量级锁,相对于直接使用synchronized来说的确是提升了性能,并且只有第一次初始化的时候才会使用到synchronized ,后续只需要返回实例对象即可。

    关于volatile的使用这里就不赘述了。

    6、静态内部类方式

    public class Singleton5 {
    
        private static class SingletonHolder {
            private static final Singleton5 INSTANCE = new Singleton5();
        }
    
        private Singleton5() {}
    
        public static Singleton5 getInstance() {
            return SingletonHolder.INSTANCE;
        }
    
    } 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    静态内部类方式可以轻松实现懒加载+线程安全(JVM类装载外部类的时候不会装载内部类)。

    7、枚举

    枚举天然就是一个单例的,也是Java四大名著中《Effective Java》里面的推荐写法。

    /**
     * 枚举自带单例模式
     **/
    public enum EnumSingleton {
    
        INSTANCE;
    
        public static EnumSingleton getInstance() {
    
            return INSTANCE;
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们来研究一下枚举的底层实现,我们在再一次点开枚举继承的抽象类Enum的底层源码,并且找到其中的valueOf()方法:

    public static <T extends Enum<T>> T valueOf(Class<T> enumType,
                                                    String name) {
    	T result = enumType.enumConstantDirectory().get(name);
    	if (result != null)
    		return result;
    	if (name == null)
    		throw new NullPointerException("Name is null");
    	throw new IllegalArgumentException(
    		"No enum constant " + enumType.getCanonicalName() + "." + name);
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    我们继续看这一行代码:

    T result = enumType.enumConstantDirectory().get(name);
    
    • 1
    Map<String, T> enumConstantDirectory() {
    	if (enumConstantDirectory == null) {
    		T[] universe = getEnumConstantsShared();
    		if (universe == null)
    			throw new IllegalArgumentException(
    				getName() + " is not an enum type");
    		Map<String, T> m = new HashMap<>(2 * universe.length);
    		for (T constant : universe)
    			m.put(((Enum<?>)constant).name(), constant);
    		enumConstantDirectory = m;
    		}
    	return enumConstantDirectory;
    }
    private volatile transient Map<String, T> enumConstantDirectory = null;
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这个时候我们会发现枚举常量字典enumConstantDirectory为Map类型,其中key为String类型,而value是一个泛型对象。其中key就是由我们自定义的,如上文中的INSTANCE;。所以,枚举是通过这个String类型的key,去拿到这个value,这才保证了单例模式的实现。但是我们发现了枚举常量字典中的常量二字。既然是常量的话,那么就意味着在类加载的时候就会赋值。这个时候,我们尴尬的发现,我们又回到了最初的起点 —> 饿汉式单例模式,JVM加载类的时候就初始化完成了。

    8、Spring IOC容器

    使用Spring容器可以完美实现单例模式。

    大致的逻辑参考如下:

    /**
     * 注册式单例,Spring中的做法
     **/
    public class ContainerSingleton {
    
        // 私有化构造器
        private ContainerSingleton() {
    
        }
    
        // 声明一个Map
        private static Map<String, Object> ioc = new ConcurrentHashMap<String, Object>();
    
        public static Object getInstance(String className) {
            synchronized (ioc) {
                if (!ioc.containsKey(className)) {
                    // 如果map中不存在这个全限定类名的key,那么需要放入新的数据
                    Object obj = null;
                    try {
                        // 利用全限定类名获取反射对象,再实现类的实例化
                        obj = Class.forName(className).newInstance();
                        // 把全限定类名以及对象,以key-vaule的形式放入map中
                        ioc.put(className, obj);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    return obj;
                } else {
                    // 如果map中存在这个全限定类名的key,直接通过这个key返回对应的对象
                    return ioc.get(className);
                }
            }
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    我们在Spring中使用起来也非常的方便,具体请查阅Spring中Bean的创建方式。

    9、使用CAS实现

    public class Singleton9 {
    
        private static final AtomicReference<Singleton9> INSTANCE = new AtomicReference<Singleton9>();
    
        private Singleton9() {
        }
    
        public static Singleton9 getInstance() {
            for (; ; ) {
                Singleton9 current = INSTANCE.get();
    
                if (current != null) {
                    return current;
                }
    
                current = new Singleton9();
    
                if (INSTANCE.compareAndSet(null, current)) {
                    return current;
                }
            }
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    该方式用的比较少,写法比较麻烦,但是也算是一种方式。

    暴力打破单例模式的方式

    1、反射打破饿汉式的方式

    import java.lang.reflect.Constructor;
     
    /**
     * 反射:程序运行阶段,获取某一个类的所有属性和方法
     * 所以 反射是对单例模式起到破坏的作用
     * 下面以饿汉式为例,进行演示
     */
    public class DestroySingleton {
        public static void main(String [] args){
            /*反射对单例模式的破坏*/
            //1、获取类对象
            Class<Singleton_1> singleton_1Class = Singleton_1.class;
            //2、获取私有的构造方法
            try {
                Constructor<Singleton_1> declaredConstructor = singleton_1Class.getDeclaredConstructor();
                //3、取消Java语言的访问检查 暴力访问
                declaredConstructor.setAccessible(true);
                //4、通过构造 创建对象
                Singleton_1 singleton_1 = declaredConstructor.newInstance();
                Singleton_1 singleton_2 = declaredConstructor.newInstance();
                System.out.println(singleton_1 == singleton_2);
     
            } catch (Exception e) {
                e.printStackTrace();
            }
     
     
        }
    }
    //饿汉式 单例模式
    class Singleton_1{
        //构造方法私有
        private Singleton_1(){
            
        };
        //属性私有
        private static Singleton_1 singleton_1 = new Singleton_1();
        //提供对外的访问方法
        public static Singleton_1 getInstance(){
            return singleton_1;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42

    我们可以发现以上代码可以打破单例模式。

    如何解决这种情况呢?我们可以在私有构造方法中加入判断:

    //饿汉式 单例模式
    class Singleton_1{
        //构造方法私有
        private Singleton_1(){
            //防止反射对单例模式的破坏
            if (singleton_1 != null){
                throw new RuntimeException("不允许反射访问。。。");
            }
        };
        //属性私有
        private static Singleton_1 singleton_1 = new Singleton_1();
        //提供对外的访问方法
        public static Singleton_1 getInstance(){
            return singleton_1;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    再次通过反射创建单例的时候,会直接抛出异常了~

    2、通过序列化打破饿汉式的方式

    
    import java.io.*;
    public class DestroySingleton {
        public static void main(String [] args){
            /*序列化对单例模式的破坏*/
            Singleton_1 s1 = null;
            Singleton_1 s2 = Singleton_1.getInstance();
    
            FileOutputStream fos = null;
            try {
    
                fos = new FileOutputStream("E:\\Singleton_1.obj");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(s2);
                oos.flush();
                oos.close();
    
                FileInputStream fis = new FileInputStream("E:\\Singleton_1.obj");
                ObjectInputStream ois = new ObjectInputStream(fis);
                s1 = (Singleton_1) ois.readObject();
                ois.close();
    
                System.out.println(s1);
                System.out.println(s2);
                System.out.println(s1 == s2);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
    
     
        }
    }
    //饿汉式 单例模式,序列化破解的方式只有实现Serializable才可以
    class Singleton_1 implements Serializable {
        //构造方法私有
        private Singleton_1(){
            //防止反射对单例模式的破坏,为了线程安全可以引入synchronized 锁定这里
            if (singleton_1 != null){
                throw new RuntimeException("不允许反射访问。。。");
            }
        };
        //属性私有
        private static Singleton_1 singleton_1 = new Singleton_1();
        //提供对外的访问方法
        public static Singleton_1 getInstance(){
            return singleton_1;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    我们发现,序列化方式打破单例更加暴力,即使在构造方法抛出异常也不能规避。

    如何解决呢?我们只要加上readResolve()方法即可,来看优化后的代码:

    //饿汉式 单例模式,序列化破解的方式只有实现Serializable才可以
    class Singleton_1 implements Serializable {
        //构造方法私有
        private Singleton_1(){
            //防止反射对单例模式的破坏
            if (singleton_1 != null){
                throw new RuntimeException("不允许反射访问。。。");
            }
        };
        //属性私有
        private static Singleton_1 singleton_1 = new Singleton_1();
        //提供对外的访问方法
        public static Singleton_1 getInstance(){
            return singleton_1;
        }
    
        // 具体原理就是 ObjectInputStream类的readObject()方法,感兴趣可以研究研究。
        private Object readResolve(){
            return singleton_1;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    虽然增加了readResolve()方法返回实例解决了单例模式破坏的问题,但是实际上实例化了两次,只不过新创建的对象没有返回而已,如果创建对象的动作发生频率加快,就意味着内存分配也会随之增大。

    3、通过反射打破懒汉式的方式

    上面我们通过反射打破饿汉式,通过在构造方法抛异常的方式可以解决。

    那么这种方式可以解决懒汉式的这种问题吗?

    
    import java.lang.reflect.Constructor;
    
    public class DestroySingleton {
        public static void main(String[] args) {
            /*反射对单例模式的破坏*/
            //1、获取类对象
            Class<Singleton_1> singleton_1Class = Singleton_1.class;
            //2、获取私有的构造方法
            try {
                Constructor<Singleton_1> declaredConstructor = singleton_1Class.getDeclaredConstructor();
                //3、取消Java语言的访问检查 暴力访问
                declaredConstructor.setAccessible(true);
                //4、通过构造 创建对象
                Singleton_1 singleton_1 = declaredConstructor.newInstance();
                Singleton_1 singleton_2 = declaredConstructor.newInstance();
                System.out.println(singleton_1 == singleton_2);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
    
    
        }
    }
    
    //懒汉式 单例模式
    class Singleton_1 {
        //构造方法私有
        private Singleton_1() {
            //防止反射对单例模式的破坏
            if (singleton_1 != null) {
                throw new RuntimeException("不允许反射访问。。。");
            }
        }
    
        //属性私有
        private volatile static Singleton_1 singleton_1;
    
        //提供对外的访问方法
        public static Singleton_1 getInstance() {
            // 判断后续线程是否需要继续加锁
            if (singleton_1 == null) {
    
                // 试图通过减少同步代码块的方式提高效率
                synchronized (Singleton_1.class) {
                    // 在给实例对象uniqueInstance赋值时,再判断一次是否为空
                    if (singleton_1 == null) {
                        singleton_1 = new Singleton_1();
                    }
                }
            }
    
            return singleton_1;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56

    我们发现执行结果是false,并不会解决这个问题。

    要想彻底不想通过反射打破懒汉式的单例,解决起来还是很麻烦的,这里就不深入追究了(可以加个内部变量等方法)。

    4、通过序列化打破懒汉式的方式

    序列化方式打破懒汉式也是很暴力,

    解决办法跟饿汉式一样,加一个readResolve方法。

    private Object readResolve(){
        return singleton_1;
    }
    
    • 1
    • 2
    • 3

    5、通过反射打破静态内部类的方式

    import java.lang.reflect.Constructor;
    
    /**
     * 静态内部类
     * 1.类装载的时候,静态内部类是不会被装载(懒加载,以外部类的装载不会导致内部类的装载)
     * 2.当调用getInstance()方法的时候,会导致静态内部类被装载,而且只会被装载一次
     * 3.在装载的时候线程是安全的。(JVM底层类装载机制)
     **/
    public class Holder {
    	
    	// 私有化构造器
        private Holder() {
    
        }
    	
    	// 提供一个全局访问点,返回静态内部类中的静态常量
        public static Holder getInstance() {
            return InnerClass.HOLDER;
        }
    	
    	// 在静态内部类中创建一个静态常量并将一个外部类的实例赋值给它。
        public static class InnerClass {
            private static final Holder HOLDER = new Holder();
        }
    
        public static void main(String[] args) throws Exception {
            // 用提供的唯一全局访问点获取实例对象
            Holder instance = Holder.getInstance();
            // 获取Holder的反射对象
            Class<Holder> clazz = Holder.class;
            // 通过反射对象获取Holder的构造器
            Constructor<Holder> declaredConstructor = clazz.getDeclaredConstructor();
            // 私有访问授权
            declaredConstructor.setAccessible(true);
            // 创建Holder的实例对象
            Holder instance2 = declaredConstructor.newInstance();
            System.out.println(instance == instance2);
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40

    我们发现可以使用反射轻松打破。

    此时我们可以在私有构造方法中加判断:

    public class Holder {
    	
    	// 私有化构造器
        private Holder() {
            // 为了线程安全,可以加synchronized锁
            if (InnerClass.HOLDER != null) {
                throw new RuntimeException("小朋友,不要试图用反射搞破坏!");
            }
        }
    	
    	// 提供一个全局访问点,返回静态内部类中的静态常量
        public static Holder getInstance() {
            return InnerClass.HOLDER;
        }
    	
    	// 在静态内部类中创建一个静态常量并将一个外部类的实例赋值给它。
        public static class InnerClass {
            private static final Holder HOLDER = new Holder();
        }
    
        public static void main(String[] args) throws Exception {
            // 获取Holder的反射对象
            Class<Holder> clazz = Holder.class;
            // 通过反射对象获取Holder的构造器
            Constructor<Holder> declaredConstructor = clazz.getDeclaredConstructor();
            // 私有访问授权
            declaredConstructor.setAccessible(true);
            // 创建Holder的实例对象
            Holder instance2 = declaredConstructor.newInstance();
            Holder instance3 = declaredConstructor.newInstance();
            System.out.println(instance3 == instance2);
        }
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    这种写法虽然干脆利落,却直接封杀了反射的可能性,甚至通过反射来第一次获取单例对象都不可以了,只能通过getInstance方法来获取。

    6、通过反射破坏枚举

    public enum EnumSingleton {
    
        INSTANCE;
    
        public static EnumSingleton getInstance() {
    
            return INSTANCE;
        }
    
        public static void main(String[] args) throws Exception {
            // 获取枚举EnumSingleton的反射对象
            Class<EnumSingleton> clazz = EnumSingleton.class;
            // 利用反射对象获取EnumSingleton的构造器
            Constructor<EnumSingleton> declaredConstructor = clazz.getDeclaredConstructor();
            // 私有访问授权
            declaredConstructor.setAccessible(true);
            // 利用反射获得的构造器实现EnumSingleton的实例化
            EnumSingleton instance = declaredConstructor.newInstance();
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    我们执行一下发现,竟然直接报错了!
    在这里插入图片描述

    查看反射创建对象newInstance()方法的底层源码:

        @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;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    关键代码:

    if ((clazz.getModifiers() & Modifier.ENUM) != 0)
        throw new IllegalArgumentException("Cannot reflectively create enum objects");
    
    • 1
    • 2

    但是,为什么提示我们java.lang.NoSuchMethodException呢?
    我们查看一下枚举继承的抽象类Enum的底层源码,发现其中会有这么一个带双参的构造方法,而不是无参:

     protected Enum(String name, int ordinal) {
    	this.name = name;
    	this.ordinal = ordinal;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5

    哦,那我们利用反射获取这个双参的构造方法就好了。再次修改代码如下:

    public static void main(String[] args) throws Exception {
        // 获取枚举EnumSingleton的反射对象
        Class<EnumSingleton> clazz = EnumSingleton.class;
        // 利用反射对象获取EnumSingleton的构造器
        Constructor<EnumSingleton> declaredConstructor = clazz.getDeclaredConstructor(String.class, int.class);
        // 私有访问授权
        declaredConstructor.setAccessible(true);
        // 利用反射获得的构造器实现EnumSingleton的实例化
        EnumSingleton instance = declaredConstructor.newInstance("ccc", 666);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    获取到我们想要的异常啦!
    在这里插入图片描述
    所以,枚举实现起来又方便又安全,推荐这种方式!

    如何实现线程唯一

    “进程唯一”指的是进程内唯一,进程间不唯一。类比一下,“线程唯一”指的是线程内唯一,线程间可以不唯一。实际上,“进程唯一”还代表了线程内、线程间都唯一,这也是“进程唯一”和“线程唯一”的区别之处。

    本文以上都是实现的是“进程唯一”,那如何实现“线程唯一”呢?

    public class IdGenerator {
      private AtomicLong id = new AtomicLong(0);
    
      private static final ConcurrentHashMap<Long, IdGenerator> instances
              = new ConcurrentHashMap<>();
    
      private IdGenerator() {}
    
      public static IdGenerator getInstance() {
        Long currentThreadId = Thread.currentThread().getId();
        instances.putIfAbsent(currentThreadId, new IdGenerator());
        return instances.get(currentThreadId);
      }
    
      public long getId() {
        return id.incrementAndGet();
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    以上代码是使用Map,我们都知道每一个线程都有一个唯一的id,我们用key为线程的Id,就可以实现线程唯一的单例啦!

    还有一种方式是,ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

    public class Singleton8 {
    
        private static final ThreadLocal<Singleton8> tlSingleton = new ThreadLocal<Singleton8>() {
            @Override
            protected Singleton8 initialValue() {
                return new Singleton8();
            }
        };
    
        private Singleton8() {}
    
        public static Singleton8 getInstance() {
            return tlSingleton.get();
        }
        
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    实现多集群下的单例

    那恐怕只能使用redis、数据库等,将单例对象存放在公共的资源中了。

    实现一个多例模式

    “单例”指的是,一个类只能创建一个对象。对应地,“多例”指的就是,一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。如果用代码来简单示例一下的话,就是下面这个样子:

    
    public class BackendServer {
      private long serverNo;
      private String serverAddress;
    
      private static final int SERVER_COUNT = 3;
      private static final Map<Long, BackendServer> serverInstances = new HashMap<>();
    
      static {
        serverInstances.put(1L, new BackendServer(1L, "192.134.22.138:8080"));
        serverInstances.put(2L, new BackendServer(2L, "192.134.22.139:8080"));
        serverInstances.put(3L, new BackendServer(3L, "192.134.22.140:8080"));
      }
    
      private BackendServer(long serverNo, String serverAddress) {
        this.serverNo = serverNo;
        this.serverAddress = serverAddress;
      }
    
      public BackendServer getInstance(long serverNo) {
        return serverInstances.get(serverNo);
      }
    
      public BackendServer getRandomInstance() {
        Random r = new Random();
        int no = r.nextInt(SERVER_COUNT)+1;
        return serverInstances.get(no);
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    实际上,对于多例模式,还有一种理解方式:同一类型的只能创建一个对象,不同类型的可以创建多个对象。这里的“类型”如何理解呢?

    我们还是通过一个例子来解释一下,具体代码如下所示。在代码中,logger name 就是刚刚说的“类型”,同一个 logger name 获取到的对象实例是相同的,不同的 logger name 获取到的对象实例是不同的。

    
    public class Logger {
      private static final ConcurrentHashMap<String, Logger> instances
              = new ConcurrentHashMap<>();
    
      private Logger() {}
    
      public static Logger getInstance(String loggerName) {
        instances.putIfAbsent(loggerName, new Logger());
        return instances.get(loggerName);
      }
    
      public void log() {
        //...
      }
    }
    
    //l1==l2, l1!=l3
    Logger l1 = Logger.getInstance("User.class");
    Logger l2 = Logger.getInstance("User.class");
    Logger l3 = Logger.getInstance("Order.class");
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    这种多例模式的理解方式有点类似工厂模式。它跟工厂模式的不同之处是,多例模式创建的对象都是同一个类的对象,而工厂模式创建的是不同子类的对象。实际上,它还有点类似享元模式,两者的区别等到我们讲到享元模式的时候再来分析。除此之外,实际上,枚举类型也相当于多例模式,一个类型只能对应一个对象,一个类可以创建多个对象。

    参考资料

    https://blog.csdn.net/Qizhi_Hu/article/details/106451236
    王争老师《设计模式之美》
    https://blog.csdn.net/mnimxq/article/details/124526216

  • 相关阅读:
    10_13C++
    Linux6.1中为什么用Radix树替换位图(bitmap)来管理进程pid
    android FM的流程
    SpringCloud原生组件之Eureka服务注册与发现
    I2S/PCM知识点记录
    雷电模拟器dnconsole命令汇总
    《运营商劫持, 中间人攻击, 黑客入侵怎么办?》- HTTPS 技术反制
    多显示屏,将Qt程序显示在指定显示器上
    [蓝桥杯 2018 省 A] 航班时间
    【技术积累】Vue.js中的核心知识【四】
  • 原文地址:https://blog.csdn.net/A_art_xiang/article/details/127646394