【23种设计模式专题】一 单例模式,谁说程序猿没有女(男)朋友

程序猿学社的GitHub,欢迎Star
github技术专题
本文已记录到github

前言

你问程序猿小哥哥,有对象吗
他会毫不犹豫的告诉你,,简单呀,new一个呗。
我们知道"对象"只会有一个,毕竟,不是谁都是韦小宝,有7个老婆,可以new 7次。
那怎么保证只会new出一个对象,这就是本文的主角单例模式
现在这年代太难了,找对象难,在程序中,new一个对象,还这么讲究。

概念

采取一定的手段,保证一个类中只有一个实例,并且只提供一个取到该实例的入口。

  • 是不是理解起来一脸懵逼,别急,后面会一一的解释这段的意思。

应用场景

  • 学过spring的社友,应该知道,在spring中创建一个bean,会有一个scope属性,他的值默认为singleton,也就是单例,看看spring都在用单例,你还敢说没有用吗?
  • 项目的配置信息,可以考虑用单例
  • 网站的计数。统计访问量

总结:
如果该对象频繁的被使用,我们就可以考虑把他设计单例。

实战篇

new两个对象,判断是否相等

package com.cxyxs.designmode.util;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/3/15 12:30
 * Modified By:
 */
public class Demo1 {
    public static void main(String[] args) {
        Girlfriend gf = new Girlfriend();
        Girlfriend gf1 = new Girlfriend();
        System.out.println(gf  == gf1);
    }
}

class  Girlfriend{

}

各位社友,觉得输出的结果是什么?true还是false

  • 引用类型比较的是两个对象的地址是否相等,实例化两次,当然不相等。
  • 各位社友想一想,每次都实例化一个对象,开销是不是很大。作为一个别人眼里勤俭节约的程序猿,每个月几乎无什么开销。理性的程序猿,不仅仅在生活中得保持这种良好的品质,在开发过程中,我们也应该保持好这种传统。

饿汉式(第一种)

package com.cxyxs.designmode.util;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/3/15 13:15
 * Modified By:
 */
public class Demo2 {
    public static void main(String[] args) {
        Girlfriend2 gf1 = Girlfriend2.getInstance();
        Girlfriend2 gf2 = Girlfriend2.getInstance();
        System.out.println(gf1 == gf2);
    }
}
class  Girlfriend2{
    //第二步
    private  static Girlfriend2 gf = new Girlfriend2();
    // 第一步
    private Girlfriend2() {

    }

    //第三步
    public  static Girlfriend2 getInstance(){
        return  gf;
    }
}


输出的值为true,说明只实例化一次。我们来梳理一下代码实现思路。

  • 为了防止,一直new,直接把构造方法私有化,防止别人调用。
  • 通过静态变量实现只初始化一次,被static修饰的变量,我们都知道只会加载一次,所以,就可以保证对象只会实例化一次。
  • 我们现在也无法通过new 对象,实际上就是通过调用构造方法的方式直接创建对象,那其他的人,要如何调用,是不是需要提供一个入口,因为无法new对象,这就意味着我们无法通过对象.属性的方式调用该方法,是不是只有把这个方法修饰为static方法,才能调用。

在学习单例过程中,我们会经常看到一个词汇“懒加载”,他是什么意思?

  • 实际上,就是需要使用的时候,才加载。案例:女朋友说喜欢草莓,你去买了一大盒,你到家后,女朋友,跟你说,我又不喜欢草莓,钱也花了,没人吃,是不是造成浪费,在软件的世界里,也是一样,避免资源的浪费。

总结:
饿汉式是通过static变量,也就是类加载原理保证单例,也就是所谓的线程安全。没有实现懒加载。
个人的建议:
可以使用,内存浪费的问题,几乎可以忽略,在实际开发过程中,存在一个类,也不调用,还傻傻的放在那里,说明这个代码,可能是无用的代码,对于,没有的代码,直接干掉。

懒汉式(第二种)

package com.cxyxs.designmode.util;

/**
 * Description:懒汉式
 * Author: 程序猿学社
 * Date:  2020/3/15 14:00
 * Modified By:
 */
public class Demo3 {
    public static void main(String[] args) {
        Girlfriend3 gf = Girlfriend3.getInstance();
        Girlfriend3 gf1 = Girlfriend3.getInstance();
        System.out.println(gf == gf1);
    }
}
class  Girlfriend3{
    private static  Girlfriend3 gf =  null;
    private Girlfriend3() {

    }
    public static Girlfriend3 getInstance(){
        if(gf == null){
            gf = new Girlfriend3();
        }
        return gf;
    };
}


打印的结果也为true,各位社友,是不是觉得,这就实现单例?
实际上,这种写法是有问题的,会存在线程安全的问题

  • gf对象被static修饰,说明资源只有一份,看过社长多线程高并发编程文章的社友,应该都知道,多个线程访问同一个资源的时候,会存在线程不安全的问题。
  • 假设有两个线程A B, A走到判断这里,而这是的B也刚好走到这里,都没有创建对象,都会创建对象。实际上就会创建实例化两次。

模拟线程不安全

package com.cxyxs.designmode.util;

import java.util.concurrent.TimeUnit;

/**
 * Description:懒汉式,模拟线程不安全
 * Author: 程序猿学社
 * Date:  2020/3/15 14:00
 * Modified By:
 */
public class Demo4 {
    public static void main(String[] args) {
        new Thread(()->{
            Girlfriend4 gf = Girlfriend4.getInstance();
            System.out.println(gf);
        }).start();

        new Thread(()->{
            Girlfriend4 gf1 = Girlfriend4.getInstance();
            System.out.println(gf1);
        }).start();
    }
}

class  Girlfriend4{
    private static  Girlfriend4 gf =  null;
    private Girlfriend4() {

    }
    public static   Girlfriend4 getInstance(){
        if(gf == null){
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            gf = new Girlfriend4();
        }
        return gf;
    };
}

  • TimeUnit.MILLISECONDS.sleep(100); 模拟延迟100毫秒,如果不加这个代码,程序一闪而过,是无法看到实际的效果的
  • 通过测试,我们可以发现,这两个对象的地址是不一样的,说明实例化两次,所以我们说他是线程不安全的。

对多线程不了解社友,看到线程不安全这个词汇,可能会有点懵逼,这里我简单的阐述一下,如何界定一个线程是否安全。

  • 首先,有一个前提,在多线程环境下,我上面的代码new了两次Thread。就是保证在多线程环境下。
  • 我们对程序有一个预期的效果,按照我们程序的预期,这个对象是实例化一次,这个就是我们的预期,最终的结果是实例化两次,跟我们预期有偏差,我们就可以说他是线程不安全的。

模拟线程不安全,方法加synchronized

跟上面的方法相比,只是增加了synchronized关键字

package com.cxyxs.designmode.util;

import java.util.concurrent.TimeUnit;

/**
 * Description:懒汉式,方法上增加同步
 * Author: 程序猿学社
 * Date:  2020/3/15 14:00
 * Modified By:
 */
public class Demo5 {
    public static void main(String[] args) {
        new Thread(()->{
            Girlfriend5 gf = Girlfriend5.getInstance();
            System.out.println(gf);
        }).start();

        new Thread(()->{
            Girlfriend5 gf1 = Girlfriend5.getInstance();
            System.out.println(gf1);
        }).start();
    }
}

class  Girlfriend5{
    private static  Girlfriend5 gf =  null;
    private Girlfriend5() {

    }
    public static synchronized   Girlfriend5 getInstance(){
        if(gf == null){
            try {
                TimeUnit.MILLISECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            gf = new Girlfriend5();
        }
        return gf;
    };
}

  • 为了解决线程安全的问题,在方法上增加关键字synchronized,意味着,同一时候,只能有一个线程调用该方法,方法运行完后,下一个线程才能拿到锁。 案例:每一个茅坑都有一把锁,本来一个洗手间,可以容纳很多人,使用synchronized,就相当于,隔壁小王在上洗手间,为了防止被人偷窥,直接把洗手间的大门锁上,需要等隔壁老王,上完后,其他人才能进去,所以说,加synchronized这种方式性能不怎么好。

总结:
通过synchronized关键字,解决懒汉式单例线程安全的问题,虽说能保证线程安全,但是,效率太低,在实际项目实战过程中,不建议使用这种方式

隔壁老王:社长,那有什么办法,可以保证线程安全,效率还行的实现方式吗?
社长:有的,通过双重校验锁方式。

双重校验锁DCL(第三种)

1.0版本,线程不安全

package com.cxyxs.designmode.util;

import java.util.concurrent.TimeUnit;

/**
 * Description:懒汉式 双重校验锁  DCL
 * Author: 程序猿学社
 * Date:  2020/3/15 14:00
 * Modified By:
 */
public class Demo6 {
    public static void main(String[] args) {
        new Thread(()->{
            Girlfriend6 gf = Girlfriend6.getInstance();
            System.out.println(gf);
        }).start();

        new Thread(()->{
            Girlfriend6 gf1 = Girlfriend6.getInstance();
            System.out.println(gf1);
        }).start();
    }
}

class  Girlfriend6{
    private static   Girlfriend6 gf =  null;
    private Girlfriend6() {

    }
    public static   Girlfriend6 getInstance(){
        if(gf == null){    //步骤1
            synchronized (Girlfriend6.class){
                if(gf == null){
                    gf = new Girlfriend6();  //步骤2
                }
            }
        }
        return gf;
    };
}

  • 通过结果查看,好像木有问题。不要被结果所蒙蔽了,实际上,这种方式,还是会存在线程不安全的问题。
  • 为了提供性能,java编译器,会进行指令重排
    (1).分配内存空间
    (2)初始化对象
    (3) 把gf变量指向刚刚分布的内存地址

这样就会存在问题,假设,他的顺序是1-3-2,线程A刚刚跑到1-3这一步,把gf指向一个地址的时候(表示gf这个对象不为空,因为已经有内存地址),而这时的线程B,刚刚到步骤1,进行对应的判断,发现gf不为空,直接退出程序。

题外话,浅谈指令重排
 package com.cxyxs.designmode.util;

/**
 * Description:
 * Author: wude
 * Date:  2020/3/18 11:29
 * Modified By:
 */
public class Test {
    public static void main(String[] args) {
        Test test = new Test();
    }
}


idea版本中,在类里面右键,保证该类已运行,不然,会提示找不到主类的错。

  • 实际上,我们在代码里面只是创建了一个main方法,实例化一个对象,而底层是这样处理的。具体都是什么意思。可以查查Java字节码。

隔壁老王:社长,既然多线程环境下,会存在指令重排的问题,是不是说通过双重校验锁这种方式实现单例,也不可行。
社长:既然存在指令重排的问题,jdk大佬当然也考虑这个问题,增加volatile关键字

2.0版本,线程安全


总结:
通过双重检验锁,很好的解决解决线程安全的问题。建议使用。实现了懒加载。

静态内部类(第四种)

package com.cxyxs.designmode.util;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/3/18 19:59
 * Modified By:
 */
public class Demo7 {
    public static void main(String[] args) {
        Girlfriend7 gf = Girlfriend7.getInstance();
        Girlfriend7 gf1 = Girlfriend7.getInstance();
        System.out.println(gf == gf1);
    }
}
class  Girlfriend7{
    private Girlfriend7() {
    }

    public static  Girlfriend7 getInstance(){
        return  Instance.gf;
    }

    private  static  class Instance{
        private static final Girlfriend7 gf = new Girlfriend7();
    }
}

总结:
静态内部类通过classloader 机制来保证单例和线程安全。同时也是懒加载的,只有使用到该静态内部类被调用时,才会被加载。

隔壁老王:社长,社长,听说反射可以破坏单例,具体是怎么一回事?
社长:既然,你提了这个问题,我们就来具体看看,为什么反射可以破坏单例。

反射破坏单例

实际上,不止反射可以破坏单例,通过序列化的方式也可以破坏单例。

package com.cxyxs.designmode.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;

/**
 * Description:模拟通过反射破坏单例
 * Author: 程序猿学社
 * Date:  2020/3/19 9:15
 * Modified By:
 */
public class Demo8 {
    public static void main(String[] args) throws Exception {
        Constructor<Girlfriend8> declaredConstructor = Girlfriend8.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);//暴力访问

        Girlfriend8 gf = declaredConstructor.newInstance();
        Girlfriend8 gf1 = declaredConstructor.newInstance();
        System.out.println(gf);
        System.out.println(gf1);
    }
}

class  Girlfriend8{
    private Girlfriend8() {
    }

    public static  Girlfriend8 getInstance(){
        return  Instance.gf;
    }

    private  static  class Instance{
        private static final Girlfriend8 gf = new Girlfriend8();
    }
}

  • 通过测试结果,我们可以发现,通过反射,这个对象竟然被实例化多次。
  • 实际上,就是因为反射可以拿到private的构造方法。

隔壁老王:社长,那怎么解决反射可以暴力破解单例的问题?
社长:在构造方法里面,再判断一下这个对象是否为空。

防止反射,增加监控代码

package com.cxyxs.designmode.util;

import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;

/**
 * Description:模拟通过反射破坏单例
 * Author: 程序猿学社
 * Date:  2020/3/19 19:15
 * Modified By:
 */
public class Demo8 {
    public static void main(String[] args) throws Exception {
        Constructor<Girlfriend8> declaredConstructor = Girlfriend8.class.getDeclaredConstructor(null);
        declaredConstructor.setAccessible(true);//暴力访问

        Girlfriend8 gf = declaredConstructor.newInstance();
        Girlfriend8 gf1 = declaredConstructor.newInstance();
        System.out.println(gf);
        System.out.println(gf1);
    }
}

class  Girlfriend8{
    private Girlfriend8() {
        synchronized (Girlfriend8.class){
            if(Instance.gf == null){
                try {
                    throw new Exception("该对象已实例化,不要试图破解!");
                }catch (Exception e){
                    e.printStackTrace();
                }
            }
        }
    }

    public static  Girlfriend8 getInstance(){
        return  Instance.gf;
    }

    private  static  class Instance {
        private static final Girlfriend8 gf = new Girlfriend8();
    }
}
  • 实际上,这种方式,只是在第二次实例化的时候,增加一个判断,发现重复实例化,就抛出一个异常,没有从根本上解决问题。老王,你可以使用另外一种,代码也十分的简洁。那就是通过枚举的方式实现单例。

枚举(第五种)

package com.cxyxs.designmode.util;

import java.lang.reflect.Constructor;

/**
 * Description:
 * Author: 程序猿学社
 * Date:  2020/3/20 19:55
 * Modified By:
 */
public enum  Demo9 {
    GF;
    public Demo9 getInstance(){
        return GF;
    }
}
class  Test9{
    public static void main(String[] args) throws Exception{
        Demo9 instance = Demo9.GF.getInstance();
        Demo9 instance1 = Demo9.GF.getInstance();
        System.out.println(instance == instance1);
    }
}

  • 通过枚举实现单例,十分的简单,并且是线程安全的,不会被反射,序列化破坏,但是,他不支持懒加载。

隔壁老王:社长,为什么枚举可以解决别人暴力破解的问题?
社长: 给你看一段源码,你就知道为什么枚举可以防止别人暴力破解。

还记得我在上一个事例中,给你说过,通过反射可以单例,写了一段代码,我们来看看源码。

  • 如果类型为枚举,是不会继续向下执行的,也就不会重新实例化一个对象。


最近有不少读者在问我java应该如何学习,在这里,把我整理的学习视频分享出来。
(1).springboot,springcloud视频
(2).架构师视频,设计模式视频,深入jvm内核原理。
(3) java面试视频

可以通过公众号“程序猿学社”,回复关键字"视频",希望能帮到你。

原创不易,不要白嫖,觉得有用的社友,给我点赞,让更多的老铁看到这篇文章。

作者:程序猿学社
原创公众号:『程序猿学社』,暂时专注于java技术栈,分享java各个技术系列专题,以及各个技术点的面试题。
原创不易,转载请注明来源(注明:来源于公众号:程序猿学社, 作者:程序猿学社)。

发布了289 篇原创文章 · 获赞 737 · 访问量 33万+

猜你喜欢

转载自blog.csdn.net/qq_16855077/article/details/104866592