这一系列的文章我会从代码的思路上介绍目前编程中存在的编程模式,并且赋予这些代码的一些实例场景。
单例模式,即单实例模式,即在程序运行生命周期内。要么不存在一个类的实例(这里有些不严谨,理论上来说是否存在取决于代码设计和是否调用),要么只存在全局一个实例对象。
这样的例子在程序设计中有很多。对于某些特殊的类,在程序中只能new出一个实例,如果多个实例的话,暂且不说线程安全问题,从程序逻辑上面也会有很多问题。
比如我们只有一台打印机:
public class Printer{
/**
* Is Printer Running?
* */
private boolean isRunning;
/**
* Default Constructor
* */
public Printer(){
}
public void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
}
那么我们就不能在需要的时候都来创建一个新的实例,尤其是当一个实例有相应状态的时候(比如打印机在Isrunning = true的状态时候,此时该打印机是不能接受新任务的),所以此时情况我们需要创建一个全局唯一的实例来帮助我们完成程序逻辑。
- 外界不能随意创建实例->构造方法私有
- 外界不能创建,所以类需要自己创建唯一实例->类中需要维护自身实例的静态变量
- 外界可以获取创建的静态变量->用公开静态方法获取静态实例
基于以上三个原则,我们重新编写上面的代码:
/**
* Created by youweixi on 15/11/19.
*/
public class Printer{
/**
* Is Printer Running?
* */
private boolean isRunning;
/**
* Static Self
* */
private static Printer printer;
/**
* Default Constructor
* */
private Printer(){
}
/**
* Public GetInstance
* */
public static Printer getInstance(){
if (printer == null){
printer = new Printer();
}
return printer;
}
public void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
}
这样我们在程序中就不能通过new来创建Printer对象。只能通过getInstance方法来获取静态唯一实例。
但是上面的代码是存在问题的。
我们知道,程序在CPU上面是相互争夺时间片来运行的,即多个异步过程(进程、线程)之间的代码运行顺序是不确定的。
如果有两个代码片段同时需要调用getInstance来获取实例:
- 此时第一个线程发现printer == null是成立的,时间片耗尽,CPU转向另一个线程。
- 另一个线程也进入函数,判断printer == null仍然成立,分配实例,然后继续运行接下来代码。
- 此时时间片耗尽,CPU转向第一个线程,线程重新分配了一个新的实例造成多实例覆盖情况。
我们可以看到上面的单例模式在多线程中会发生问题,原因在于调用的方法和单例创建的时间。基于这两个原因,我们可以对上面的单例模式进行改进。
1. 提前单例创建时间
public class Printer{
/**
* Is Printer Running?
* */
private boolean isRunning;
/**
* Static Self
* */
private static Printer printer = new Printer();
/**
* Default Constructor
* */
private Printer(){
}
/**
* Public GetInstance
* */
public static Printer getInstance(){
return printer;
}
}
即在编译时候创建静态变量,这样的话,在调用getInstance方法的时候不必再进行检测创建。这样的话可能会带来资源浪费,尤其是在很长一段时间内没有发生实例获取的项目中。
2. 对getInstance加同步控制synchronized
/**
* Public GetInstance
* */
public static synchronized Printer getInstance(){
if (printer == null){
printer = new Printer();
}
return printer;
}
这样的话可以保证每次只有一个线程拿到方法锁进入方法。但是这种方式会带来性能损失。如果工程中频繁通过调用getInstance获取实例而出现等待方法锁。
3. 二次检测
所谓的二次检测即是对方法二的改进,进行第一次检测的时候过滤大部分请求已经存在静态实例的请求,第二次检测才真正进行同步块中的静态实例分配。可以说这种方法是最安全和节省资源的。
/**
* Public GetInstance
* */
public static Printer getInstance(){
if (printer == null){
synchronized (this){
if (printer == null){
printer = new Printer();
}
}
}
return printer;
}
这样的话即时有两个程序同时进入printer==null的程序逻辑也还是会只有一个会拿到内层检测创建的同步锁完成第一次创建。