一、致谢
首先还是要感谢实验室的各位亲们对我的支持和鼓励,我一定会好好的继续把设计模式的专题系列做下去,让每位想要学会的设计模式的童鞋近距离的接触它,让每位找工作前的同学在面试前都能大致了解设计模式并能运用到自己的项目中去,让已经在公司的童鞋也能经常回顾这些模式的用法增加对它们的认知,总之我还是相信天道酬勤的,大家一起加油。
二、单例模式需求介绍
我们还是以Tank大战的游戏为例吧,游戏中有玩家自己控制的Tank,也有很多敌方的Tank,对于玩家独有的Tank游戏中应该只保持一个,可是我们在写游戏代码的过程中可能会在多处想用该Tank对象的引用,很多同学会说这还不简单吗,在游戏中我将该对象的引用在代码各个部分中传递(可以通过普通的传参,也可以通过全局的容器,或者其它方式都无所谓)不就可以了吗?当然可以,不过这种方式比较繁琐,造成代码比较混乱,可读性也比较差,更可怕的是如果整个项目由多人完成,在工程的各个部分项目组成员不容易控制该Tank类的对象只有一份,所以我们很自然的想要一种通用的获取该对象的方法,而且每次取到的都是该Tank对象的引用,而实现这种方法的正是单例模式(顾名思义)。
三、线程不安全的单例模式
为了实现这种单例模式,我们可以分析以下三点:
1、为了以统一的方式产生对象,我们需要一个工厂生产该Tank对象,不是通过new,而是通过getInstance()方法;
2、为了保持该对象在系统中只产生一份,该对象类的构造方法不应该是public的,既然构造方法是私有的,那该对象只能由该对象所属的类产生,即它所属的类就是生产它的工厂;
3、生产出来的对象应该保存在该类中,用于每次作为getInstance的返回
综上所述,下面我们给出生产该Tank对象的单例模式对应的代码:
public class Tank {
private static Tank t;
private Tank(){
}
public static Tank getInstance(){
if(t == null){
t = new Tank();
}
return t;
}
}
采用这种机制在单线程运行环境下没有任何问题,设想在游戏中若有多个线程同时调用Tank.getInstance()方法,就存在t被new出来两次的情形,因为当第一个线程执行过if(t == null)后,CPU时间片到时导致了线程的切换问题,此时线程二也运行该段程序,t被new出来一次然后被返回到主程序中,过了一段时间后第一个线程终于被重新调度,然而它直接从t=new Tank()开始,对t又重新new了一次然后返回到主程序中,这次返回的对象和上次返回的显然不是同一个对象。
四、线程安全但低效的单例模式
为了实现线程安全,我们很自然的想到java里提供的线程同步机制,一种懒惰的做法是用synchronized方法实现对象的生产
public class Tank {
private static Tank t;
private Tank(){
}
public synchronized static Tank getInstance(){
if(t == null){
t = new Tank();
}
return t;
}
}
静态synchronized方法的执行需要获取Tank.Class的锁,所以多个线程只能同时有一个线程执行Tank.getInstance()方法,这就保证了不会有多个线程同时判断t==null的情况出现,自然不会生产出两个不同的Tank对象。低效的原因是每次执行Tank.getInstance()都需要获取Tank.Class的锁,若其它线程正在运行这段代码,则本线程会挂起等待其它线程释放Tank.Class的锁。
五、线程安全且高效的单例模式
为了提高上面单例模式的效率,我们需要将synchronized关键字挪到getInstance()里面去,为了保证t = new Tank()只被多个线程执行一次,这里我们就要用到一些设计的技巧。单一的用synchronized修饰t = new Tank()并不能达到目的,因为两个线程仍然可以同时判断t==null,一种比较好的做法是我们可以在判断t为null后,在synchronized代码块中继续判断一次t是否为null,若为null则创建Tank,否则不创建。这样我们就能保证多个线程只能进入第二次判断t==null一次,其它已经第一次判断t==null的线程第二次判空时一定会失败,具体代码如下:public class Tank {
private static Tank t;
private Tank(){
}
public static Tank getInstance(){
if(t == null){
//第一次判null,多个线程可能同时进入
synchronized(Tank.class){ //保证了上面第一次判null成功的线程依次访问下面的代码
if(t == null){ //第一个访问的线程判null成功,其它的再次访问时一定失败
//这就保证了t = new Tank()只被执行一次
t = new Tank();
}
}
}
return t;
}
}
上面的代码可能技巧性比较强,需要大家仔细的体会理解,对于有些同学而言对synchronized关键字的用法还不是很理解,需要再对多线程程序设计的部分再充充电。
六、饥渴加载的单例模式
对于上面提到的双次判null的技巧有的同学可能会提出质疑,因为他们觉着对于实现线程安全的单例模式根本用不着那么麻烦,下面贴出他们可能的想法对应的代码:
public class Tank {
private static Tank t = new Tank();
private Tank(){
}
public static Tank getInstance(){
return t;
}
}
这中做法是利用静态成员的java虚拟机加载机制,t 成员在jvm加载该Tank类的时候就执行了Tank对象 t 的初始化,以后不管多少个线程调用Tank.getInstance()都会返回这个t对象,这样不是很好吗?对于简单的Tank对象来说这种方法是可行的,而对于较大较复杂的对象我们总是想实现对它的延迟加载(即第一次调用该对象的时候才在内存中生成该对象),站在这样的角度考虑问题,还是第5种方法比较完美。
和第六种写法有同样效果的另一中写法是:
public class Tank {
private static Tank t ;
static{
t = new Tank();
}
private Tank(){
}
public static Tank getInstance(){
return t;
}
}
七、总结
对于面试中或者实际的项目中我们给出第5节的形式是最perfect的,它既是线程安全的,又是高效的,同时又是延迟加载的。