设计模式学习(二):单例模式

设计模式,学习,模式 · 浏览次数 : 325

小编点评

**单例模式的实现** * 使用静态内部类或枚举实现单例 * 使用 HashMap 来存储对象类型和对象之间的对应关系 * 通过一个 Map 来控制对象的个数 **多例模式的实现** * 通过一个 Map 来存储对象类型和对象之间的对应关系 * 通过一个 Map 来控制对象的个数 * 使用一个 Spring 中的 AbstractBeanFactory 中包含的两个功能

正文

设计模式学习(二):单例模式

作者:Grey

原文地址:

博客园:设计模式学习(二):单例模式

CSDN:设计模式学习(二):单例模式

单例模式

单例模式是创建型模式。

单例的定义:“一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”定义中提到,“一个类只允许创建唯一一个对象”。那对象的唯一性的作用范围是指进程内只允许创建一个对象,也就是说,单例模式创建的对象是进程唯一的(而非线程)

image

为什么要使用单例

  1. 处理资源访问冲突,比如写日志的类,如果不使用单例,就必须使用锁机制来解决日志被覆盖的问题。

  2. 表示全局唯一类,比如配置信息类,在系统中,只有一个配置文件,当配置文件加载到内存中,以对象形式存在,也理所应当只有一份;唯一 ID 生成器也是类似的机制。如果程序中有两个对象,那就会存在生成重复 ID 的情况,所以,我们应该将 ID 生成器类设计为单例。

饿汉式

类加载的时候就会初始化这个实例,JVM 保证唯一实例,线程安全,但是可以通过反射破坏

方式一

public class Singleton1 {
    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

方式二

public class Singleton2 {
    private static final Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }
    private Singleton2() {
     
    }
    public static Singleton2 getInstance() {
        return INSTANCE;
    }
}

注意:

这种方式不支持延迟加载,如果实例占用资源多(比如占用内存多)或初始化耗时长(比如需要加载各种配置文件),提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,如果初始化耗时长,那最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能,我们可以将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错(比如 Java 中的 PermGen Space OOM ),我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。

这两种方式都可以通过反射方式破坏,例如:

Class<?> aClass=Class.forName("singleton.Singleton2",true,Thread.currentThread().getContextClassLoader());
Singleton2 instance1=(Singleton2)aClass.newInstance();
Singleton2 instance2=(Singleton2)aClass.newInstance();
System.out.println(instance1==instance2);

懒汉式

虽然可以实现按需初始化,但是线程不安全, 因为在判断 INSTANCE == null 的时候,有可能出现一个线程还没有把 INSTANCE初始化好,另外一个线程判断 INSTANCE==null 得到 true,就会继续初始化

public class Singleton3 {
    private static Singleton3 INSTANCE;

    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        if (INSTANCE == null) {
            // 模拟初始化对象需要的耗时操作
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }
}

为了防止线程不安全,可以在 getInstance 方法上加锁,这样既实现了按需初始化,又保证了线程安全,

但是加锁可能会导致一些性能的问题:我们给 getInstance()这个方法加了一把大锁,导致这个函数的并发度很低。量化一下的话,并发度是 1,也就相当于串行操作了。如果这个单例类偶尔会被用到,那这种实现方式还可以接受。但是,如果频繁地用到,那频繁加锁、释放锁及并发度低等问题,会导致性能瓶颈,这种实现方式就不可取了。

public class Singleton4 {
    private static Singleton4 INSTANCE;

    private Singleton4() {
    }

    public static synchronized Singleton4 getInstance() {
        if (INSTANCE == null) {
            // 模拟初始化对象需要的耗时操作
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton4();
        }
        return INSTANCE;
    }
}

为了提升一点点性能,可以不给 getInstance() 整个方法加锁,而是对 INSTANCE 判空这段代码加锁, 但是这样一来又带来了线程不安全的问题

public class Singleton5 {
    private static Singleton5 INSTANCE;

    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton5.class) {
                // 模拟初始化对象需要的耗时操作
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Singleton5();
            }
        }
        return INSTANCE;
    }
}

Double Check Locking 模式,就是双加锁检查模式,这种方式中,volatile 关键字是必需的,目的为了防止指令重排,生成一个半初始化的的实例,导致生成两个实例。

具体可参考 双重检索(DCL)的思考: 为什么要加volatile?

public class Singleton6 {
    private volatile static Singleton6 INSTANCE;

    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        if (INSTANCE == null) {
            synchronized (Singleton6.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}

以下两种更为优雅的方式,既保证了线程安全,又实现了按需加载。

方式一:静态内部类方式, JVM 保证单例,加载外部类时不会加载内部类,这样可以实现懒加载

public class Singleton7 {
    private Singleton7() {
    }

    public static Singleton7 getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {
        private static final Singleton7 INSTANCE = new Singleton7();
    }

}

方式二: 使用枚举, 这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化,这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。

public enum Singleton8 {
    INSTANCE;
}

单例模式的替代方案

使用静态方法

   // 静态方法实现方式
public class IdGenerator {
    private static AtomicLong id = new AtomicLong(0);
   
    public static long getId() { 
       return id.incrementAndGet();
    }
}

// 使用举例
long id = IdGenerator.getId();

使用依赖注入

   // 1. 老的使用方式
   public demofunction() {
     //...
     long id = IdGenerator.getInstance().getId();
     //...
   }
   
   // 2. 新的使用方式:依赖注入
   public demofunction(IdGenerator idGenerator) {
     long id = idGenerator.getId();
   }
   // 外部调用demofunction()的时候,传入idGenerator
   IdGenerator idGenerator = IdGenerator.getInsance();
   demofunction(idGenerator);

线程单例

通过一个 HashMap 来存储对象,其中 key 是线程 ID,value 是对象。这样我们就可以做到,不同的线程对应不同的对象,同一个线程只能对应一个对象。实际上,Java 语言本身提供了 ThreadLocal 工具类,可以更加轻松地实现线程唯一单例。不过,ThreadLocal 底层实现原理也是基于下面代码中所示的 HashMap 。


public class IdGenerator {
  private AtomicLong id = new AtomicLong(0);

  private static final ConcurrentHashMap<Long, IdGenerator> instances = new ConcurrentHashMap<>();

  private IdGenerator() {}

  public static IdGenerator getInstance() {
    Long currentThreadId = Thread.currentThread().getId();
    instances.putIfAbsent(currentThreadId, new IdGenerator());
    return instances.get(currentThreadId);
  }

  public long getId() {
    return id.incrementAndGet();
  }
}

集群模式下单例

集群模式下如果要实现单例需要把这个单例对象序列化并存储到外部共享存储区(比如文件)。进程在使用这个单例对象的时候,需要先从外部共享存储区中将它读取到内存,并反序列化成对象,然后再使用,使用完成之后还需要再存储回外部共享存储区。为了保证任何时刻,在进程间都只有一份对象存在,一个进程在获取到对象之后,需要对对象加锁,避免其他进程再将其获取。在进程使用完这个对象之后,还需要显式地将对象从内存中删除,并且释放对对象的加锁。

如何实现一个多例模式

“单例”指的是一个类只能创建一个对象。对应地,“多例”指的就是一个类可以创建多个对象,但是个数是有限制的,比如只能创建 3 个对象。多例的实现也比较简单,通过一个 Map 来存储对象类型和对象之间的对应关系,来控制对象的个数。

单例模式的应用举例

JDK 的 Runtime 类

public class Runtime {
  private static Runtime currentRuntime = new Runtime();

  public static Runtime getRuntime() {
    return currentRuntime;
  }
  
  /** Don't let anyone else instantiate this class */
  private Runtime() {}
.......
}

还有就是 Spring 中 AbstractBeanFactory 中包含的两个功能。

功能一,就是从缓存中获取单例 Bean

功能二,就是从 Bean 的实例中获取对象。

UML 和 代码

UML 图

代码

更多

设计模式学习专栏

参考资料

与设计模式学习(二):单例模式相似的内容:

设计模式学习(二):单例模式

设计模式学习(二):单例模式 作者:Grey 原文地址: 博客园:设计模式学习(二):单例模式 CSDN:设计模式学习(二):单例模式 单例模式 单例模式是创建型模式。 单例的定义:“一个类只允许创建唯一一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模式,简称单例模式。”定

设计模式学习(二)工厂模式——工厂方法模式+注册表

目录工厂方法模式的瑕疵注册表 工厂方法模式的瑕疵 在前一篇笔记中我们介绍了工厂方法模式,示例的类图如下: 考虑一种情况:现在要在程序运行时,根据外部资源,动态的实例化对象。也就是说在编译期我们无法知道要实例化的对象的类型。因此在实例化的过程中,就需要加以判断。 例如,在我的例子中,要根据连接到主机的

【23种设计模式】工厂方法模式(二)

## 前言 在讲述之工厂方法模式前,我们来先了解简单工厂模式,简单工厂模式是最简单的设计模式之一,它虽然不属于GoF的23种设计模式,但是应用也较为频繁,同时它也是学习其他创建型模式的基础。下面我们来先了解下简单工厂模式,然后针对它的缺点来引出工厂方法模式。 ## 简单工厂模式定义 **简单工厂模式

Hbase学习二:Hbase数据特点和架构特点

转载请注明出处: 1.Hbase数据特点 大:一个表可以有上亿行,上百万列。 面向列:面向列表(簇)的存储和权限控制,列(簇)独立检索。 稀疏:对于为空(NULL)的列,并不占用存储空间,因此,表可以设计的非常稀疏。 无模式:每一行都有一个可以排序的主键和任意多的列,列可以根据需要动态增加,同一张表

设计模式学习(三):工厂模式

设计模式学习(三):工厂模式 作者:Grey 原文地址: 博客园:设计模式学习(三):工厂模式 CSDN:设计模式学习(三):工厂模式 工厂模式 工厂模式是创建型模式,工厂模式分为:简单工厂,工厂方法和抽象工厂三种类型。 简单工厂 这个模式很简单,比如我们需要制造不同类型的鼠标,我们只需要创建一个鼠

设计模式学习(四):建造者模式

设计模式学习(四):建造者模式 作者:Grey 原文地址: 博客园:设计模式学习(四):建造者模式 CSDN:设计模式学习(四):建造者模式 建造者模式 建造者模式是创建型模式。 我们在对一个实体类进行属性的 get 或 set 的时候,可以通过封装一些常用的构造方法来简化实体类的构造。 比如 Ef

设计模式学习(五):原型模式

设计模式学习(五):原型模式 作者:Grey 原文地址: 博客园:设计模式学习(五):原型模式 CSDN:设计模式学习(五):原型模式 原型模式 原型模式是创建型模式。 如果对象的创建成本比较大,而同一个类的不同对象之间差别不大(大部分字段的值都相同),在这种情况下,我们可以利用对已有对象(原型)进

设计模式学习(六):代理模式

设计模式学习(六):代理模式 作者:Grey 原文地址: 博客园:设计模式学习(六):代理模式 CSDN:设计模式学习(六):代理模式 代理模式 代理模式是结构型模式,分为静态代理和动态代理。 静态代理 举个例子,假设需要在某个类的某段代码的前后加上日志记录,就可以通过静态代理的方式实现,代码如下

设计模式学习(七):适配器模式

设计模式学习(七):适配器模式 作者:Grey 原文地址: 博客园:设计模式学习(七):适配器模式 CSDN:设计模式学习(七):适配器模式 适配器模式 适配器模式是一种结构型模式。 举例说明,假设有一个播放器,需要根据不同格式以及对应的文件来播放,接口设计如下: public interface

设计模式学习(八):桥接模式

设计模式学习(八):桥接模式 作者:Grey 原文地址: 博客园:设计模式学习(八):桥接模式 CSDN:设计模式学习(八):桥接模式 桥接模式 桥接模式是一种结构型模式。它将抽象部分和实现部分分离,使他们可以独立地变化。 使用桥接模式,可以将抽象和具体的发展单独分支(即:抽象中持有一个具体的引用)