创建型设计模式--为什么要使用单例

GoF 是 "Gang of Four"(四人帮)的简称,它们是指 4 位著名的计算机科学家:
Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides。他们合作编写
了一本非常著名的关于设计模式的书籍《Design Patterns: Elements of Reusable
Object-Oriented Software》(设计模式:可复用的面向对象软件元素)。这本书在
软件开发领域具有里程碑式的地位,对面向对象设计产生了深远影响。

GoF 提出了 23 种设计模式,将它们分为三大类:
1. 创建型模式(Creational Patterns):这类模式主要关注对象的创建过程。它们
分别是:
单例模式(Singleton)
工厂方法模式(Factory Method)
抽象工厂模式(Abstract Factory)
建造者模式(Builder)
原型模式(Prototype)
2. 结构型模式(Structural Patterns):这类模式主要关注类和对象之间的组合。
它们分别是:
适配器模式(Adapter)
桥接模式(Bridge)
组合模式(Composite)
装饰模式(Decorator)
外观模式(Facade)
享元模式(Flyweight)
代理模式(Proxy)
3. 行为型模式(Behavioral Patterns):这类模式主要关注对象之间的通信。它们
分别是:
职责链模式(Chain of Responsibility)
命令模式(Command)
解释器模式(Interpreter)
迭代器模式(Iterator)
中介者模式(Mediator)
备忘录模式(Memento)
观察者模式(Observer)
状态模式(State)
策略模式(Strategy)
模板方法模式(Template Method)

访问者模式(Visitor)

这些设计模式为面向对象软件设计提供了一套可复用的解决方案。掌握和理解这些模
式有助于提高软件开发人员的编程技巧和设计能力。

第一章 单例设计模式
单例设计模式(Singleton Design Pattern)理解起来非常简单。一个类只允许创建
一个对象(或者实例),那这个类就是一个单例类,这种设计模式就叫作单例设计模
式,简称单例模式

为什么要使用单例
1、表示全局唯一
如果有些数据在系统中应该且只能保存一份,那就应该设计为单例类。如:
配置类:在系统中,我们只有一个配置文件,当配置文件被加载到内存之后,应
该被映射为一个唯一的【配置实例】,此时就可以使用单例,当然也可以不用。
全局计数器:我们使用一个全局的计数器进行数据统计、生成全局递增ID等功
能。若计数器不唯一,很有可能产生统计无效,ID重复等。

public class GlobalCounter{
	private AtomicLong atomicLong = new AtomicLong(0);
	private static final GlobalCountter instance = new FlobalCounter();
	private GlobalCounter(){}
	public static GlobalCounter getInstance(){
		return instance;
	}
	public long getId(){
		return atomicLong.incrementAndGet();
	}
}

long courrentNumber = GloablCounter.getInstance().getId();

2、处理资源访问冲突
如果让我们设计一个日志输出的功能,你不要跟我杠,即使市面存在很多的日志框
架,我们也要自己设计。
如下,我们写了简单的小例子:

public class Logger {
private String basePath = "D://info.log";
private FileWriter writer;
public Logger() {
File file = new File(basePath);
try {
writer = new FileWriter(file, true); //true表示追加写入
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void log(String message) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
}

这样的话我们就可以把这个原本我们所要输出的相关的信息方法这个我们所制定的文件当中

@RestController("user")
public class UserController {
public Result login(){
// 登录成功
Logger logger = new Logger();
logger.log("tom logged in successfully.");
// ...
return new Result();
}
}

多个logger实例,在多个线程中,同时操作同一个文件,就可能产生相互覆盖的问题。
因为tomcat处理每一个请求都会使用一个新的线程(暂且不考虑多路复用)。此时
日志文件就成了一个共享资源,但凡是多线程访问共享资源,我们都要考虑并发修改
产生的问题。
有的同学可能想到如下的解决方案,加锁呀,代码如下:

public synchronized void log(String message) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

 

public void write(String str, int off, int len) throws IOException {
synchronized (lock) {
char cbuf[];
if (len <= WRITE_BUFFER_SIZE) {
if (writeBuffer == null) {
writeBuffer = new char[WRITE_BUFFER_SIZE];
}
cbuf = writeBuffer;
} else { // Don't permanently allocate very large buffers.
cbuf = new char[len];
}
str.getChars(off, (off + len), cbuf, 0);
write(cbuf, 0, len);
}
}

当然,加锁是一定能解决共享资源冲突问题的,我们只要放大锁的范围从【this】到
【class】,这个问题也是能解决的,代码如下:

public void log(String message) {
synchronized (Logger.class) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}

从以上的内容我们也发现了:
如果使用单个实例输出日志,锁【this】即可。
如果要保证JVM级别防止日志文件访问冲突,锁【class】即可。
如果要保证集群服务级别的防止日志文件访问冲突,加分布式锁即可。
如果我们是一个简单工程,对日志输入要求不高。单例模式的解决思路就十分合适,
既然同一个Logger无法并行输出到一个文件中,那么针对这个日志文件创建多个
Logger实例也就失去了意义,如果工程要求我们所有的日志输出到同一个日志文件
中,这样其实并不需要创建大量的Logger实例,这样的好处有:
一方面节省内存空间。
另一方面节省系统文件句柄(对于操作系统来说,文件句柄也是一种资源,不能
随便浪费)。
按照这个设计思路,我们实现了 Logger 单例类。具体代码如下所示:
public void log(String message) {
synchronized (Logger.class) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

public class Logger {
private String basePath = "D://log/";
除此之外,并发队列(比如 Java 中的 BlockingQueue)也可以解决这个问题:多个
线程同时往并发队列里写日志,一个单独的线程负责将并发队列中的数据写入到日志
文件。这种方式实现起来也稍微有点复杂。当然,我们还可将其延伸至消息队列处理
分布式系统的日志。
二、如何实现一个单例
常见的单例设计模式,有如下五种写法,在编写单例代码的时候要注意以下几点:
1、构造器需要私有化
private static Logger instance = new Logger();
private FileWriter writer;
private Logger() {
File file = new File(basePath);
try {
writer = new FileWriter(file, true); //true表示追加写入
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static Logger getInstance(){
return instance;
}
public void log(String message) {
try {
writer.write(message);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public void setBasePath(String basePath) {
this.basePath = basePath;
}
}

除此之外,并发队列(比如 Java 中的 BlockingQueue)也可以解决这个问题:多个
线程同时往并发队列里写日志,一个单独的线程负责将并发队列中的数据写入到日志
文件。这种方式实现起来也稍微有点复杂。当然,我们还可将其延伸至消息队列处理
分布式系统的日志。

对于我们为什么要去学习相关的单例模式:

我们可以做出一下的相关的总结

1.表示全局的唯一性

2.处理多个线程下访问同一个资源的时候访问这个出现的相关的资源访问冲突

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值