单例式是最常见的设计模式之一,但是对于单例式,还是有一些问题,我们希望能够着重的搞明白一下的几个问题
为什么使用单例
单例有哪些问题
单例和静态类的区别
有什么替代的方案
我们将依次的对这些问题进行相关的解答
单例式的原则很简单,就是一个类只允许创建一个对象实例,这种单个的设计模式就是单例设计模式
那么单例式常用于什么地方呢?
一,处理资源访问冲突
如果我们自定义实现了一个日志打印类Logger类,那么可以做的实现如下
public class Logger {
private FileWriter writer; public Logger() { File file = new File(“/Users/wangzheng/log.txt”); writer = new FileWriter(file, true); //true表示追加写入 } public void log(String message) { writer.write(mesasge); } } |
看起来还可以,但是实际用的时候,会发现多个Logger在写入同一个文件的时候,会出现日志信息互相覆盖的情况,出现了互相覆盖的情况,就是FileWriter写之间的冲突问题,那么如何解决呢?是否可以给log函数加上锁呢?答案是否定的,因为加上锁的话,也只是锁住了一个对象,无法解决多个对象直接的问题
那么,可以试着加上一个类级别的锁,来解决共享问题,让所有的对象共享一个类级别的锁,就避免了不同对象之间的调用导致的日志覆盖的问题
public class Logger {
private FileWriter writer; public Logger() { File file = new File(“/Users/wangzheng/log.txt”); writer = new FileWriter(file, true); //true表示追加写入 } public void log(String message) { synchronized(Logger.class) { // 类级别的锁 writer.write(mesasge); } } } |
再深一点,可以考虑一个分布式锁的解决方案,但是一个分布式锁并不容易实现
对于这个需求,我认为使用一个消息队列是一个很好的选择,能够做到读写分离
在之后,就是单例式设计思路了
保证只有一个Logger对象,在Logger对象中,利用FileWirter的锁来进行加锁解锁
二,单例式的资源类,全局唯一
像是我们在项目中的配置文件,理所当然的只需要读取一次就可以了,所以可以编写一个单例式的配置资源类,像是我们之前讲解的ID自动生成器,就可以做到整个项目只有一个接口,而且有了多个生成对象,很可能形成重复ID的情况
那首先介绍最简单的单例实现方式
单例式的实现主要就是注意,构造函数需要会private的,避免外部new来创建
考虑创建对象时候的线程安全
是否需要延迟加载
getInstance()性能是否高
饿汉式
实现很简单,就是有一个静态的类,在初始化的时候就初始化好,然后直接获取
public class IdGenerator {
private AtomicLong id = new AtomicLong(0); private static final IdGenerator instance = new IdGenerator(); private IdGenerator() {} public static IdGenerator getInstance() { return instance; } public long getId() { return id.incrementAndGet(); } } |
但是并不支持延迟加载,对于一些初始化耗时长的对象,是一种浪费资源的问题,但这种不支持懒加载的好处也是都放在了启动时候去加载,避免运行时候的性能,而且有问题能够提早的去暴露,如果资源不够,也能尽早的指导
对应的懒汉式
就是支持了延迟加载
public class IdGenerator {
private AtomicLong id = new AtomicLong(0); private static IdGenerator instance; private IdGenerator() {} public static synchronized IdGenerator getInstance() { if (instance == null) { instance = new IdGenerator(); } return instance; } public long getId() { return id.incrementAndGet(); } } |
但是这就有一个问题,因为getInstance()上加上了锁,那么会导致这个函数的并发度很低,基本说就是单线程的,那么如果多次调用,很难以接受
于是在此基础上,改进得到了双重检测的实现方式
public class IdGenerator {
private AtomicLong id = new AtomicLong(0); private static IdGenerator instance; private IdGenerator() {} public static IdGenerator getInstance() { if (instance == null) { synchronized(IdGenerator.class) { // 此处为类级别的锁 if (instance == null) { instance = new IdGenerator(); } } } return instance; } public long getId() { return id.incrementAndGet(); } } |
虽然存在着指令重排序的问题,但是可以通过加上volatile关键字来禁止指令重排序,而且,只有低版本Java才会出现这个情况,实际上JDK内部已经将new和初始化操作设计为了原子类,可以避免重排序
同样,为了减少加锁,并且支持延迟加载
还有着静态内部类这种东西
public class IdGenerator {
private AtomicLong id = new AtomicLong(0); private IdGenerator() {} private static class SingletonHolder{ private static final IdGenerator instance = new IdGenerator(); } public static IdGenerator getInstance() { return SingletonHolder.instance; } public long getId() { return id.incrementAndGet(); } } |
这样的话,只有外部类调用getInstance的时候,才会加载静态内部类,才会获取到instance,能保证唯一性和延迟加载
最后,就是枚举
public enum IdGenerator {
INSTANCE; private AtomicLong id = new AtomicLong(0); public long getId() { return id.incrementAndGet(); } } |
同样能够保证唯一性,但是没法做到懒加载
那么本章我就讲完了,很简单
说明了单例式的定义
就是一个类只能创建一个对象,这个类就是单例式的模式
常见的单例的用处在于,某些数据在系统中只能保存一份,那么就应该设计为单例式,比如系统的配置信息类
常见的实现由
饿汉式
在类加载的时候,就将静态实例创建好了,只是不支持延迟加载
懒汉式
支持了延迟加载,但是由于是加上了重量级的锁,于是并发度非常的低
双重检测
支持延迟加载,并且确保了高并发的单例式的实现方式
静态内部类
利用了静态内部类的机制,做到既能保证延迟加载,也能做到高并发
枚举
利用枚举类的特性,做到了线程安全和实例的唯一性