单例式是最常见的设计模式之一,但是对于单例式,还是有一些问题,我们希望能够着重的搞明白一下的几个问题

为什么使用单例

单例有哪些问题

单例和静态类的区别

有什么替代的方案

我们将依次的对这些问题进行相关的解答

单例式的原则很简单,就是一个类只允许创建一个对象实例,这种单个的设计模式就是单例设计模式

那么单例式常用于什么地方呢?

一,处理资源访问冲突

如果我们自定义实现了一个日志打印类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();

}

}

同样能够保证唯一性,但是没法做到懒加载

那么本章我就讲完了,很简单

说明了单例式的定义

就是一个类只能创建一个对象,这个类就是单例式的模式

常见的单例的用处在于,某些数据在系统中只能保存一份,那么就应该设计为单例式,比如系统的配置信息类

常见的实现由

饿汉式

在类加载的时候,就将静态实例创建好了,只是不支持延迟加载

懒汉式

支持了延迟加载,但是由于是加上了重量级的锁,于是并发度非常的低

双重检测

支持延迟加载,并且确保了高并发的单例式的实现方式

静态内部类

利用了静态内部类的机制,做到既能保证延迟加载,也能做到高并发

枚举

利用枚举类的特性,做到了线程安全和实例的唯一性

发表评论

邮箱地址不会被公开。 必填项已用*标注