精华内容
下载资源
问答
  • 单例模式详解
    2022-03-28 10:36:53

    单例模式:

    或许最简单的设计范式就是“单子”(Singleton) ,它能提供对象的一个(而且只有一个)实例。单子在Java库中得到了应用,但下面这个例子显得更直接一些:

    
    //: SingletonPattern. java
    
    // The Singleton design pattern: you can
    
    //never instantiate more than one.
    
    
    //Since this isn't inherited from a Cloneable
    
    //base class and cloneability isn't added,
    
    //making it final prevents cloneability from
    
    //being added in any derived classes:
    
    final class Singleton{
    
        private static Singleton s = new Singleton(47);
    
        private int i;
    
        private Singleton(int x){ i = x; }
    
        public static Singleton getHandle(){
    
            return s;
    
        }
    
        public int getValue(){ return i;}
    
        public void setValue(int x){ i = x; }
    
    }
    
    
    public class SingletonPattern {
    
        public static void main(String[] args) {
    
            Singleton s = Singleton.getHandle();
    
            System.out.println(s.getValue());
    
            Singleton s2 = Singleton.getHandle();
    
            s2.setValue(9);
    
            System.out.println(s.getValue());
    
            try{
    
                //Can't do this:compile-time error.
    
                //Singleton s3 = (Singleton)s2.clone();
    
            }catch(Exception e){
    
    
            }
    
        }
    
    }
    

    创建单子的关键就是防止客户程序员采用除由我们提供的之外的任何一种方式来创建一个对象。必须将所有构建器都设为private (私有),而且至少要创建一个构建器,以防止编译器帮我们自动同步一个默认构建器(它会自做聪明地创建成为“友好的”——friendly, 而非private)。

    此时应决定如何创建自己的对象。在这儿,我们选择了静态创建的方式。但亦可选择等候客户程序员发出一个创建请求,然后根据他们的要求动态创建。不管在哪种情况下,对象都应该保存为“私有”属性。我们通过公用方法提供访问途径。在这里,getHandle() 会产生指向Singleton的一个句柄。剩下的接口(getValue()和setValue())属于普通的类接口。

    Java也允许通过克隆(Clone) 方式来创建一个对象。在这个例子中,将类设为final可禁止克隆的发生。由于Singleton是从0bject直接继承的,所以clone ()方法会保持protected (受保护)属性,不能够使用它(强行使用会造成编译期错误)。然而,假如我们是从一个类结构中继承,那个结构已经过载了clone ()方法,使其具有public属性,并实现了Cloneable,那么为了禁止克隆,需要过载clone(),并掷出一一个CloneNotSupportedException (不支持克隆违例)。亦可过载clone(),并简单地返回this。那样做会造成一定的混淆,因为客户程序员可能错误地认为对象尚未克隆,仍然操纵的是原来的那个。

    注意我们并不限于只能创建一个对象。亦可利用该技术创建一个有限的对象池。但在那种情况下,可能需要解决池内对象的共享问题。如果不幸真的遇到这个问题,可以自己设计一套方案,实现共享对象的登记与撤消登记。

    单例模式定义:确保一个类最多只有一个实例,并提供一个全局访问点。

    单例模式可以分为两种:预加载(饿汉模式)和懒加载(懒汉模式)

    1.预加载(饿汉模式):

    顾名思义,就是预先加载。再进一步解释就是还没有使用该单例对象,但是,该单例对象就已经被加载到内存了。

    
    public class PreloadSingleton {
    
        public static PreloadSingleton instance = new PreloadSingleton();
    
    
        //其他的类无法实例化单例类的对象
    
        private PreloadSingleton(){
    
    
        };
    
    
        public static PreloadSingleton getInstance() {
    
            return instance;
    
        }
    
    }
    

    很明显,没有使用该单例对象,该对象就被加载到了内存,会造成内存的浪费。

    2.懒加载(懒汉模式):

    为了避免内存的浪费,我们可以采用懒加载,即用到该单例对象的时候再创建,在类加载时,不创建实例,因此类加载速度快,但运行时获取对象的速度慢。

    
    public class Singleton {
    
        private static volatile Singleton instance = null;
    
    
        private Singleton(){
    
    
        };
    
    
        public static Singleton getInstance(){
    
            if(instance == null){
    
                instance = new Singleton();
    
            }
    
            return instance;
    
        }
    
    }
    

    3.单例模式和线程安全

    (1)预加载只有一条语句return instance,这显然可以保证线程安全。但是,我们知道预加载会造成内存的浪费。

    (2)懒加载不浪费内存,但是无法保证线程的安全。首先,if判断以及其内存执行代码是非原子性的。其次,new Singleton()无法保证执行的顺序性。

    不满足原子性或者顺序性,线程肯定是不安全的,这是基本的常识,不再赘述。我主要讲一下为什么new Singleton()无法保证顺序性。我们知道创建一个对象分三步:

    
    memory=allocate();//1:初始化内存空间
    
    
    ctorInstance(memory);//2:初始化对象
    
    
    instance=memory();//3:设置instance指向刚分配的内存地址
    
    

    jvm为了提高程序执行性能,会对没有依赖关系的代码进行重排序,上面2和3行代码可能被重新排序。我们用两个线程来说明线程是不安全的。线程A和线程B都创建对象。其中,A2和A3的重排序,将导致线程B在B1处判断出instance不为空,线程B接下来将访问instance引用的对象。此时,线程B将会访问到一个还未初始化的对象(线程不安全)。

    时间

    线程A

    线程B

    t1

    A1:分配对象的内存空间

    t2

    A3:设置instance指向内存空间

    t3

    B1:判断instance是否为空

    t4

    B2:由于instance不为null,线程B将访问instance引用的对象

    t5

    A2:初始化对象

    t6

    A4:访问instance引用的对象

    4.保证懒加载(懒汉模式)的线程安全

    我们首先想到的就是使用synchronized关键字。synchronized加载getInstace()函数上确实保证了线程的安全。但是,如果要经常的调用getInstance()方法,不管有没有初始化实例,都会唤醒和阻塞线程。为了避免线程的上下文切换消耗大量时间,如果对象已经实例化了,我们没有必要再使用synchronized加锁,直接返回对象。

    此外我们通过第三点的讨论知道new一个对象的代码是无法保证顺序性的,因此,我们需要使用另一个关键字volatile保证对象实例化过程的顺序性。
     

    
    public class penguin {
    
        private static volatile penguin m_penguin = null;
    
        // 避免通过new初始化对象
    
        private void penguin() {}
    
        public void beating() {
    
            System.out.println("打墨墨");
    
        };
    
        public static penguin getInstance() {
    
            if (null == m_penguin) {
    
                synchronized(penguin.class) {
    
                    if (null == m_penguin) {
    
                        m_penguin = new penguin();
    
                    }
    
                }
    
            }
    
            return m_penguin;
    
        }
    
    }

    5.懒汉模式实现要点

    1. 单例使用volatile修饰;
    2. 单例实例化时,要用synchronized 进行同步处理;
    3. 双重null判断。

    6.适用场景:

    单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:

    1. 需要频繁实例化然后销毁的对象。
    2. 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
    3. 有状态的工具类对象。
    4. 频繁访问数据库或文件的对象。

    7.优缺点:

    优点:

    1. 在单例模式中,活动的单例只有一个实例,对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化,确保所有的对象都访问一个实例
    2. 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性。
    3. 提供了对唯一实例的受控访问。
    4. 由于在系统内存中只存在一个对象,因此可以 节约系统资源,当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。
    5. 避免对共享资源的多重占用。

    缺点:

    1. 不适用于变化的对象,如果同一类型的对象总是要在不同的用例场景发生变化,单例就会引起数据的错误,不能保存彼此的状态。
    2. 由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。
    3. 单例类的职责过重,在一定程度上违背了“单一职责原则”。
    4. 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。

    8.volatile问题

    大家有没有注意到,单例模式中用到了关键字volatile,在PHP和Go中没有类似的关键字,但是JAVA必须加,当初还有疑问,我们先看一下volatile的作用:

    volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

    下面直接总结一下volatile的作用:

    1. 它会强制将对缓存的修改操作立即写入主存,让所有的线程可见;
    2. 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
    3. 如果是写操作,它会导致其他CPU中对应的缓存行无效。
    更多相关内容
  • PHP单例模式详解 单例模式的概念 单例模式是指整个应用中某个类只有一个对象实例的设计模式。具体来说,作为对象的创建方式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统全局的提供这个实例。它...
  • 本文实例讲述了JavaScript设计模式—单例模式.分享给大家供大家参考,具体如下: 单例模式也称为单体模式,其中: 1,单体模式用于创建命名空间,将系列关联的属性和方法组织成一个逻辑单元,减少全局变量。  逻辑...
  • 这一次重温一下《JavaScript设计模式与开发实践》,开篇为单例模式。 /** * pre 单例模式 * 定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点 * 应用:单例模式是一种常用的模式,有一些对象我们...
  • 设计模式--单例模式详解

    千次阅读 2022-04-13 19:02:45
    单例模式的定义 单例模式(Singleton Pattern )是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛,例如,公司CEO、部门经理等。...

    本专栏内容参考自:咕泡学院Tom老师的《Spring5核心原理与30个类手写实战》,仅作个人学习记录使用,如有侵权,联系速删。

    单例模式的定义

    单例模式(Singleton Pattern )是指确保一个类在任何情况下都绝对只有一个实例,并提供一个全局访问点。单例模式是创建型模式。单例模式在现实生活中应用也非常广泛,例如,公司CEO、部门经理等。J2EE标准中的ServletContext,ServletContextConfig等、Spring框架应用中的ApplicationContext、数据库的连接池等也都是单例形式。

    饿汉式单例模式

    饿汉式单例模式在类加载的时候就立即初始化,并且创建单例对象。它绝对线程安全,在线程还没
    出现以前就实例化了,不可能存在访问安全问题。

    /**
     * 优点:执行效率高,性能高,没有任何的锁
     * 缺点:某些情况下,可能会造成内存浪费
     */
    public class HungrySingleton {
    
        private static final HungrySingleton hungrySingleton = new HungrySingleton();
    
        private HungrySingleton(){}
    
        public static HungrySingleton getInstance(){
            return  hungrySingleton;
        }
    }
    

    还有另外一种写法,利用静态代码块的机制:

    public class HungryStaticSingleton {
        //先静态后动态
        //先上,后下
        //先属性后方法
        private static final HungryStaticSingleton hungrySingleton;
    
        //装个B
        static {
            hungrySingleton = new HungryStaticSingleton();
        }
    
        private HungryStaticSingleton(){}
    
        public static HungryStaticSingleton getInstance(){
            return  hungrySingleton;
        }
    }
    

    这两种写法都非常简单,也非常好理解,饿汉式单例模式适用于单例对象较少的情况。这样写可以保证绝对线程安全、执行效率比较高。但是它的缺点也很明显,就是所有对象类加载的时候就实例化。
    这样一来,如果系统中有大批量的单例对象存在,那系统初始化是就会导致大量的内存浪费。也就是说,
    不管对象用与不用都占着空间,浪费了内存,有可能“占着茅坑不拉屎”。那有没有更优的写法呢?下
    面我们来继续分析。

    懒汉式单例模式

    为了解决饿汉式单例可能带来的内存浪费问题,于是就出现了懒汉式单例的写法,懒汉式单例模式的特点是,单例对象要在被使用时才会初始化,下面看懒汉式单例模式的简单实现
    LazySimpleSingleton :

    /**
     * 优点:节省了内存,线程安全
     * 缺点:性能低
     */
    public class LazySimpleSingletion {
        private static LazySimpleSingletion instance;
        private LazySimpleSingletion(){}
    
        public static LazySimpleSingletion getInstance(){
            if(instance == null){
                instance = new LazySimpleSingletion();
            }
            return instance;
        }
    }
    

    但这样写又带来了一个新的问题,如果在多线程环境下,就会出现线程安全问题。我先来模拟一下,
    编写线程类ExectorThread :

    public class ExectorThread implements Runnable{
    	@Override
        public void run() {
            LazySimpleSingletion instance = LazySimpleSingletion.getInstance();
            LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
        }
    }
    

    客户端测试代码如下:

    public class LazySimpleSingletonTest {
        public static void main(String[] args) {
            Thread t1 = new Thread(new ExectorThread());
            Thread t2 = new Thread(new ExectorThread());
            t1.start();
            t2.start();
            System.out.println("End");
        }
    }
    

    运行结果:
    在这里插入图片描述
    果然,上面的代码有一定概率出现两种不同结果,这意味着上面的单例存在线程安全隐患
    我们打上断点,一步一步调试,通过不断切换线程,并观测其内存状态,我们发现在线程环境下LazySimpleSingleton被实例化了两次。有时我们得到的运行结果可能是相同的两个对象,实际上是被后面执行的线程覆盖了,我们看到了一个假象,线程安全隐患依旧存在。那么,我们如何来优化代码,使得懒汉式单例模式在线程环境下安全呢?来看下面的代码给getInstance()加上synchronized关键字,使这个方法变成线程同步方法∶

    public class LazySimpleSingletion {
        private static LazySimpleSingletion instance;
        private LazySimpleSingletion(){}
    
        public synchronized static LazySimpleSingletion getInstance(){
            if(instance == null){
                instance = new LazySimpleSingletion();
            }
            return instance;
        }
    }
    

    我们再来调试。当执行其中一个线程并调用getInstance()方法时,另一个线程在调用getInstance()方法,线程的状态由RUNNING变成了MONITOR,出现阻塞。直到第一个线程执行完,第二个线程才恢复到RUNNING状态继续调用getInstance(方法,如下图所示。
    在这里插入图片描述
    上图完美地展现了synchronized监视锁的运行状态,线程安全的问题解决了。但是,用synchronized加锁时,在线程数量比较多的情况下,如果CPU分配压力上升,则会导致大批线程阻塞,从而导致程序性能大幅下降。那么,有没有一种更好的方式,既能兼顾线程安全又能提升程序性能呢?答案是肯定的。我们来看双重检查锁的单例模式∶

    /**
     * 优点:性能高了,线程安全了
     * 缺点:可读性难度加大,不够优雅
     */
    public class LazyDoubleCheckSingleton {
        private volatile static LazyDoubleCheckSingleton instance;
        private LazyDoubleCheckSingleton(){}
    
        public static LazyDoubleCheckSingleton getInstance(){
            //检查是否要阻塞
            if (instance == null) {
                synchronized (LazyDoubleCheckSingleton.class) {
                    //检查是否要重新创建实例
                    if (instance == null) {
                        instance = new LazyDoubleCheckSingleton();
                        //指令重排序的问题
                    }
                }
            }
            return instance;
        }
    }
    

    当第一个线程调用getInstance()方法时,第二个线程也可以调用。当第一个线程执行到synchronized时会上锁,第二个线程就会变成MONITOR状态,出现阻塞。此时,阻塞并不是基于整个LazySimpleSingleton类的阻塞,而是在getInstance()方法内部的阻塞,只要逻辑不太复杂,对于调用者而言感知不到。
    但是,用到synchronized 关键字总归要上锁,对程序性能还是存在一定影响的。难道就真的没有
    更好的方案吗﹖当然有。我们可以从类初始化的角度来考虑,看下面的代码,采用静态内部类的方式:

    public class LazyStaticInnerClassSingleton {
    
        private LazyStaticInnerClassSingleton(){
        }
        
        private static final LazyStaticInnerClassSingleton getInstance(){
            return LazyHolder.INSTANCE;
        }
    
        private static class LazyHolder{
            private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
        }
    
    }
    

    这种方式兼顾了饿汉式单例模式的内存浪费问题和synchronized的性能问题。内部类一定是要在方法调用之前初始化,巧妙地避免了线程安全问题。由于这种方式比较简单,我就不带大家一步一步调试了。但是,金无足赤,人无完人,单例模式亦如此。这种写法真的就完美了吗?

    反射破坏单例

    现在我们来看一个事故现场。大家有没有发现,上面介绍的单例模式的构造方法除了加上private
    关键字,没有做任何处理。如果我们使用反射来调用其构造方法,再调用getInstance()方法,应该有
    两个不同的实例。现在来看一段测试代码,以LazyInnerClassSingleton为例:

    public class ReflectTest {
    
        public static void main(String[] args) {
            try {
                //在很无聊的情况下进行破坏
                Class<?> clazz = LazyStaticInnerClassSingleton.class;
                //通过反射获取私有的构造方法
                Constructor c = clazz.getDeclaredConstructor(null);
                //授权强制访问
                c.setAccessible(true);
                //实例化
                Object instance1 = c.newInstance();
                //再次实例化
                Object instance2 = c.newInstance();
                
                System.out.println(instance1);
    
                System.out.println(instance2);
    
                System.out.println(instance1 == instance2);
    
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
    

    运行结果如下图:
    在这里插入图片描述
    显然,创建了两个不同的实例,不符合单例模式的定义。那怎么办呢?我们来做一次优化。现在,我们在其构造方法中做一
    些限制,一旦出现多次重复创建,则直接抛出异常。来看优化后的代码︰

    public class LazyStaticInnerClassSingleton {
    
        private LazyStaticInnerClassSingleton(){
            if(LazyHolder.INSTANCE != null){
                throw new RuntimeException("不允许非法访问");
            }
        }
    
        private static LazyStaticInnerClassSingleton getInstance(){
            return LazyHolder.INSTANCE;
        }
    
        private static class LazyHolder{
            private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
        }
    
    }
    

    再次运行,会得到以下结果:
    在这里插入图片描述
    至此,自认为史上最牛的单例模式的实现方式便大功告成,避免了线程问题,反射破坏,性能尚可,且写法优雅。但是,上面看似完美的单例写法还是有可能被破坏。

    序列化破坏单例

    一个单例对象创建好后,有时候需要将对象序列化然后写入磁盘,下次使用时再从磁盘中读取对象并进行反序列化,将其转化为内存对象。反序列化后的对象会重新分配内存,即重新创建。如果序列化的目标对象为单例对象,就违背了单例模式的初衷,相当于破坏了单例,来看一段代码︰

    public class SeriableSingleton implements Serializable {
    
    
        //序列化
        //把内存中对象的状态转换为字节码的形式
        //把字节码通过IO输出流,写到磁盘上
        //永久保存下来,持久化
    
        //反序列化
        //将持久化的字节码内容,通过IO输入流读到内存中来
        //转化成一个Java对象
    
    
        public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
        private SeriableSingleton(){}
    
        public static SeriableSingleton getInstance(){
            return INSTANCE;
        }
    
    }
    

    测试代码:

    public class SeriableSingletonTest {
        public static void main(String[] args) {
    
            SeriableSingleton s1 = null;
            SeriableSingleton s2 = SeriableSingleton.getInstance();
    
            FileOutputStream fos = null;
            try {
    
                fos = new FileOutputStream("SeriableSingleton.obj");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(s2);
                oos.flush();
                oos.close();
    
                FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
                ObjectInputStream ois = new ObjectInputStream(fis);
                s1 = (SeriableSingleton)ois.readObject();
                ois.close();
    
                System.out.println(s1);
                System.out.println(s2);
                System.out.println(s1 == s2);
    
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    

    运行结果如下图:
    在这里插入图片描述
    从运行结果可以看出,反序列化后的对象和手动创建的对象是不一致的,实例化了两次,违背了单例模式的设计初衷。那么,我们如何保证在序列化的情况下也能够实现单例模式呢﹖其实很简单,只需要增加readResolve()方法即可。来看优化后的代码︰

    public class SeriableSingleton implements Serializable {
    
    
        //序列化
        //把内存中对象的状态转换为字节码的形式
        //把字节码通过IO输出流,写到磁盘上
        //永久保存下来,持久化
    
        //反序列化
        //将持久化的字节码内容,通过IO输入流读到内存中来
        //转化成一个Java对象
    
    
        public  final static SeriableSingleton INSTANCE = new SeriableSingleton();
        private SeriableSingleton(){}
    
        public static SeriableSingleton getInstance(){
            return INSTANCE;
        }
        
        //添加此方法即可
        private Object readResolve(){
            return INSTANCE;
        }
    }
    

    再次运行,结果如下:
    在这里插入图片描述
    大家一定会想∶这是什么原因呢﹖为什么要这样写?看上去很神奇的样子,也让人有些费解。不如我们一起来看看JDK的源码实现以了解清楚。我们进入ObjectInputStream类的readObject()方法,
    代码如下:

    private final Object readObject(Class<?> type)
            throws IOException, ClassNotFoundException
        {
            if (enableOverride) {
                return readObjectOverride();
            }
    
            if (! (type == Object.class || type == String.class))
                throw new AssertionError("internal error");
    
            // if nested read, passHandle contains handle of enclosing object
            int outerHandle = passHandle;
            try {
                Object obj = readObject0(type, false);
                handles.markDependency(outerHandle, passHandle);
                ClassNotFoundException ex = handles.lookupException(passHandle);
                if (ex != null) {
                    throw ex;
                }
                if (depth == 0) {
                    vlist.doCallbacks();
                }
                return obj;
            } finally {
                passHandle = outerHandle;
                if (closed && depth == 0) {
                    clear();
                }
            }
        }
    

    我们可以发现,代码中调用了重写的readObject()方法,让我们点进去:

    private Object readObject0(Class<?> type, boolean unshared) throws IOException {
            boolean oldMode = bin.getBlockDataMode();
            if (oldMode) {
                int remain = bin.currentBlockRemaining();
                if (remain > 0) {
                    throw new OptionalDataException(remain);
                } else if (defaultDataEnd) {
                    /*
                     * Fix for 4360508: stream is currently at the end of a field
                     * value block written via default serialization; since there
                     * is no terminating TC_ENDBLOCKDATA tag, simulate
                     * end-of-custom-data behavior explicitly.
                     */
                    throw new OptionalDataException(true);
                }
                bin.setBlockDataMode(false);
            }
    
            byte tc;
            while ((tc = bin.peekByte()) == TC_RESET) {
                bin.readByte();
                handleReset();
            }
    
            depth++;
            totalObjectRefs++;
            try {
                switch (tc) {
                    case TC_NULL:
                        return readNull();
    
                    case TC_REFERENCE:
                        // check the type of the existing object
                        return type.cast(readHandle(unshared));
    
                    case TC_CLASS:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast a class to java.lang.String");
                        }
                        return readClass(unshared);
    
                    case TC_CLASSDESC:
                    case TC_PROXYCLASSDESC:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast a class to java.lang.String");
                        }
                        return readClassDesc(unshared);
    
                    case TC_STRING:
                    case TC_LONGSTRING:
                        return checkResolve(readString(unshared));
    
                    case TC_ARRAY:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast an array to java.lang.String");
                        }
                        return checkResolve(readArray(unshared));
    
                    case TC_ENUM:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast an enum to java.lang.String");
                        }
                        return checkResolve(readEnum(unshared));
    
                    case TC_OBJECT:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast an object to java.lang.String");
                        }
                        return checkResolve(readOrdinaryObject(unshared));
    
                    case TC_EXCEPTION:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast an exception to java.lang.String");
                        }
                        IOException ex = readFatalException();
                        throw new WriteAbortedException("writing aborted", ex);
    
                    case TC_BLOCKDATA:
                    case TC_BLOCKDATALONG:
                        if (oldMode) {
                            bin.setBlockDataMode(true);
                            bin.peek();             // force header read
                            throw new OptionalDataException(
                                bin.currentBlockRemaining());
                        } else {
                            throw new StreamCorruptedException(
                                "unexpected block data");
                        }
    
                    case TC_ENDBLOCKDATA:
                        if (oldMode) {
                            throw new OptionalDataException(true);
                        } else {
                            throw new StreamCorruptedException(
                                "unexpected end of block data");
                        }
    
                    default:
                        throw new StreamCorruptedException(
                            String.format("invalid type code: %02X", tc));
                }
            } finally {
                depth--;
                bin.setBlockDataMode(oldMode);
            }
        }
    

    代码挺长,我们注意一下这一段

    case TC_OBJECT:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast an object to java.lang.String");
                        }
                        return checkResolve(readOrdinaryObject(unshared));
    

    我们看到里面调用了readOrdinaryObject()方法,让我们再点进去看看:

    private Object readOrdinaryObject(boolean unshared)
            throws IOException
        {
            if (bin.readByte() != TC_OBJECT) {
                throw new InternalError();
            }
    
            ObjectStreamClass desc = readClassDesc(false);
            desc.checkDeserialize();
    
            Class<?> cl = desc.forClass();
            if (cl == String.class || cl == Class.class
                    || cl == ObjectStreamClass.class) {
                throw new InvalidClassException("invalid class descriptor");
            }
    
            Object obj;
            try {
                obj = desc.isInstantiable() ? desc.newInstance() : null;
            } catch (Exception ex) {
                throw (IOException) new InvalidClassException(
                    desc.forClass().getName(),
                    "unable to create instance").initCause(ex);
            }
    
            passHandle = handles.assign(unshared ? unsharedMarker : obj);
            ClassNotFoundException resolveEx = desc.getResolveException();
            if (resolveEx != null) {
                handles.markException(passHandle, resolveEx);
            }
    
            if (desc.isExternalizable()) {
                readExternalData((Externalizable) obj, desc);
            } else {
                readSerialData(obj, desc);
            }
    
            handles.finish(passHandle);
    
            if (obj != null &&
                handles.lookupException(passHandle) == null &&
                desc.hasReadResolveMethod())
            {
                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;
        }
    

    注意一下里面的isInstantiable(),在这一段

    Object obj;
            try {
            //这里这里
                obj = desc.isInstantiable() ? desc.newInstance() : null;
            } catch (Exception ex) {
                throw (IOException) new InvalidClassException(
                    desc.forClass().getName(),
                    "unable to create instance").initCause(ex);
            }
    

    点进去,可以看到里面很简单:

        boolean isInstantiable() {
            requireInitialized();
            return (cons != null);
        }
    

    判断了一下构造方法是否为空,不为空就返回true,这意味着只要有无参的构造方法,就会实例化。

    然后再往上返回,回到ObjectInputStream的readOrdinaryObject()方法,找到这一块

            if (obj != null &&
                handles.lookupException(passHandle) == null &&
                desc.hasReadResolveMethod())
    

    我们可以看到代码里在判断无参构造方法之后,又调用了hasReadResolveMethod()这个方法,来看代码:

        boolean hasReadResolveMethod() {
            requireInitialized();
            return (readResolveMethod != null);
        }
    

    就两三行,逻辑很简单,判断readResolveMethod 是否为空,不为空就返回true,那么readResolveMethod是在哪里赋值的呢?通过全局查找知道,在私有方法ObjectStreamClass()中给readResolveMethod进行了赋值,来看代码︰

    //533行
    readResolveMethod = getInheritableMethod(
                            cl, "readResolve", null, Object.class);
    

    上面的逻辑其实就是通过反射找到一个无参的 readResolve()方法,并且保存下来。现在回到ObjectinputStream的readOrdinaryObject(方法继续往下看,如果readResolve()方法存在则调用invokeReadResolve()方法,来看代码︰

    Object invokeReadResolve(Object obj)
            throws IOException, UnsupportedOperationException
        {
            requireInitialized();
            if (readResolveMethod != null) {
                try {
                    return readResolveMethod.invoke(obj, (Object[]) null);
                } catch (InvocationTargetException ex) {
                    Throwable th = ex.getTargetException();
                    if (th instanceof ObjectStreamException) {
                        throw (ObjectStreamException) th;
                    } else {
                        throwMiscException(th);
                        throw new InternalError(th);  // never reached
                    }
                } catch (IllegalAccessException ex) {
                    // should not occur, as access checks have been suppressed
                    throw new InternalError(ex);
                }
            } else {
                throw new UnsupportedOperationException();
            }
        }
    

    我们可以看到,在invokeReadResolve()方法中用反射调用了readResolveMethod方法。
    通过JDK源码分析我们可以看出,虽然增加readResolve()方法返回实例解决了单例模式被破坏的
    问题,但是实际上实例化了两次,只不过新创建的对象没有被返回而已。如果创建对象的动作发生频率
    加快,就意味着内存分配开销也会随之增大,难道真的就没办法从根本上解决问题吗﹖下面讲的注册式单例也许能帮助到你。

    注册式单例模式

    注册式单例模式又称为登记式单例模式,就是将每一个实例都登记到某一个地方,使用唯一的标识
    获取实例。注册式单例模式有两种:一种为枚举式单例模式,另一种为容器式单例模式。

    1 枚举式单例模式

    先来看枚举式单例模式的写法,来看代码,创建EnumSingleton类:

    public enum EnumSingleton {
        INSTANCE;
    
        private Object data;
    
        public Object getData() {
            return data;
        }
    
        public void setData(Object data) {
            this.data = data;
        }
    
        public static EnumSingleton getInstance(){
            return INSTANCE;
        }
    }
    

    写一下测试代码:

    public class EnumSingletonTest2 {
        public static void main(String[] args) {
            try {
                EnumSingleton instance1 = null;
                EnumSingleton instance2 = EnumSingleton.getInstance();
                instance2.setData(new Object());
                FileOutputStream fos = new FileOutputStream("EnumSingleton.obj");
                ObjectOutputStream oos = new ObjectOutputStream(fos);
                oos.writeObject(instance2);
                oos.flush();
                oos.close();
    
                FileInputStream fis = new FileInputStream("EnumSingleton.obj");
                ObjectInputStream ois = new ObjectInputStream(fis);
                instance1 = (EnumSingleton) ois.readObject();
                ois.close();
    
                System.out.println(instance1.getData());
                System.out.println(instance2.getData());
                System.out.println(instance1.getData() == instance2.getData());
    
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
    
        }
    }
    

    结果如下:
    在这里插入图片描述
    没有做任何处理,我们发现运行结果和预期的一样。那么枚举式单例模式如此神奇,它的神秘之处
    在哪里体现呢?下面通过分析源码来揭开它的神秘面纱。
    下载一个非常好用的Java 反编译工具Jad(下载地址: https://varaneckas.com/jad/ ),解压后配置好环境变量(这里不做详细介绍),就可以使用命令行调用了。找到工程所在的Class目录,复制EnumSingleton.class所在的路径,如下图所示。
    在这里插入图片描述
    然后切换到命令行,切换到工程所在的Class目录,输入命令jad并在后面输入复制好的路径,在
    Class目录下会多出一个EnumSingleton.jad文件。打开EnumSingleton.jad文件我们惊奇地发现有
    如下代码︰

    static
    {
    	INSTANCE = new EnumSingLeton("INSTANCE",0);
    	$VALUES = (new EnumSingleton[] {
    		INSTANCE
    		});
    }
    

    原来,枚举式单例模式在静态代码块中就给INSTANCE进行了赋值,是饿汉式单例模式的实现。至此,我们还可以试想,序列化能否破坏枚举式单例模式呢?不妨再来看一下JDK源码,还是回到ObjectInputStream的readObject0(方法∶找到这一块

                    case TC_ENUM:
                        if (type == String.class) {
                            throw new ClassCastException("Cannot cast an enum to java.lang.String");
                        }
                        return checkResolve(readEnum(unshared));
    
    

    我们看到,在readObject0(中调用了readEnum()方法,来看readEnum()方法的代码实现︰

        private Enum<?> readEnum(boolean unshared) throws IOException {
            if (bin.readByte() != TC_ENUM) {
                throw new InternalError();
            }
    
            ObjectStreamClass desc = readClassDesc(false);
            if (!desc.isEnum()) {
                throw new InvalidClassException("non-enum class: " + desc);
            }
    
            int enumHandle = handles.assign(unshared ? unsharedMarker : null);
            ClassNotFoundException resolveEx = desc.getResolveException();
            if (resolveEx != null) {
                handles.markException(enumHandle, resolveEx);
            }
    
            String name = readString(false);
            Enum<?> result = null;
            Class<?> cl = desc.forClass();
            if (cl != null) {
                try {
                    @SuppressWarnings("unchecked")
                    Enum<?> en = Enum.valueOf((Class)cl, name);
                    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;
        }
    

    我们发现,枚举类型其实通过类名和类对象类找到一个唯一的枚举对象。因此,枚举对象不可能被
    类加载器加载多次。那么反射是否能破坏枚举式单例模式呢﹖来看一段测试代码︰

    public class EnumSingletonTest3 {
        public static void main(String[] args) {
            try {
                Class clazz = EnumSingleton.class;
                Constructor c = clazz.getDeclaredConstructor();
                c.newInstance();
    
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
    
        }
    }
    

    运行结果如下:
    在这里插入图片描述

    结果中报的是java.lang.NoSuchMethodException异常,意思是没找到无参的构造方法。这时候,
    我们打开java.lang.Enum的源码,查看它的构造方法,只有一个protected类型的构造方法,代码如
    下:

        protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
    

    然后我们将测试方法改一下:

    public class EnumSingletonTest3 {
        public static void main(String[] args) {
            try {
                Class clazz = EnumSingleton.class;
                Constructor c = clazz.getDeclaredConstructor(String.class,int.class);
                c.setAccessible(true);
                EnumSingleton enumSingleton = (EnumSingleton) c.newInstance("zwq",666);
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
    
        }
    }
    

    结果变成了这样:
    在这里插入图片描述
    这时错误已经非常明显了,“Cannot reflectively create enum objects",即不能用反射来创建
    枚举类型。我们还是习惯性地想来看看JDK源码,进入Constructor的newlnstance()方法︰

        @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;
        }
    

    从上述代码可以看到,在newInstance()方法中做了强制性的判断,如果修饰符是Modifier.ENUM枚举类型,则直接抛出异常。这岂不是和静态内部类的处理方式有异曲同工之妙?对,但是我们自己再构造方法中写逻辑处理可能存在未知的风险,而JDK的处理是最官方、最权威、最稳定的。因此枚举式
    单例模式也是《Effective Java》书中推荐的一种单例模式实现写法。
    到此为止,我们是不是已经非常清晰明了呢?JDK枚举的语法特殊性及反射也为枚举保驾护航,让
    枚举式单例模式成为一种比较优雅的实现。

    2 容器式单例模式

    其实枚举式单例,虽然写法优雅,但是也会有一些问题。因为它在类加载之时就将所有的对象初始化放在类内存中,这其实和饿汉式并无差异,不适合大量创建单例对象的场景。那么,接下来看注册式
    单例模式的另一种写法,即容器式单例模式,创建ContainerSingleton类:

    public class ContainerSingleton {
    
        private ContainerSingleton(){}
    
        private static Map<String,Object> ioc = new ConcurrentHashMap<String, Object>();
    
        public static Object getInstance(String className){
            Object instance = null;
            if(!ioc.containsKey(className)){
                try {
                    instance = Class.forName(className).newInstance();
                    ioc.put(className, instance);
                }catch (Exception e){
                    e.printStackTrace();
                }
                return instance;
            }else{
                return ioc.get(className);
            }
        }
    
    }
    

    容器式单例模式适用于需要大量创建单例对象的场景,便于管理。但它是非线程安全的。到此,注
    册式单例模式介绍完毕。我们有兴趣可以看看Spring中的容器式单例模式的实现代码︰

    AbstractAutowireCapableBeanFactory
    

    这个类里面可以看到符合单例模式的一切定义

    线程单例实现ThreadLocal

    ThreadLocal不能保证其创建的对象是全局唯一的,但是能保证在单个线程中是唯一的,天生是线程安全的。下面来看代码︰

    public class ThreadLocalSingleton {
        private static final ThreadLocal<ThreadLocalSingleton> threadLocaLInstance =
                new ThreadLocal<ThreadLocalSingleton>(){
                    @Override
                    protected ThreadLocalSingleton initialValue() {
                        return new ThreadLocalSingleton();
                    }
                };
    
        private ThreadLocalSingleton(){}
    
        public static ThreadLocalSingleton getInstance(){
            return threadLocaLInstance.get();
        }
    }
    

    写个测试代码:

    public class ThreadLocalSingletonTest {
    
        public static void main(String[] args) {
            System.out.println(ThreadLocalSingleton.getInstance());
            System.out.println(ThreadLocalSingleton.getInstance());
            System.out.println(ThreadLocalSingleton.getInstance());
    
            Thread t1 = new Thread(new ExectorThread());
            Thread t2 = new Thread(new ExectorThread());
            t1.start();
            t2.start();
            System.out.println("End");
        }
    }
    

    运行结果:
    在这里插入图片描述
    我们发现,在主线程中无论调用多少次,获取到的实例都是同一个,但是在两个子线程中分别获取到
    了不同的实例。那么ThreadLocal是如何实现这样的效果的呢?我们知道,单例模式为了达到线程安全
    的目的,会给方法上锁,以时间换空间。ThreadLocal将所有的对象全部放在ThreadLocalMap中,
    为每个线程都提供一个对象,实际上是以空间换时间来实现线程隔离的。

    展开全文
  • 设计模式之单例模式详解(附应用举例实现),包含三种实现单例模式的方法

    1 单例模式介绍

    单例模式(Singleton Pattern)确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。

    例如Windows任务管理器,在正常情况下只能打开唯一一个任务管理器。

    image-20220422171912798

    单例模式是一种对象创建型模式,其有三个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。

    主要解决: 一个全局使用的类频繁地创建与销毁。

    何时使用: 当您想控制实例数目,节省系统资源的时候。

    如何解决: 判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

    关键代码: 构造函数是私有的。

    应用实例:

    • 1、一个班级只有一个班主任。
    • 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
    • 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

    优点:

    • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
    • 2、避免对资源的多重占用(比如写文件操作)。

    缺点: 没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

    使用场景:

    • 1、要求生产唯一序列号。
    • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
    • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

    注意事项: getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

    2 单例模式详解

    2.1 单例模式结构

    单例模式是结构最简单的设计模式,它只包含一个类,即单例类。单例模式的结构图如下。

    image-20220422172423873

    由图可知,单例模式只包含一个单例角色,也就是Singleton。对于Singleton(单例),在单例类的内部创建它的唯一实例,并通过静态方法getInstance()让客户端可以使用它的唯一实例;为了防止在外部对单例类实例化,将其构造函数的可见性设为private;在单例类内部定义了一个Singleton类型的静态对象作为可供外部访问的唯一实例。

    2.2 单例模式实现

    典型的单例模式的实现代码如下:

    public class Singleton {
        // 静态私有成员变量
        private static Singleton instance = null;  
        // 私有构造函数
        private Singleton() {	
        }
    	
        // 静态公有工厂方法,返回唯一实例
        public static Singleton getInstance() {
            if(instance == null)
                instance = new Singleton();	
            return instance;
        }
    }
    

    2.3 单例模式应用举例

    • 题目描述

      某软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高了系统的整体处理能力,缩短了响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键,试使用单例模式设计服务器负载均衡器。

    • UML类图

      image-20220422195354566

      其中将负载均衡器LoadBalance设计为单例类,其中包含一个存储服务器信息的集合serverList,每次在serverList中随机选择一台服务器来响应客户端的请求。

    • 代码

      代码地址

    3 饿汉式单例与懒汉式单例

    3.1 饿汉式单例

    饿汉式单例(Eager Singleton)是实现起来最简单的单例类,饿汉式单例类结构图如下。

    image-20220422200347381

    有图中我们可以看出,由于在定义静态变量的时候实例化单例类,因此在类加载时单例对象就已创建,代码如下:

    public class EagerSingleton { 
        private static final EagerSingleton instance = new EagerSingleton(); 
        private EagerSingleton() { } 
    
        public static EagerSingleton getInstance() {
            return instance; 
        }
    }
    

    当类被加载时,静态变量instance会被初始化,此时类的私有构造函数就会被调用,单例类的唯一实例将被创建。

    3.2 懒汉式单例与双重检查锁定

    与饿汉式单例相同的是,懒汉式单例(Lazy Singleton)的构造函数也是私有的;与饿汉式单例类不同的是,懒汉式单例类在第一次被引用时将自己实例化,在懒汉式单例类被加载时不会将自己实例化。懒汉式单例类的结构图如下。

    image-20220422200830831

    但如果多个线程同时访问将导致创建多个单例对象!这个时候为了避免多个线程同时调用getInstance()方法,可以使用关键字synchronized,代码如下:

    public class LazySingleton { 
        private static LazySingleton instance = null; 
    
        private LazySingleton() { } 
    
        // 使用synchronized关键字对方法加锁,确保任意时刻只有一个线程可以执行该方法
        synchronized public static LazySingleton getInstance() { 
            if (instance == null) {
                instance = new LazySingleton(); 
            }
            return instance; 
        }
    }
    

    在上述懒汉式单例类中,在getInstance()方法前面增加了关键字synchronized进行线程锁定,已处理多个线程同时访问问题。但我们每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发环境中将会导致性能大大降低。因此可以继续对懒汉式单例进行改进,我们发现无需对getInstance()方法进行锁定,仅需锁定代码段instance = new LazySingleton()即可。故可进行如下改进:

    public static LazySingleton getInstance() { 
        if (instance == null) {
            synchronized (LazySingleton.class) {
                instance = new LazySingleton(); 
            }
        }
        return instance; 
    }
    

    问题看似解决,但如果使用上述代码,实际上还是会存在单例对象不唯一的情况。因为线程A和线程B如果同时进入判断,由于锁的原因,一个会先创建,但是另一个并不知道对象已经创建,这样就会导致产生多个实例对象。违背了单例模式的设计思想。我们需要使用双重检查锁定,即在锁内再进行一次instance == null的判断。使用双重检查锁定实现的懒汉式单例类的完整代码如下:

    public class LazySingleton { 
        private volatile static LazySingleton instance = null; 
    
        private LazySingleton() { } 
    
        public static LazySingleton getInstance() { 
            //第一重判断
            if (instance == null) {
                //锁定代码块
                synchronized (LazySingleton.class) {
                    //第二重判断
                    if (instance == null) {
                        instance = new LazySingleton(); //创建单例实例
                    }
                }
            }
        return instance; 
        }
    }
    
    

    需要注意的时,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理。

    3.3 饿汉式单例类与懒汉式单例类的比较

    • 饿汉式单例类:无须考虑多个线程同时访问的问题;调用速度和反应时间优于懒汉式单例;资源利用效率不及懒汉式单例;系统加载时间可能会比较长。
    • 懒汉式单例类:实现了延迟加载;必须处理好多个线程同时访问的问题;需通过双重检查锁定等机制进行控制,将导致系统性能受到一定影响。

    4 使用静态内部类实现单例模式

    饿汉式单例类不能实现延迟加载,不管将来用不用始终占用内存;懒汉式单例类安全控制烦琐,而且性能受影响。可见它们都存在一些问题,为了克服这些问题,在Java语言中可以通过Initialization on Demand Holder(IoDH)技术来实现单例模式。

    在IoDH中,需要在单例类中增加一个静态内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用,实现代码如下:

    //Initialization on Demand Holder
    public class Singleton {
        private Singleton() {
        }
    
        //静态内部类
        private static class HolderClass {
            private final static Singleton instance = new Singleton();
        }
    
        public static Singleton getInstance() {
            return HolderClass.instance;
        }
    
        public static void main(String args[]) {
            Singleton s1, s2; 
            s1 = Singleton.getInstance();
            s2 = Singleton.getInstance();
            System.out.println(s1==s2);
        }
    }
    

    通过使用IoDH既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式;其缺点是与编程语言本身的特性相关,很多面向对象语言并不支持IoDH。

    展开全文
  • js单例模式详解实例

    2020-12-11 13:23:04
    什么是单例单例要求一个类有且只有一个实例,提供一个全局的访问点。因此它要绕过常规的控制器,使其只能有一个实例,供使用者使用,而使用着不关心有几个实例,因此这是设计者的责任 代码如下:In JavaScript, ...
  • 2、缩小命名空间 单例模式是对全局变量的一种改进。它避免了那些存储唯一实例的全局变量污染命名空间 3、允许对操作和表示的精华 单例类可以有子类。而且用这个扩展类的实例来配置一个应用是很容易的。你可以用你所...
  • 单例模式有两种实现方式,一种是饿汉式,一种是懒汉式。 饿汉式:类加载到内存后,就实例化一个单例,JVM保证线程安全,简单实用,推荐使用!唯一缺点,不管用到与否,类装载时就完成实例化,也就是Class.forName(...

    单例模式有两种实现方式,一种是饿汉式,一种是懒汉式。

    饿汉式:类加载到内存后,就实例化一个单例,JVM保证线程安全,简单实用,推荐使用!唯一缺点,不管用到与否,类装载时就完成实例化,也就是Class.forName("")加载到内存就会实例化。(不过话又说回来,你如果不用它,你要装载它干啥)。

    懒汉式:类加载到内容后,不会实例化一个单例,而是在需要时才实例化,但是实现这个方式需要考虑到一些问题,下面我们来分析。

     

    1、饿汉式

    一、直接初始化

    public class MgrTest01 {
        private static final MgrTest01 INSTANCE = new MgrTest01();
    
        private MgrTest01() {};
    
        public static MgrTest01 getInstance(){
            return INSTANCE;
        }
    
        public static void main(String[] args) {
            MgrTest01 mgrTest011 = MgrTest01.getInstance();
            MgrTest01 mgrTest012 = MgrTest01.getInstance();
    
            System.out.println(mgrTest011 == mgrTest012);
        }
    }

    执行结果:true

    二、使用静态语句初始化(本质上和直接初始化没有什么区别)

    public class MgrTest02 {
        private static final MgrTest02 INSTANCE;
    
        static {
            INSTANCE = new MgrTest02();
        }
    
        private MgrTest02() {};
    
        public static MgrTest02 getInstance(){
            return INSTANCE;
        }
    
        public static void main(String[] args) {
            MgrTest02 mgrTest011 = MgrTest02.getInstance();
            MgrTest02 mgrTest012 = MgrTest02.getInstance();
    
            System.out.println(mgrTest011 == mgrTest012);
        }
    }

    结果:true

    2、懒汉式

    一、按需初始化

    public class MgrTest03 {
        private static MgrTest03 INSTANCE;
    
        private MgrTest03() {};
    
        public static MgrTest03 getInstance()  {
            if(null == INSTANCE){
                try {
                    Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new MgrTest03();
            }
            return INSTANCE;
        }
    
        public static void main(String[] args) {
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest03.getInstance())).start();
            }
            try {
                Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest03.getInstance())).start();
            }
        }
    }

    执行结果:获取的实例对象可能会不同(虽然达到了按需初始化的目的,但是却带来了线程不安全的问题)

    问题原因:因为在对象还没有创建之前。多个线程同时调用getInstance方法获取实例的时候,可能存在第一个线程进入了if语句,但是还没有来的及执行实例化对象,后面线程也进入了if语句。等到第一个线程实例化之后,虽然这个时候再有线程调用getInstance,不会再进入if语句直接拿对象,但是已经进入if语句的线程又创建了新的对象。(注意:我们可以看到前五次的对象可能不是同一个,但是后五次肯定是同一个了(中间加入延迟是模拟在对象创建之后再调用getInstance的场景),所以这个问题是在对象还没有创建之前,然后有多个线程同时调用getInstance方法可能出现的问题

    二、可以通过synchronized来解决上个问题

    public class MgrTest04 {
        private static MgrTest04 INSTANCE;
    
        private MgrTest04() {};
    
        public static synchronized MgrTest04 getInstance()  {
            if(null == INSTANCE){
                try {
                    Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new MgrTest04();
            }
            return INSTANCE;
        }
    
        public static void main(String[] args) {
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest04.getInstance())).start();
            }
            try {
                Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest04.getInstance())).start();
            }
        }
    }

    执行结果:获取的实例对象都是同一个(可以实现懒加载,并且不会有线程安全的问题,但是效率下降)

    但是又引发了另一个问题,每次调用getInstance方法的时候都要加锁,因为调用加了synchronized的方法每次都要去判断有没有申请到这把锁,执行效率就降低了。本来我们只是解决在INSTANCE还没有实例化的时候线程安全问题,而INSTANCE初始化之后调用getInstance方法是不会有线程安全问题的,所以我们只需要在INSTANCE为空的时候才需要加锁获取,已经不为空了就没有必要还加锁获取。

     三、通过减小同步代码快的方式提高效率,但是不可行(需要注意)

    public class MgrTest05 {
        private static MgrTest05 INSTANCE;
    
        private MgrTest05() {};
    
        public static MgrTest05 getInstance()  {
            if(null == INSTANCE){
                synchronized (MgrTest05.class){
                    try {
                        Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new MgrTest05();
                }
            }
            return INSTANCE;
        }
    
        public static void main(String[] args) {
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest05.getInstance())).start();
            }
            try {
                Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest05.getInstance())).start();
            }
        }
    }

    执行结果:获取的实例对象可能会不同(虽然减少了同步代码块,但是出现了线程安全问题)

    问题原因:和上面讲的懒加载第一种方式问题类似,虽然把实例化INSTANCE对象的代码同步了,但是还是有可能存在第一个线程进入了if语句,然后进入了同步代码块上锁了,但是还没有来的及执行实例化对象,后面线程也进入了if语句,只是被锁在实例化对象语句外面,等到第一个进入同步代码的线程出来后,被锁在外面的线程还是可以进入,然后实例化了新的对象,就出现了上面类似的线程安全问题。

    四、通过双重检查来解决上一个问题

    public class MgrTest06 {
        //做JIT优化的时候会指令重排 加上volatile关键之阻止编译时和运行时的指令重排
        private static volatile MgrTest06 INSTANCE;
    
        private MgrTest06() {};
    
        public static MgrTest06 getInstance()  {
            if(null == INSTANCE){
                synchronized (MgrTest06.class){
                    //双重检查
                    if(null == INSTANCE){
                        try {
                            Thread.sleep(10);//延迟10毫秒、让问题出现的可能性增大
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        INSTANCE = new MgrTest06();
                    }
                }
            }
            return INSTANCE;
        }
    
        public static void main(String[] args) {
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest06.getInstance())).start();
            }
            try {
                Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest06.getInstance())).start();
            }
        }
    }

    执行结果:获取的实例对象都是同一个(可以实现懒加载,并且不会有线程安全的问题,同时也解决了效率问题)

    这样实现了只在INSTANCE为空的时候才需要加锁获取实例,已经不为空了再调用getInstance方法就判断不为空,然后直接获取。

    五、静态内部类单例

    public class MgrTest07 {
        private MgrTest07() {};
    
        private static class MgrTest07Holder{
            private static final MgrTest07 INSTANCE = new MgrTest07();
        }
    
        public static MgrTest07 getInstance(){
            return MgrTest07Holder.INSTANCE;
        }
    
        public static void main(String[] args) {
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest07.getInstance())).start();
            }
            try {
                Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest07.getInstance())).start();
            }
        }
    }

    执行结果:(JVM保证单例,虚拟机加载类的时候只加载一次,所以INSTANCE也只会加载一次,同时实现了懒加载,因为加载外部类时不会加载内部类)

    六、枚举单例(不仅可以解决线程同步,还可以防止反序列化)

    public enum MgrTest08 {
    
        INSTANCE;
    
        public static void main(String[] args){
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest08.INSTANCE.hashCode())).start();
            }
            try {
                Thread.sleep(3000);//延迟3秒、模拟单例INSTANCE已经有值之后再调用getInstance
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            for(int i=0;i<5;i++){
                new Thread(() -> System.out.println(MgrTest08.INSTANCE.hashCode())).start();
            }
        }
    }

    执行结果:

    总结:一般使用直接初始化单例的和静态内部类单例的方式就可以了,不过使用枚举单例的方式更好,因为只有枚举单例,不仅可以解决线程安全问题,还可以防止反序列化,主要看实际开发中需不需要考虑到这些问题,来选择哪种方式实现单例就可以了。


    序列化的问题:

    为什么在做单例的时候要防止这一点?

    因为java的反射是通过一个class文件,然后把整个class加载到内存,再把它创建一个实例出来,而除了枚举方式,其它的都可以找到class文件通过反序列化的方式(反射)再创建一个实例出来,如果想让它不能被反序列化需要设置一些变量,过程比较复杂。

    枚举单例为什么可以防止反序列化?

    因为枚举类没有构造方法(java规定没有构造方法),就算拿到class文件也没有办法实例化一个对象出来,它反序列化只是一个INSTANCE值(当前案例),然后根据这个值来找对象的话,找到的是和单例创建的同一个对象。

     

     

     

     

    展开全文
  • 主要为大家详细介绍了Asp.Net设计模式之单例模式的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
  • C#单例模式C#单例模式详解C#单例模式详解C#单例模式详解
  • 单例模式详解.pdf

    2019-12-10 11:04:02
    1、掌握单例模式的应用场景。 2、掌握 IDEA 环境下的多线程调试方式。 3、掌握保证线程安全的单例模式策略。 4、掌握反射暴力攻击单例解决方案及原理分析。 5、序列化破坏单例的原理及解决方案。 6、掌握常见的...
  • 单例模式是设计模式中最常见也最简单的一种设计模式,保证了在程序中只有一个实例存在并且能全局的访问到。比如在Android实际APP 开发中用到的 账号信息对象管理, 数据库对象(SQLiteOpenHelper)等都会用到单例...
  • python的单例模式详解

    2022-01-05 07:54:38
    一、什么是单例模式 单例模式(Singleton Pattern)是一种常用的软件设计模式,该模式的主要目的是确保某一个类只有一个实例存在。当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场 比如,...
  • C++单例模式详解

    2020-12-22 18:05:43
    单例模式也称为单件模式、单子模式,可能是使用广泛的设计模式。其意图是保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。有很多地方需要这样的功能模块,如系统的日志输出,GUI...
  • 单例模式
  • 主要介绍了9种Java单例模式详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 26,118
精华内容 10,447
关键字:

单例模式详解

友情链接: app-debug.rar