单例模式(三)

过气的,终究是过气了

上一章简单介绍了 UML 类图(二), 如果没有看过,请观看上一章

一. 单例模式

所谓的单例设计模式,就是采用一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,
并且该类只提供一个取得其对象实例的方法 (静态方法)

一.一 单例模式介绍

引用 菜鸟教程里面的单例模式介绍: https://www.runoob.com/design-pattern/singleton-pattern.html

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,
它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。

这个类提供了一种访问其唯一的对象的方式,

可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

一.二 介绍

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

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

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

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

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

应用实例:

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

优点:

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

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

使用场景:

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

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

二. 单例模式代码应用

二.一 八种模式

  1. 饿汉式 (静态常量)
  2. 饿汉式 (静态代码块)
  3. 懒汉式 (线程不安全)
  4. 懒汉式 (线程安全, 同步方法)
  5. 懒汉式 (线程安全,同步代码块)
  6. 双重检查
  7. 静态内部类
  8. 枚举

二.二 饿汉式 (静态常量)

直接构建对象

二.二.一 代码

public class Single01 {
    /**
    类的内部创建对象
     final static
     */
    private final static Single01 instance = new Single01();
    /**
    构建方法地私有化
     */
    private Single01() {

    }
    /**
      对外提供一个静态的公共方法
     */
    public static Single01 getInstance() {
        return instance;
    }
}

测试方法:

 	@Test
    public void oneTest() {
        Single01 single01 = Single01.getInstance();
        Single01 single02 = Single01.getInstance();

        log.info("是否相同: {}", single01 == single02);
        log.info(" hashcode 是否相同: {}", single01.hashCode() == single02.hashCode());
    }

单例模式(三)_第1张图片

二.二.二 优缺点

优点: 写法比较简单,就是在类装载的时候就完成实例化。 避免了线程同步的问题。

缺点: 在类装载的时候就完成实例化,没有达到 Lazy Loading 懒加载的效果。
如果从始至终从未使用过这个变量,则会造成内存的浪费。

这种方式 基于 classloader 机制避免了多线程同步的问题,不过 instance 在类装载时就进行实例化,
在单例模式中大多数都是调用 getInstance() 方法,但是导致类装载的原因有多种,因此不能确定有其他的方式 (其他的静态方法)
导致类装载, 这时候初始化 instance 就没有达到 lazy loading 的效果。

结论: 这种单例模式可用,但可能会造成内存浪费。

二.三 饿汉式 (静态代码块)

静态代码块里面构建对象

二.三.一 代码

public class Single02 {
    private static Single02 instance ;
    static {
        instance = new Single02();
    }
    private Single02 (){

    }

    public static Single02 getInstance() {
        return instance;
    }
}
@Test
    public void twoTest() {
        Single02 single01 = Single02.getInstance();
        Single02 single02 = Single02.getInstance();

        log.info("是否相同: {}", single01 == single02);
        log.info(" hashcode 是否相同: {}", single01.hashCode() == single02.hashCode());
    }

单例模式(三)_第2张图片

二.三.二 优缺点

  1. 这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,
    也是在类装载的时候,就执 行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。
  2. 结论:这种单例模式可用,但是可能造成内存浪费

二.四 懒汉式 (线程不安全)

方法中 验证 为空 再进行构建对象

二.四.一 代码

public class LanSingle03 {
    private static LanSingle03 instance;

    private LanSingle03 (){

    }

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

测试方法:

  @Test
    public void threeTest() {
        LanSingle03 single01 = LanSingle03.getInstance();
        LanSingle03 single02 = LanSingle03.getInstance();

        log.info("是否相同: {}", single01 == single02);
        log.info(" hashcode 是否相同: {}", single01.hashCode() == single02.hashCode());
    }

单例模式(三)_第3张图片

但是该方式在多线程环境下会存在并发问题

  @Test
    public void threadTest() throws Exception{
        for( int i = 0; i< 20; i++) {
            new Thread(()->{
                log.info(">>> 打印实例: {}", LanSingle03.getInstance());
            },i+"").start();
        }
        TimeUnit.SECONDS.sleep(2);
    }

单例模式(三)_第4张图片

二.四.二 优缺点说明

  1. 起到了 Lazy Loading 的效果,但是只能在单线程下使用。
  2. 如果在多线程下,一个线程进行了 if 判断语句,还没有来得及往下执行,另一个线程也通过了这个判断语句, 这时便会产生多个实例。
    所以在多线程环境下不可以使用这种方式

结论: 在实际开发中,不要使用这种方式

二.五 懒汉式 (线程安全,同步方法)

方法上添加 synchronized 进行同步

二.五.一 代码

public class LanSingle04 {
    private static LanSingle04 instance;

    private LanSingle04(){

    }

    public synchronized static LanSingle04 getInstance() {
        if (instance == null) {
            instance = new LanSingle04();
        }
        return instance;
    }
}
  @Test
    public void fourTest() {
        LanSingle04 single01 = LanSingle04.getInstance();
        LanSingle04 single02 = LanSingle04.getInstance();

        log.info("是否相同: {}", single01 == single02);
        log.info(" hashcode 是否相同: {}", single01.hashCode() == single02.hashCode());
    }

单例模式(三)_第5张图片

二.五.二 优缺点

  1. 解决了线程安全问题
  2. 效率太低了, 每个线程在想获得类的实例的时候,执行 getInstance() 方法都要进行同步。
    而其实这个方法只执行一次实例化代码就够了, 后面的想获得该类的实例的时候,直接 return 就行了。
    方法进行同步,效率太低。
  3. 结论: 在实际开发中,不推荐使用这种方式

二.六 懒汉式 (线程安全,同步代码块)

方法中,为空时, 同步类,同步代码块内部进行实例化

二.六.一 代码

public class LanSingle05 {
    private static LanSingle05 instance;

    private LanSingle05(){

    }

    public static LanSingle05 getInstance() {
        if (instance == null) {
            synchronized (LanSingle05.class){
                instance = new LanSingle05();
            }
        }
        return instance;
    }
}
    @Test
    public void fiveTest() {
        LanSingle05 single01 = LanSingle05.getInstance();
        LanSingle05 single02 = LanSingle05.getInstance();

        log.info("是否相同: {}", single01 == single02);
        log.info(" hashcode 是否相同: {}", single01.hashCode() == single02.hashCode());
    }

单例模式(三)_第6张图片

有线程安全的问题:

 @Test
    public void threadTest() throws Exception{
        for( int i = 0; i< 20; i++) {
            new Thread(()->{
                log.info(">>> 打印实例: {}", LanSingle05.getInstance());
            },i+"").start();
        }
        TimeUnit.SECONDS.sleep(2);
    }

单例模式(三)_第7张图片

二.六.二 优缺点

  1. 有线程同步问题

结论: 在实际开发中,不推荐使用这种方式

二.七 双重检查

双重检查,在 同步代码块内部,再判断一下是否为空, 为空才进行实例化

二.七.一 代码

public class CheckSingle06 {
    private static CheckSingle06 instance;

    private CheckSingle06(){

    }

    public static CheckSingle06 getInstance() {
        if (instance == null) {
            synchronized (CheckSingle06.class){
               if (instance == null) {
                   instance = new CheckSingle06();
               }
            }
        }
        return instance;
    }
}
  @Test
    public void sexTest() {
        CheckSingle06 single01 = CheckSingle06.getInstance();
        CheckSingle06 single02 = CheckSingle06.getInstance();

        log.info("是否相同: {}", single01 == single02);
        log.info(" hashcode 是否相同: {}", single01.hashCode() == single02.hashCode());
    }

单例模式(三)_第8张图片

二.七.二 优缺点

  1. Double-Check 概念是多线程开发中常使用到的, 进行了两次 if( instance == null) 检查,这样就可以保证线程安全了。
  2. 实例化代码只用了一次,后面再访问时, 判断 if (instance ==null) 直接 return 实例化对象 ,也避免了反复进行方法同步。
  3. 线程安全的: 会延迟加载, 效率较高。
  4. 结论: 在实际开发中, 推荐使用这种单例设计模式

二.八 静态内部类

定义一个静态的内部类, 内部类中属性进行构建

二.八.一 代码

public class InnerSingle07 {
    private InnerSingle07(){

    }

    private static class InnerClass {
        private static final InnerSingle07 INSTANCE = new InnerSingle07();
    }


    public static InnerSingle07 getInstance() {
       return InnerClass.INSTANCE;
    }
}

测试方法:

 @Test
    public void sevenTest() {
        InnerSingle07 single01 = InnerSingle07.getInstance();
        InnerSingle07 single02 = InnerSingle07.getInstance();

        log.info("是否相同: {}", single01 == single02);
        log.info(" hashcode 是否相同: {}", single01.hashCode() == single02.hashCode());
    }

单例模式(三)_第9张图片

二.八.二 优缺点

  1. 采用了类装载的机制来保证初始化实例只有一个线程
  2. 静态内部类方式在 InnerSingle07 类被装载时并不会立即实例化, 而是在需要实例化时,调用 getInstance 方法,
    才会装载 InnerClass 类,从而完成 InnerSingle07 的实例化。
  3. 类的静态属性只会在第一次加载类的时候初始化, 所以在这里, JVM 帮助我们保证了线程的安全性,
    在类进行初始化时,别的线程是无法进入的。
  4. 优点: 避免了线程不安全,利用静态内部类特点实现了延迟加载,效率高。
  5. 结论: 推荐使用

二.九 枚举

枚举 enum

二.九.一 代码

public enum EnumSingle08 {
   INSTANCE("1");

    private String name;

    EnumSingle08(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
@Test
    public void nightTest() {
        EnumSingle08 single01 = EnumSingle08.INSTANCE;
        EnumSingle08 single02 = EnumSingle08.INSTANCE;

        log.info("是否相同: {}", single01 == single02);
        log.info(" hashcode 是否相同: {}", single01.hashCode() == single02.hashCode());
    }

单例模式(三)_第10张图片

二.九.二 优缺点

  1. 这借助 JDK1.5 中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建 新的对象。
  2. 这种方式是 Effective Java 作者 Josh Bloch 提倡的方式
  3. 结论:推荐使用

三. Java 设计模式应用

java.lang.Runtime 类

是饿汉式第一种

单例模式(三)_第11张图片


本章节的代码放置在 github 上:

https://github.com/yuejianli/DesignPattern/tree/develop/Single


谢谢您的观看,如果喜欢,请关注我,再次感谢 !!!

你可能感兴趣的:(设计模式,单例模式,Java单例)