什么是单例模式呢?百度百科对单例模式做出这样的解释:单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实例。
定义说明了单例模式的重点就是一个类只有一个对象实例,不是2个或多个。在传统的OS(操作系统中),只有单线程,后来为了使多任务能在同一个操作系统中并行运行,引进了多线程的概念。在这里讲述线程是为了接下来更好的介绍单例模式的设计。
单例模式设计一:饿汉式(Lazy loadding)
public class Singleton {
private Singleton(){ }
private static final Singleton singleton = new Singleton() ; //注意这里加了final,确保该实例不被jvm回收
private static Singleton getInstance(){
return singleton ;
}
}
单例模式设计二:懒汉式,因为单例模式一中不管该资源(Singleton.class)是否被请求,它都会创建一个对象,占用jvm内存。既然这样,那我们就改进一下吧,所以有了下述代码
public class Singleton {
private Singleton(){ }
private static Singleton singleton = null ;
private static Singleton getInstance(){
if(singleton == null){
singleton = new Singleton() ;
}
return singleton ;
}
}
单例模式设计三:在单例模式设计二中存在了多线程导致不能是程序正常运行,那么我们就给程序加个锁吧,先让一个线程执行完,再让另一个线程执行。
public class Singleton {
private Singleton(){ }
private static Singleton singleton = null ;
private static synchronized Singleton getInstance(){
if(singleton == null){
singleton = new Singleton() ;
}
return singleton ;
}
}
单例模式设计四:单例模式三加了同步锁之后,虽然在很大程度上避免了两个实例的出现概率,但是给系统加了额外的开销,这无疑还不是一个最好的解决方法。那就劫机者改进代码吧。
public class Singleton {
private Singleton(){ }
private static Singleton singleton = null ;
private static Singleton getInstance(){
if(singleton == null){ //1
synchronized (Singleton.class) { //2
singleton = new Singleton() ; //3
}
}
return singleton ;
}
}
a:线程A执行到1挂起,线程BA认为singleton为null
b:线程B执行到1挂起,线程B认为singleton为null
c:线程A被唤醒执行synchronized块代码,走完创建了一个对象
d:线程B被唤醒执行synchronized块代码,走完创建了另一个对象
单例模式设计五:单例模式设计四还是避免不了两个实例出现的情况,那我就来个双重检查锁定吧(singleton == null )。
public class Singleton {
private Singleton(){ }
private static Singleton singleton = null ;
private static Singleton getInstance(){
if(singleton == null){ //1
synchronized (Singleton.class) { //2
if(singleton == null){ //3
singleton = new Singleton() ; //4
}
}
}
return singleton ;
}
}
在同步锁代码块内部,再判断一次对象是否为null,为null才创建对象。这种写法已经接近完美:
a:线程A执行到1,已经进入synchronized的时候,线程挂起,线程A占有Singleton.class资源锁;
b:线程B执行到1,当它准备synchronized块时,因为Singleton.class被占用,线程2阻塞;
c:线程A被唤醒,判断出对象为null,执行完创建一个对象
d:线程B被唤醒,判断出对象不为null,不执行创建语句
如此分析,发现似乎没问题。
但是实际上并不能保证它在单处理器或多处理器上正确运行;
问题就出现在singleton = new Singleton()这一行代码。它可以简单的分成如下三个步骤:
mem= singleton();//1
instance = mem;//2
constructorSingleton(instance);//3
这行代码先在内存开辟空间,赋给singleton的引用,然后执行new 初始化数据,但是注意初始化是要消耗时间。如果此时线程C(另一个线程)在执行步骤1的时候,发现singleton 为非null,就直接返回,那么线程C返回的其实是一个没构造完成的对象。
我们期望1,2,3 按照反序执行,但是实际jvm内存模型,并没有明确的有序指定。
这归咎于java的平台的内存模型允许“无序写入”。
单例模式设计六:引入volatile关键字,用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最的值。volatile很容易被误用,用来进行原子性操作。
public class Singleton {
private Singleton(){ }
private static volatile Singleton singleton = null ;
private static Singleton getInstance(){
if(singleton == null){ //1
synchronized (Singleton.class) { //2
if(singleton == null){ //3
singleton = new Singleton() ; //4
}
}
}
return singleton ;
}
}
volatile 变量具有 synchronized 的可见性特性,但是不具备原子特性。这就是说线程能够自动发现 volatile 变量的最新值。
而volatile使用时有明确的规定:
(1)对变量的写操作不依赖于当前值;
(2)该变量没有包含在具有其他变量的不变式中;
只有在状态真正独立于程序内其他内容时才能使用 volatile。
读者想了解更多关于volatile关键字的信息,可以参考这篇文章 Java 理论与实践: 正确使用 Volatile 变量
这篇文章时笔者偶尔看到一边博客上详细介绍了单例模式的设计,加上自己的理解而写出来了,略有抄袭的嫌疑,姑且算是原创吧。不过这并不是重点,重点时我们怎么设计这个单例模式。其实作为一名程序员,首先要懂得抄代码(读懂的基础上抄),接下来才是写代码。就好比我们小学学习26个字母拼音时,因为我们读懂了26个字母,渐渐会运用他们(相当于一个抄袭阶段),接下来才是组合拼音形成单词,多个单词练成一片优美的文章(属于写的阶段了)。
写代码也是一样,一样的道理,个人感觉在在知识的世界里不存在无所谓的抄袭。其实写这篇文章主要是想体现我们在做任何是的时候,刚开始提出的方案可能是可行的,但不一定是最优的,经过不断的修改,才有可能使这个方案达到最优(但这也只是可能而已,不一定有最优的,哈哈)重要的是体验这个修改的阶段,弄清修改的原因和目的。从这反射到大学学习上来,刚学习一些课程的时候,因为没有相应的背景知识,所以不想学,当然课程也没学懂,当有一天需要这门课的知识时,我们强迫自己去学习这门课,背景知识没有就自己去网上搜罗,后来才发现这门课的用处体现出来了。在这里,读者想表达的是中国大学的教育应该理论与实践并行发展,不是大学3或4年学习了大量的理论知识,到大三或大四的时候去搞一个毕业实习,让学生体会到:“哇,原来我学到的理论知识能在这里运用,真棒“。虽然亡羊补牢,为时不晚,但是现在物欲横流的社会,大学生能有几个4年一心在学习理论知识,到最后才运用到实践去呢?现在听老师口头上说这门课的知识以后在哪里会用到,我们需要的不是一个口头说说,而是切切实实的学了就要用去实践,在实践中懂得更多。
上面那一段话就当作笔者的牢骚话,不必在意哈。
最后,送给广大读者一句话:”历史没有真正的真相,只有残存的道理“。(这是今天上算法课时马新老师提到的一句话,个人感觉很受用,和这篇文章无关哈)