
课程咨询: 400-996-5531 / 投诉建议: 400-111-8989
认真做教育 专心促就业
单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例。
通过上面百度百科的定义,我们大概可以知道:单例模式的目的是让某个类在系统中只有实例。
那么为什么要让系统中的某个类只存在一个实例呢?或者说什么样的类要使用单例模式生成对象呢?
举个简单的例子来说,不管你打开几个电脑上的任务管理器,在同一时刻你看到的进程或者应用程序都是完全相同的(如果不相同那你的电脑就该炸了),所以你每次只需要打开一个任务管理器就可以了,而不是同时打开多个。
这个在程序里面怎么理解呢?就是说某个类没有自己的状态,不管你实例化多少个这个类的对象都是完全一样的,更进一步来说的话就是如果在系统中存在多个这个类的对象可能会导致程序错误。
那么到底什么样的类可以做成单例呢?可以这么来说,能做成单例的类,在系统中的同一时刻必须只能有一个状态,如果存在多个状态就会导致程序错误。
当然,将系统中的某个类做成单例还有另外的一个好处就是可以减少系统资源消耗,因为对于一个无状态的类,只存在一个实例对象就可以尽可能的减少内存空间的浪费,减少无谓的GC消耗。
往常这时候就该上设计模式的类图了,但是单例模式实在是太简单了,以至于类图就不用上了,下面我们直接进入代码板块。
提起单例模式,我想大多人直接出现在脑海里的就是懒汉式和饿汉式,下面我们先来看看经典的懒汉式:
package com.pattern.single;
/**
*懒汉式单例模式
*/
public class LazySingleton {
/**
*静态对象
*/
private static LazySingleton lazySingleton;
/**
*私有的构造方法
*/
private LazySingleton(){}
/**
*静态的获取实例方法
* @return
*/
public static LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
当初写出一个这样的单例就觉得老牛逼了,我还记得当初刚学会那会儿我还在嘚瑟的教我的同桌说单例怎么写呢?当初说的话我都还记得,怎么写单例呢,其实就是三点,静态对象、私有构造方法、静态获取实例的方法,这三个组合到一起就是单例。
当时还在培训,所以觉得写出这个就老牛逼了,但是现在来看的话这个单例是有很大的问题的,最明显的就是不支持多线程,怎么办呢?说起多线程最容易让人想到的就是synchronized,那么上述程序要适合多线程使用,最简单的改造方法就是在获取实例的方法中使用synchronized关键字修饰,修改后的代码如下所示:
public static synchronized LazySingleton getInstance(){
if(lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
But,在看并发编程这本书的时候,我不记得书中的原话了,但是意思是这样的,什么呢?就是说,如果在做并发的时候只是一味的使用synchronized并不是一个很好的解决办法,因为这会导致在一个线程试行时很多线程都在无谓的等待的情况,这是一个简单但是并不友好的设计,因此在考虑并发的情况时,并不能简单粗暴的直接使用synchronized关键字。
那么到底要怎么做呢?请看下面的代码:
package com.pattern.single;
/**
*线程安全的单例模式
*/
public class SyncSingleton {
/**
*静态实例
*/
private static SyncSingleton syncSingleton;
/**
*私有构造方法
*/
private SyncSingleton(){}
/**
*双重加锁
* @return
*/
public static SyncSingleton getInstance(){
if(syncSingleton == null){
synchronized (SyncSingleton.class) {
if(syncSingleton == null){
syncSingleton = new SyncSingleton();
}
}
}
return syncSingleton;
}
}
上面的代码修改的只有getInstance方法,其他是不变得,针对修改后的方法需要解释的有两点:
第一,只在对象实例还未创建的时候进行同步,对象创建完成以后返回的过程就不需要同步了,这样就可以节省很多线程的等待时间,比上面的只是单纯的使用synchronized要节省很多线程等待时间;
第二,为什么要判断两次null,这个解释起来就有点麻烦了,假设现在有两个线程(A和B)同时进行了第一次判断为null,A线程拿到了锁,然后判断对象实例为空,然后创建了对象实例,此时A线程放开锁,B线程获取到了锁,此时如果不判断,那么B线程就会再次创建一个对象实例,这就会产生多个对象实例,因此需要在锁的内外进行两次判断Null;
看到这里,是不是觉得这个单例模式就完美了?你看又能满足单例,又能满足多线程,多完美啊,小兄弟啊,你还是太年轻啊,如果100分是满分的话,这个也就是80分了吧,为什么这么说呢?
其实也是很尴尬的事情,什么事情呢?就是我们经常说的虚拟机的事了,当然这里指的就是JVM,我们知道,我们的程序在虚拟机中都是一条一条的指令进行处理的,在JVM处理执行这些指令的时候,有时候会为了调优或者其他的原因改变执行指令的顺序,这TM就尴尬了,我们都知道,在创建一个对象的时候,一般是分成三步走,先是分配内存,然后初始化构造器,最后将对象指向分配的内存地址,一般情况下是正常的,但是你不能保证JVM在执行指令的时候不会把第三步和第二步互换一下,这时候就出现问题了。
此时对象的构造器还没有进行初始化,但是却已经把对象指向了分配的内存地址,也就是说虽然此时这个对象还没有完全初始化完成,但是外部却以为它初始化完成可以使用了,此时在其他线程进入锁内部判断对象实例是否为空时就会返回false,然后把未初始化完成的对象返回使用,这就会导致一些未知的错误,因此上面的单例也是不完美的。
那么怎么解决上面的问题呢?这就要用到一个关键字volatile,volatile关键字具体的作用大家可以自行百度一下,但是使用volatile关键字可以阻止虚拟机对执行的指令进行优化,也就是只要使用了volatile关键字,那么虚拟机就会完全按照上面的三步走战略执行,下面是修改过的程序:
package com.pattern.single;
/**
*线程安全的单例模式()
*/
public class SyncSingleton {
/**
*静态实例
*/
private static volatile SyncSingleton syncSingleton;
/**
*私有构造方法
*/
private SyncSingleton(){}
/**
*双重加锁
* @return
*/
public static SyncSingleton getInstance(){
if(syncSingleton == null){
synchronized (SyncSingleton.class) {
if(syncSingleton == null){
syncSingleton = new SyncSingleton();
}
}
}
return syncSingleton;
}
}
唯一改变的就是在静态实例前面加上了volatile关键字,阻止虚拟机进行指令重排,从而避免出现错误;
还是上面的程序,100分能打多少分呢,99分,还有一分是怕你骄傲,其实不然,还有一分是因为逼格不高,那么怎么才能保证程序正确的情况下还能有高逼格呢,那就得用内部类了,请看下面的程序:
package com.pattern.single;
/**
*内部类形式的单例
*/
public class InnerClassSingle {
/**
*私有构造方法
*/
private InnerClassSingle(){}
/**
*获取实例方法
* @return
*/
public static InnerClassSingle getInstance(){
return Singleton.innerClassSingle;
}
/**
* 静态内部类
*/
private static class Singleton{
static InnerClassSingle innerClassSingle = new InnerClassSingle();
}
}
前面我们花了半天的功夫才得到一段即满足单例又满足多线程环境使用的单例,那么上面的这段程序满足前面的这些要求吗?答案是满足,具体的且听我慢慢道来:
首先说单例的问题,我们都知道,静态变量只在类初始化的时候初始化一次,上面我们使用的是静态内部类,因此也就满足了单例的要求,只会初始化一次,不会出现多个实例;
其次,多线程的问题,还是上面说的,静态属性只在第一次初始化类的时候初始化一次,这是在JVM中进行的,并且JVM会强行同步这个过程,并不会出现初始化不完全的问题,因为在初始化的时候其他线程是无法使用的,这也就保证了并发问题;
综合以上两点,上面的程序是一个几乎完美的单例,保证了再应用中一个类只有一个实例,当然这是在不适用反射的情况下,如果使用反射那就另论了,其次,保证了并发情况下的正确使用,并且不会出现因为JVM的优化而出现未知错误的问题。
一个懒汉式就说了这么,还有个饿汉式是不是得另外开一篇文章了,这个嘛,就不用了,饿汉式其实就是下面一段程序:
package com.pattern.single;
public class HungrySingle {
private static HungrySingle hungrySingle = new HungrySingle();
private HungrySingle(){}
public static HungrySingle getInstance(){
return hungrySingle;
}
}
别看这个饿汉式简单,但是它可是即满足单例又满足线程安全的,但是缺点也很明显,就是类加载时就会初始化,浪费内存。
了解详情请登陆昆明达内IT培训官网(km.tedu.cn)!