设计模式理解与实现(1)—— 单例模式

   日期:2020-11-07     浏览:90    评论:0    
核心提示:前言学习设计模式也是在代码上精进的必经之路,在阅读各种框架源码时,如果不懂设计模式,就会看得很吃力;在面试时,面试官也总会问:“了解过设计模式吗?”,如果这个时候你能和面试官多聊上几句你对设计模式的理解,是非常加分的本系列以Java为编程语言,从设计模式中最简单的单例模式开始,介绍常用的设计模式源码看完有收获别忘了点个star哦~设计模式理解与实现 - 专栏设计模式理解与实现(1)—— 单例模式导航前言源码设计模式理解与实现 - 专栏单例模式作用优点缺点实现饿汉式1. 最常规的饿汉式思路2.

前言

学习设计模式也是在代码上精进的必经之路,在阅读各种框架源码时,如果不懂设计模式,就会看得很吃力;在面试时,面试官也总会问:“了解过设计模式吗?”,如果这个时候你能和面试官多聊上几句你对设计模式的理解,是非常加分的

本系列以Java为编程语言,从设计模式中最简单的单例模式开始,介绍常用的设计模式

源码

看完有收获别忘了点个star哦~

设计模式理解与实现 - 专栏

设计模式理解与实现(1)—— 单例模式

导航

  • 前言
  • 源码
  • 设计模式理解与实现 - 专栏
  • 单例模式
    • 作用
    • 优点
    • 缺点
    • 实现
      • 饿汉式
        • 1. 最常规的饿汉式思路
        • 2. 饿汉式 with 枚举
      • 懒汉式
        • 懒汉式的线程安全问题
      • 线程安全的懒汉式
        • 1. 懒汉式 with 同步方法
        • 2. 懒汉式 with DCL - 双重检验锁
        • 3. 懒汉式 with 私有静态内部类

单例模式

我们知道,在Java中,每一个对象都可以称为一个实例,如:

	// 创建一个学生实例 stu1
	Student stu1 = new Student();

在普通的类中,其构造方法常是public的,在程序编写过程中需要用到一个类时,只需要new 类名(参数);即可

而单例模式,顾名思义,单例,就是单一实例的意思,指在一个程序运行时,一个类只能有一个实例

作用

一个类只能有一个实例,用处都有啥呢?

  1. 可作为全局唯一的访问点,用于:
    1. 计数器
    2. 创建连接 / 访问资源,如数据库,IO操作等
    3. 要求生成全局唯一序列号的场景
  2. 减少一个全局类不必要的创建和销毁造成的开销

优点

说完用处说好处:

  1. 由于单例模式一个类只能有一个实例,减少了内存的使用,也减少了创建(特别是有些对象的产生需要吃很多资源,如读配置、创建依赖对象)和销毁实例带来的开销
  2. 可以避免对资源文件的多重占用,比如说调用一个单例的实例来写文件,其他线程想要执行此操作就需要等待,而不会冲突

缺点

说完好处说坏处:

  1. 单例模式一般是没有接口的,扩展很困难,要变化的话只能改代码:因为接口对单例没有意义,单例要求“自行实例化”,而接口、抽象类是不能被实例化的
  2. 单例模式和单一职责原则冲突的:一个类应该只做自己职责内的事,而不应该关心自己是否是单例的

实现

说完概念说实现,单例模式常见的实现有饿汉式懒汉式

*无论是哪种单例模式,其构造方法都是private的,在他人需要获取该类的单例时,只需调用类方法getInstance()即可

饿汉式

饿汉式单例模式将自身的类实例初始化到类私有静态常量

1. 最常规的饿汉式思路

最常规的饿汉式就像下面的代码,平平无奇

public class HungryMan { 

    
    private static final HungryMan instance = new HungryMan();

    
    private HungryMan() { };

    
    public static HungryMan getInstance() { 
        return instance;
    }
}

2. 饿汉式 with 枚举

我们应知道,每一个枚举类型和定义的枚举变量在JVM中都是唯一的,根据这个特性,我们也可以利用枚举来创建单例模式

用枚举来创建单例模式,优点有:

  1. 实现超简单
  2. 不怕反射和反序列化的破坏
public enum HungryManWithEnum { 
    INSTANCE;

    public void doSomething(){ 
        System.out.println("借助枚举实现饿汉式单例 - 被调用啦~");
    }
}

我们只定义一个枚举变量INSTANCE,那么INSTANCE就已经是这个类的唯一实例了,在其它类的方法中,我们可以直接使用类名.INSTANCE.枚举实例方法来调用单例方法,非常简单直观

懒汉式

在饿汉式中,其将其自身的类实例直接初始化到类私有静态常量,在类被创建的时候,实例也会一起被创建

但有时候类被创建时,我们还没到使用它的实例的时候,这时生成的实例就是没有用的,创建实例造成了开销,也浪费了内存空间

有没有办法让程序在调用getInstance()方法时才生成被调用类的实例呢?有

public class LazyManWithoutDCL { 

    private static LazyManWithoutDCL instance = null;

    private LazyManWithoutDCL(){ };

    public static LazyManWithoutDCL getInstance(){ 
        if(instance == null){ 
            instance = new LazyManWithoutDCL();
        }
        return instance;
    }
}

我们将实例的生成放到getInstance()方法中,这样,就能在调用getInstance()方法时,才生成实例了,解决了饿汉式的问题

懒汉式的线程安全问题

刚实现完懒加载,问题紧接着就来了,像上面的代码,在串行运行的时候是没有问题的,但是,在并发的情况下呢?我们来做一个测试


public class Tester { 

    private static int testCount = 20;

    private static CountDownLatch cdl = new CountDownLatch(testCount);

    public static void LazyManWithoutDCLTester() throws InterruptedException { 
        System.out.println("懒汉式不带DCL测试开始");
        // 开启20个线程,获取懒汉式不带DCL的实例并打印其地址
        for(int i=0;i<testCount;++i){ 
            new Thread(()->{ 
                System.out.println(LazyManWithoutDCL.getInstance());
                cdl.countDown();
            }).start();
        }
        cdl.await();
        System.out.println("懒汉式不带DCL测试结束");
    }

    public static void main(String[] args) throws InterruptedException { 
        LazyManWithoutDCLTester();
    }
}

我们开启20个线程,并发地访问这个单例,来看一下输出的结果:

懒汉式不带DCL测试开始
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@d718472
design_patterns.singleton.LazyManWithoutDCL@d718472
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@a47a58
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
design_patterns.singleton.LazyManWithoutDCL@7fb62507
懒汉式不带DCL测试结束

观察输出的内存地址,竟然输出了三种不一样的,这说明这个懒汉式的单例模式竟然创建了三个实例,真的太不靠谱了

那么,有解决办法吗?

线程安全的懒汉式

1. 懒汉式 with 同步方法

不就是不同步出问题了嘛,加个synchronized就行了~

    public static synchronized LazyManWithoutDCL getInstance(){ 
        if(instance == null){ 
            instance = new LazyManWithoutDCL();
        }
        return instance;
    }

确实可以解决问题,不过synchronized锁的粒度也太大了,这样子写的单例运行效率极低

2. 懒汉式 with DCL - 双重检验锁

锁的粒度太大,我们就缩小锁的粒度就好了~

public class LazyManWithDCL { 

    
    private volatile static LazyManWithDCL instance = null;

    
    private LazyManWithDCL() { };

    public static LazyManWithDCL getInstance(){ 
        // 第一重校验
        if(instance == null){ 
            synchronized (LazyManWithDCL.class){ 
                // 拿到锁后,进行第二重校验,避免第二个进来的线程再次生成实例
                if(instance == null){ 
                    instance = new LazyManWithDCL();
                }
            }
        }
        return instance;
    }
}

需要注意的点:

  1. 如果没有双重检验,只有一次检验,那么有可能:第一个线程拿到锁后令instance = new Instance();,在缓存回写的过程中,第二个线程拿到锁了,又执行一遍instance = new Instance();…这样在内存中就不是单例了
  2. 如果静态变量instance没有使用volatile修饰,则第一个线程拿到锁执行完instance = new Instance();后没有进行缓存回写,第二个线程不知道第一个线程已经给instance赋值了,看到的还是instance == null,就会继续执行instance = new Instance();

3. 懒汉式 with 私有静态内部类

我们知道,一个类在被加载时,才会将类里的内容进行初始化
我们在单例模式类里面再创建一个私有的静态内部类,这个内部类只能被它的外部类所创建,且只能创建一次(线程安全)

而在外部调用单例模式类的getInstance()时,由getInstance()去返回存在内部类里的单例,这样,在第一次调用getInstance()时,内部类才会被加载(懒加载),并创建单例

public class LazyManWithStaticInnerClass { 

    private static class MyInnerClass{ 
        public static final LazyManWithStaticInnerClass instance = new LazyManWithStaticInnerClass();
    }

    private LazyManWithStaticInnerClass(){ };

    public static LazyManWithStaticInnerClass getInstance(){ 
        // 直接调用静态内部类的实例
        return MyInnerClass.instance;
    }
}

这种方式实现的懒汉式就好像饿汉式+嵌套一样,实现起来比较简单

以上就是单例模式的内容和实现啦,有错误的地方欢迎在评论区指正哦~

 
打赏
 本文转载自:网络 
所有权利归属于原作者,如文章来源标示错误或侵犯了您的权利请联系微信13520258486
更多>最近资讯中心
更多>最新资讯中心
0相关评论

推荐图文
推荐资讯中心
点击排行
最新信息
新手指南
采购商服务
供应商服务
交易安全
关注我们
手机网站:
新浪微博:
微信关注:

13520258486

周一至周五 9:00-18:00
(其他时间联系在线客服)

24小时在线客服