设计模式-单例模式

单例模式定义:

单例模式的英文叫做singleton模式,单例模式就是,在你的系统里,你要判断一下,如果有一些类,只需要一个实例就可以了,那就给那个类 做成单例的模式。

单例模式常见的场景:

实际上,如果结合 业务需求 在实际项目里面,实践过的,或 使用过的设计模式,单例模式和工厂模式是最常用的两种。那么,单例模式常见的场景,可能就两类。

第一类 就是,比如说 我们自定义了一个框架,然后针对这个框架,自定义了一份xml格式的一个配置文件,要读取这个配置文件,把这个配置文件中的数据 读取到类中。

如果 这个类的实例,只要保存一份就可以;那么此时,就可以使用单例模式,将这个类做成,它的这个实例 只能有一个;然后,在这个实例中,保存了配置文件中的数据。ok,这是第一个场景。

第二个场景 就是,类似于,我们在使用工厂模式时,在工厂模式里面,其实我们有些这个工厂 是需要实例化对象的,因为要基于 这个实例化对象,然后来实现一些,比如 继承,接口 或 其他实现 等功能,那么,像这些工厂的实例,就可以做成这个单例的。

那 除此之外的 其他情况下,需要自己去判断,如果是一个类的这个实例,只需要保存一份,那就做成单例。

综上,单例具体什么时候用,在做这个系统的时候,一般来说 都能判断出来。但其实 比较重要的是,这个单例怎么来实现。这个单例所谓的实现,大体上 可以分成两类,一种叫做 饱汉模式,一种叫做 饿汉模式。

单例模式的实现1-饿汉模式:

那 什么叫做 饿汉模式,就很简单,直接showCode:

/**
 * 饿汉模式
 */
public class HungrySingletonPatternDemo {

    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.execute();

        // 如果把 Singleton 这个类放到外面去,而不是以内部类的形式出现,这时去new Singleton()的这个构造函数,就会报错
        //new Singleton();
    }

    public static class Singleton {

        /**
         * 第一步:直接就是将这个类的实例在这里创建出来,赋予 static final 修饰的变量
         *
         * static:就是一个类的静态变量
         * final:这个变量的引用第一次初始化赋予之后,就再也不能修改引用了
         */
        private static final Singleton instance = new Singleton();

        /**
         * 第二步:将构造函数搞成private私有的
         *
         * 此时除了这个类自己本身,其他任何人都不能创建它的这个实例对象
         */
        private Singleton(){}

        /**
         * 第三步:给一个static静态方法,返回自己唯一的内部创建的一个实例
         * @return
         */
        public static Singleton getInstance() {
            return instance;
        }

        public void execute() {
            System.out.println("单例类的方法");
        }
    }
}

 

先写一个 所谓的这个饿汉模式,创建一个类,叫做HungrySingletonPatternDemo,就是这个饿汉模式。然后,定义一个内部类Singleton,public static class Singleton {},在这个Singleton的里面,第一步,我们在这先把它写出来,就是private static final Singleton instance = new Singleton(),直接就是,将这个类的实例在这里创建出来,然后赋予这个static final修饰的变量,这个static的意义是说,就是一个类的静态变量,这个final是说,这个变量的引用 第一次初始化赋予之后,就再也不能修改这个引用了。这些都是java的基础。也就是,在Singleton它这个类初始化的时候,第一步的 它这一行代码就会执行,然后 类一初始化,直接就把 它自己的实例 给创建出来了。

然后, 第二步其实就是,这个private Singleton() {},将它的构造函数搞成这个private 私有的,此时 除了这个类自己本身,其他任何人 都不能创建它的 这个实例对象。ok,这个是第二步。

然后,第3步的话,就是 public static Singleton getInstance() {return instance;},给它一个static 静态方法,然后,返回自己唯一的 内部创建的 一个实例。

最后,我们还可以 给它一个方法,public void execute() { System.out.println("单例类的方法");}。

然后,我们来用一下这个类,写一个main方法,Singleton singleton = Singleton.getInstance();,然后,singleton.execute();,就这样吧。

执行一下main方法,来看一下,ok,单例类的方法。

其实为什么叫它是饿汉模式,因为 这个类,可以认为 它很饿,好着急。饿,指的就是说,它好像就是一种很饥渴的状态。所以说,它因为很饿,过于饥渴,就着急忙慌的,在自己这个类初始化的时候,private static final Singleton instance = new Singleton(),这一行代码就会执行,直接就是把自己的这个对象实例,给创建出来,给它初始化出来了,所以给它叫做饿汉模式。

但是,你会发现,在单例这种模式下,这个Singleton类的实例,全局就只有一个,就只有它,因为 外面不能 再对它去创建 它的实例了,比如说,你要是 在main方法中,再new一个Singleton,这样new Singleton(),那肯定会报错。但因为当前,我们这个Singleton是HungrySingletonPatternDemo的这个内部类,所以不会报错。但是如果把Singleton这个类,给放到外面去。

public class Singleton {

    /**
     * 第一步:直接就是将这个类的实例在这里创建出来,赋予 static final 修饰的变量
     *
     * static:就是一个类的静态变量
     * final:这个变量的引用第一次初始化赋予之后,就再也不能修改引用了
     */
    private static final Singleton instance = new Singleton();

    /**
     * 第二步:将构造函数搞成private私有的
     *
     * 此时除了这个类自己本身,其他任何人都不能创建它的这个实例对象
     */
    private Singleton(){}

    /**
     * 第三步:给一个static静态方法,返回自己唯一的内部创建的一个实例
     * @return
     */
    public static Singleton getInstance() {
        return instance;
    }

    public void execute() {
        System.out.println("单例类的方法");
    }
}

我再写一个Singleton类 去演示出来,然后HungrySinglePatternDemo内部的Singleton这个类,这边把它给注掉,复制到外面这个Singleton中,这样把Singleton就放到外面去了。

/**
 * 饿汉模式
 */
public class HungrySingletonPatternDemo {

    public static void main(String[] args) {
        Singleton singleton = Singleton.getInstance();
        singleton.execute();

        // 如果把 Singleton 这个类放到外面去,而不是以内部类的形式出现,这时去new Singleton()的这个构造函数,就会报错
        new Singleton();
    }

//    public static class Singleton {
//
//        /**
//         * 第一步:直接就是将这个类的实例在这里创建出来,赋予 static final 修饰的变量
//         *
//         * static:就是一个类的静态变量
//         * final:这个变量的引用第一次初始化赋予之后,就再也不能修改引用了
//         */
//        private static final Singleton instance = new Singleton();
//
//        /**
//         * 第二步:将构造函数搞成private私有的
//         *
//         * 此时除了这个类自己本身,其他任何人都不能创建它的这个实例对象
//         */
//        private Singleton(){}
//
//        /**
//         * 第三步:给一个static静态方法,返回自己唯一的内部创建的一个实例
//         * @return
//         */
//        public static Singleton getInstance() {
//            return instance;
//        }
//
//        public void execute() {
//            System.out.println("单例类的方法");
//        }
//    }
}

这时,在HungrySingletonPatternDemo类中的main这里,如果去new Singleton()的这个构造函数,去创建它的实例,是不行的,它会提示说,The constructor Singleton() is not visible,就是说它的Singleton的 这个构造函数是私有的。饿汉单例模式,会在类初始化的时候,就被创建出来,而且全局只有一个,所以,一定是线程安全的。那么,这个就是所谓的 饿汉模式。

单例模式实现2-线程不安全的饱汉模式:

然后的话,是线程不安全的饱汉模式,那个叫做,UnsafeFullSingletonPatternDemo。代码如下:

/**
 * 线程不安全的饱汉模式
 */
public class UnsafeFullSingletonPatternDemo {

    /**
     * 线程不安全
     */
    public static class Singleton {
        private static Singleton instance;

        private Singleton() {

        }

        public static Singleton getInstance() {
            /**
             * 假设有两个线程过来
             *
             * 线程的基础:线程是并发着执行的,cpu,先执行一会儿线程1,然后停止执行线程1;切换过去执行线程2
             * 执行线程2一会儿之后,再停止执行线程2;回来继续执行线程1
             *
             * 第一个线程,判断发现说instance == null,代码进入到了下面去
             * 第二个线程,执行到这儿,发现,此时instance == null,那么就没什么问题了,继续往下走
             */
            if(instance == null) {
                // 第一个线程跑到了这儿来,但是此时第一个线程,还没有执行下面的那行代码
                // 此时,第二个线程代码也执行到了这儿,cpu切换回线程1

                // 执行线程1的代码,线程1会创建一个实例出来
                // 但是切换到线程2去执行的时候,线程2 的代码已经执行到这儿来了,此时又会再一次执行下面的代码
                // 就是会再一次创建一个实例,之前线程1创建的那个实例,就会被垃圾回收,废弃掉了
                instance = new Singleton();
            }
            return instance;
        }
    }
}

首先什么叫饱汉模式,饱汉模式,它的意思就是说,我这肚子好撑啊,实在是不想在上来,一开始的时候,就创建我的这个实例,因为我太撑了,我是这个饱汉,我并不饥渴,所以说我不想,在这个类初始化的时候,上来就在这,有个private static Singleton instance,声明这个单例持有自己本身变量的时候,就创建我的这个对象实例。

然后,我希望是,通过一个对外的静态方法,public static Singleton getInstance(){},当然还是要给个private Singleton(){},一个私有的无参构造器。我希望是说,如果有人,要来获取的我的实例,我在对外的getInstance方法中,这边来给singleton的Instance,给它判断一下,if(instance == null){instance = new Singleton} return instance,如果它是null的话,new Singleton,然后,给它返回instance,就这样子。

也就是说,Singleton这是一个单例类,它说,我肚子好撑,我是饱汉;然后,我不想上来就给它,初始化这个实例instance;然后我提供一个方法,这个方法里面说,你来调用的时候,如果你第一次来调用,如果我发现说,这个实例instance是null,我就给它创建好实例,然后给它返回,那么后面的话呢,再有请求过来,直接去用,我之前创建好的这个实例就可以了。然后,这个 main class就不用写了,没有什么好演示的,因为太简单了。

但是这个,为什么说是 线程不安全的,它之所以称之为线程不安全,主要的问题出在这,getInstance方法,假设有两个线程过来。然后,第一个线程,判断发现说,这个instance == null,然后这个代码就进入到了if里面去,那个第一个线程跑到了这来,instance = new Singleton();,ok,稍等,但是此时第一个线程,还没有执行下面的,instance = new Singleton(),这行代码。那首先这里需要有一个线程的基础,就是有一个前提,这个线程,它是并发着去执行的。就是在你的系统里,你可以认为系统的这个cpu,它是先执行一会线程1,然后停止执行线程1,然后,切换过去执行线程2,然后执行线程2,一会之后,再停止执行线程2,然后,回来继续执行线程1,所以,它是这样一个概念。

那么,第一个线程它先跑过来,if(instance == null) {instance = new Singleton},它说,这是null啊,然后第一个线程的代码执行到了instance = new Singleton这里,cpu给它停了,切换到了第二个线程,然后,第二个线程执行到这,if(instance == null) {instance = new Singleton},那发现此时instance,还是null啊,因为第一个线程,还没有去执行instance = new Singleton这行代码呢,所以第二个线程,就说这个instance还是null,那么就没什么问题了,然后,继续往下走,此时第二个线程的代码也执行到了instance = new Singleton这。那这个时候,cpu切换回线程1, cpu去执行线程1的那个代码,然后线程1就会来执行这个代码instance = new Singleton,线程1会创建一个实例出来,然后线程1就走完了,调用者它可以拿到这个实例。

但是,切换到这个线程2去执行,这个时候,这个线程2 的这个代码,已经执行到instance = new Singleton这来了,就是线程2的代码,已经在if判断成立的这个括号里面了,那么此时,线程2又会执行下面的那个代码instance = new Singleton,就是会再次创建一个实例,然后之前线程1创建的那个实例,就会被垃圾回收,因为就没有人引用了,就已经被废弃掉了。所以,就是因为会有这样的一个,线程不安全的这种情况,可能会去导致,instance这一个实例创建多次,所以它叫做线程不安全的。

线程不安全,就是多线程并发 去访问一个共享资源,private static Singleton instance,这就是一个共享资源的时候,因为线程来回来去的切换,可能就会导致,if(instance == null) {instance = new Singleton},这里就会有线程不安全的问题。这个就叫做线程不安全的饱汉模式。

单例模式实现3.1-不完全线程安全的饱汉模式:

那么,如果要做成线程安全的话,有一个常规性的一个方法,应该怎么来做呢,直接先show code:

public class SafeFullSingletonPatternDemo {

    public static class Singleton {
        private static Singleton instance;

        private Singleton() {

        }

        // 不是完美的
        // 因为不同的JVM的编译器的问题,可能导致说,这个情况下,还是线程不安全的
        // 具体的我不在这儿讲,因为涉及到复杂的JVM内部的原理:指令重排

        public static Singleton getInstance() {
            // 如果线程1和线程2都执行到了这一步,然后此时线程1判断发现还是null
            // 线程2此时判断发现instance == null,也会进去
            if (instance == null) {
                // 线程1就会进来,此时线程1停止,切换到线程2
                // 线程2也会进来,此时切换到线程1

                // 线程1,发现这里需要加锁,在这里加锁,获取到了这个锁
                // 线程2过来,线程2发现说,我也想要在这里加锁,发现说这个锁被人加了,线程2挂起等待别人释放锁
                // 此时切换回线程2,线程2发现锁被释放,然后在这里加锁
                synchronized (SafeFullSingletonPatternDemo.class) {
                    // 线程1就进来了,此时切换到线程2
                    // 切换回线程1,线程1此时在这里,再次判断,instance == null
                    // 线程2就进来了,double check,如果这里没有instance == null的判断,那么线程2就会再次创建一个实例
                    // 但是这里是双重检查,线程2又判断了一下,instance == null?否,不是null
                    if (instance == null) {
                        // 线程1就会进来,创建一个实例
                        instance = new Singleton();
                    }
                }
            }
            // 这边出来以后,线程1就释放锁了
            // 线程2跳出来,直接获取一个instance返回了,这个instance就是之前线程1创建的实例
            return instance;
        }
    }
}

 

创建一个SafeFullSingletonPatternDemo类,再添加一个内部类,public static class Singleton {},然后它说,我是这个饱汉吧,不需要一上来,初始化这个类的时候,就进行实例化,所以只需要声明一下instance即刻,private static Singleton instance; ,再加一个私有的无参构造器 private Singleton(){},和一个getInstance方法,public static Singleton getInstance(){},这个getInstance方法里面,它先判断一下,if(instance == null),如果它是null的话,ok,synchronized,加锁。因为它是一个静态方法,所以它加的这个锁,只能加在它的.class上面,也就是synchronized(SafeFullSingletonPatternDemo.class) {},然后这个锁里面,还得再来判断一下,如果instance是null的话,然后就执行instance = new Singleton();,就是if(instance == null) { instance = new Singleton},最后,将return instance;,将创建好的实例instance返回。这样的一个逻辑。

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

以上这个东西,叫做double check,双重校验锁。这个是什么意思,可以再来想一下,如果这时,那个线程1,和线程2,都执行到了这一步,第一个if(instance == null){},然后,此时线程1,判断发现,instance还是这个null,对吧,那么线程1,就会进来,准备执行synchronized(SafeFullSingletonPatternDemo.class){},此时线程1,停止,切换到线程2。

然后那个,切换到了线程2了以后,线程2,这个时候还在第一个if(instance == null){} 外面,线程2此时,判断发现,这个instance也是等于null啊,那么也会进去,对吧,然后此时线程2也会进来,准备执行synchronized(SafeFullSingletonPatternDemo.class){},此时切换到线程1。

然后那个,线程1发现了,这里需要加锁,那线程1,它就在这里加锁,然后线程1,获取到了这个锁,它加锁成功了,对吧。那么线程1呢,就进来了,准备执行第二个if(instance == null){},对吧,然后此时,切换到线程2。

然后那个,线程2,这时候在这,准备执行synchronized(SafeFullSingletonPatternDemo.class){},对吧,然后线程2过来,发现说,哎,我也想要在这里加锁啊,结果尝试加锁,发现说,这个锁被人加了啊,所以说,线程2挂起,等待别人释放锁,对吧,线程2就卡这了。

然后那个,此时再切换回 这个线程1,线程1此时在这里,执行第二个if(instance == null){},再次判断这个instance,它还是这个null,然后这个线程1,就会进来,进入第二个if(instance == null){}里面,执行instance = new Singleton();,创建一个实例,对吧。然后这个线程1,创建完了以后,就啪嚓一下将锁释放,准备return instance;,return之后,线程1就执行完了。

然后那个,线程1这边,创建instance实例,出来以后,线程1 就释放锁了,线程1释放锁了以后呢,如果此时,切换回这个线程2,线程2执行synchronized(SafeFullSingletonPatternDemo.class){},发现锁被释放,然后这个线程2,在这里加锁,线程2就进来了,执行第二个if(instance == null){}判断,instance这个时候,因为之前已经被线程1给创建出来了,所以它已经不是null了,所以不会再次创建一个Singleton实例。

所以说这边,为什么叫做doublecheck,就是说为什么锁前也要加一个if(instance==null),下面锁的里面,为什么又要加一个if(instance==null),因为如果第一个if(instance==null)这里,没有instance是null的这个判断,那么线程2,就可能会,在线程1释放锁之后,返回已经创建好的实例之前,再次进入锁里,创建一个实例,但是这里是双重检查,然后这个线程2,又判断了一下,这个instance是否为null呢,发现是否,instance这个时候,因为之前已经被线程1给创建出来了,所以它已经不是null了,否。然后线程2就直接,if就没有执行成立,啪,跳出来了,线程2跳出来了,然后,直接获取一个instance返回了,这个instance,就是之前线程1创建的这个实例,ok。

单例模式实现3.2-完全线程安全的饱汉模式:

好, 那么,所以说这个东西就是说,用doublecheck去确保线程的这个安全,但是的话呢,要说一点,就是像这个doublecheck的单例模式,其实它还是,不是完美的。因为这个不同的JVM的编译器的问题,可能导致说,在doublecheck单例模式的这个情况下,还是线程不安全的。因为涉及到复杂的jvm内部的这个原理,具体的参见 JMM内存模型之指令重排原则等。所以这种doublecheck双重检查的单例模式,它也不是说完全线程安全的。

那如果要做到彻底的线程安全,要怎么做呢,要做成,那个叫做 InnerClassFullSingletonPatternDemo,就是基于内部类来实现这个Singleton。先声明这个Singleton的单例类,public static class Singleton {},然后内部给它一个私有的无参构造器 private Singleton {},然后这边是在Singleton类的内部,再添加一个内部类,public static class InnerHolder {},然后这个InnerHolder的内部是,声明一个静态的不可变的常量instance,并初始化赋值new Singleton(),public static final Singleton instance = new Singleton();,最后,在Singleton中,提供一个静态方法,返回这个InnerHolder.instance,public static Singleton getInstance(){ return InnerHolder.instance;},ok。showCode如下:

/**
 * 这个才是我们实际开发过程中,最最常用的单例模式,内部类的方式来实现
 */
public class InnerClassFullSingletonPatternDemo {

    /**
     * 可以做到饱汉模式
     *
     * 内部类,只要没有被使用,就不会初始化,Singleton的实例就不会创建
     *
     * 在第一次有人调用getInstance方法的时候,内部类会初始化,创建一个Singleton的实例
     *
     * 然后java能确保的一点是,类静态初始化的过程一定只会执行一次
     *
     */
    public static class Singleton {

        private Singleton() {

        }

        public static class InnerHolder {

            public static final Singleton instance = new Singleton();
        }

        public static Singleton getInstance() {
            return InnerHolder.instance;
        }
    }
}

 

这个是 完全线程安全的,而且它这个,可以做到饱汉模式。就是它的实例,上来是没有初始化的,然后,是被放在了它的一个 内部类InnerHolder里面,这个内部类,只要没有被使用,就不会初始化,那么那个Singleton的这个实例,就不会创建。就

public static final Singleton instance = new Singleton();

这一行就不会执行。那么在第一次有人调用那个,getInstance()这个方法的时候,第一次有人调用它,那么我们就返回那个内部类里面的这个instance实例。也就是这个时候,你第一次使用它getInstance方法,那么这个内部类会初始化,然后创建一个Singleton的这个实例。

也就是,第一次有人调getInstance()方法的时候,然后这个java能确保的一点是,就是类静态初始化的这个时候,类静态初始化的这个过程,一定只会执行一次,也就是说这个内部类,它一定只会初始化一次。

简言之,在第一次有人调用getInstance()这个方法的时候,第一次执行到这的时候,然后它会初始化,那只有 它唯一的一次初始化的时候,然后才会创建Singleton这个实例,基于内部类来实现的Singleton,这个东西能够保证,首先,它的这个实例,不是上来就创建的,它可以保证一个饱汉的模式,然后当你第一次需要的时候,第一次调用getInstance()这个方法的时候,InnerHolder.instance被执行,内部类InnerHoder中的instance它初始化,然后才会创建出来它。而且它一定是线程安全的,因为这个类的静态初始化的这个过程,这个jdk,就是说这个jvm给你保证,它一定只给你保证执行一次,一定不会说因为有多线程,这段代码执行多次,一定不会,不可能的一件事情。

好,所以说这个 基于内部类来实现的Singleton,才是我们实际开发过程中,最最常用的这个单例模式,就是用这个内部类的方式来实现,这个是最最常用的,而且是最最保险的,最最安全的,最最稳妥的。那么以上4个单例类,基本上到这就差不多了。

那么,在项目里的话,单例模式其实还是有很多地方可以去实践的,比如说一些,对一些工厂的这个实例,结合一些工厂模式来做。就是说对一些工厂的实例 可以做成是单例的,这个都没有问题。

 

 

end

posted @ 2022-10-04 17:11  HarryVan  阅读(17)  评论(0编辑  收藏  举报