一、单例模式简介
在应用系统开发中,我们常常有以下需求:
- 在多个线程之间,比如servlet环境,共享同一个资源或者操作同一个对象
- 在整个程序空间使用全局变量,共享资源
- 大规模系统中,为了性能的考虑,需要节省对象的创建时间等等。
因为Singleton模式可以保证为一个类只生成唯一的实例对象,所以这些情况,Singleton模式就派上用场了。
二、单例模式的结构
三、单例模式的具体实现
假设有一个巧克力工厂,但是工厂只有一个巧克力熔化炉,所以,我们在创建工厂的时候,只能有一个实例,此时我们需要用到单利设计模式。
1、饱汉式
所谓饱汉式,就是在需要的时候才进行创建实例对象。
首先我们进行ChocolateFactoryC类设计,这是经典的单例设计模式:
// An highlighted block
package design.single.gys.classic;
public class ChocolateFactoryC {
private boolean full;
private boolean boiled;
private static ChocolateFactoryC singlefactory;
private ChocolateFactoryC() {};
public static ChocolateFactoryC getInstance() {
if(singlefactory==null) {
for(int i=0;i<100000;i++)
singlefactory=new ChocolateFactoryC() ;
}
return singlefactory;
}
public void fill()
{
if(!full)
System.out.println("加入巧克力");
else
System.out.println("已加满巧克力");
full=true;
}
public void boil()
{
if(!boiled)
System.out.println("加热巧克力");
else
System.out.println("已加热巧克力");
boiled=true;
}
public void finish()
{
System.out.println("倒出巧克力");
full=false;
boiled=false;
}
}
在单线程的程序运行过程中,似乎没有什么问题.测试程序如下:
// An highlighted block
package design.single.gys.classic;
public class Test1 {
public static void main(String[] args) {
// TODO Auto-generated method stub
ChocolateFactoryC cf1=ChocolateFactoryC.getInstance();
ChocolateFactoryC cf2=ChocolateFactoryC.getInstance();
cf1.fill();
cf2.fill();
cf1.boil();
cf2.boil();
System.out.println(cf1==cf2);
System.out.println(cf1);
System.out.println(cf2);
}
}
测试结果1:
// An highlighted block
加入巧克力
已加满巧克力
加热巧克力
已加热巧克力
true
design.single.gys.classic.ChocolateFactoryC@10dea4e
design.single.gys.classic.ChocolateFactoryC@10dea4e
可以看到,cf1和cf2是同一个引用,地址相同,且在运行巧克力工厂的时候不会重复加入巧克力重复加热,巧克力工厂就能很好的运行了,喜欢巧克力的人们也就很开心了。
然而,问题来了。如果是多线程条件下,巧克力工厂还能很好的运行吗?下面我们用一个多线程的测试程序来测试这个工厂:
// An highlighted block
package design.single.gys.classic;
import java.util.HashSet;
public class Test implements Runnable{
private static HashSet<ChocolateFactoryC> hs=new HashSet<ChocolateFactoryC>();
@Override
public void run() {
// TODO Auto-generated method stub
ChocolateFactoryC cf=ChocolateFactoryC.getInstance();
synchronized(Test.class) {
hs.add(cf);
}
}
public static void test()
{
for(int i=0;i<1000;i++)
new Thread(new Test()).start();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
test();
synchronized(Test.class) {
System.out.println("集合大小:"+Test.hs.size());
for(ChocolateFactoryC c: hs)
System.out.println(c);
}
}
}
在这个程序中,我们创建了1000个线程,每个线程都想去创建这个工厂,但是工厂只有一个啊,我们用一个HashSet 来保存所有线程得到的工厂,最后输出集中保存了多少个不一样的工厂。
测试结果2:
// An highlighted block
集合大小:3
design.single.gys.classic.ChocolateFactoryC@19758f
design.single.gys.classic.ChocolateFactoryC@16d60d5
design.single.gys.classic.ChocolateFactoryC@38e772
集中保存了三个不一样的工厂,当然每次运行的结果都会不一样。这下糟了,明明只有一个工厂,这是却多出了两个不应该存在工厂,程序变得混乱的,工厂没法正常工作,巧克力爱好者们不开心了,大闹了起来。
看来,经典单例设计模式不太符合我们的要求。为了满足巧克力爱好者们,我们不得不进行改进。
2、饿汉式
由于在饱汉式中我们在需要工厂时才进行实例化,但是在多线程中下面这行代码会出现明显的问题,当一个线程执行到if语句中的循环时(为了效果明显,特地增加了循环),该线程随时可能被剥夺执行权,此时的判断语句输出位true,转而下一个线程进行执行,同样判断语句输出也是true,这里就是问题所在,此时将创建两个工厂实体对象。
// An highlighted block
if(singlefactory==null) {
//此处之后线程随时可能会被剥夺执行权
for(int i=0;i<100000;i++)
//此处之前线程随时可能会被剥夺执行权
singlefactory=new ChocolateFactoryC() ;
}
我们对其进行改进,将饱汉式改为饿汉式,我们直接创建工厂实例,在需要的时候,我们调用静态的getInstance()方法获得工厂对象。
代码如下:
// An highlighted block
package design.single.gys.improve;
public class ChocolateFactoryD {
private static ChocolateFactoryD singlefactory=new ChocolateFactoryD();
private ChocolateFactoryD() {};
public static ChocolateFactoryD getInstance() {
return singlefactory;
}
}
测试代码:
// An highlighted block
package design.single.gys.improve;
import java.util.LinkedHashSet;
public class Test implements Runnable{
private static LinkedHashSet<ChocolateFactoryD> hs=new LinkedHashSet<ChocolateFactoryD>();
@Override
public void run() {
// TODO Auto-generated method stub
ChocolateFactoryD cf=ChocolateFactoryD.getInstance();
synchronized(Test.class) {
hs.add(cf);
}
}
public static void test()
{
for(int i=0;i<1000;i++)
new Thread(new Test()).start();
}
public static void main(String[] args) {
// TODO Auto-generated method stub
test();
System.out.println("集合大小:"+hs.size());
for (ChocolateFactoryD c: hs)
System.out.println(c);
}
}
测试结果:
// An highlighted block
集合大小:1
design.single.gys.improve.ChocolateFactoryD@19758f
此时返回的对象必定是同一个工厂了,那么巧克力爱好者么又开始欢呼了。
有一天巧克力工厂宣布要关闭,为什么呢?工厂24小时在运作,成本太高,已经远远超出巧克力爱好者们的消费需求。也就是说,我们创建的的工厂,即使我们不需要,也会一直存在内存中,占用内存。还没高兴多久的人们又开始躁动了。
没办法,我们继续改进。
3、改进的单例模式
在饱汉式中,因为getInstance()方法运行到一半时,可能会被剥夺执行权,那么我们使用同步方法进行改进。
// An highlighted block
package design.single.gys.synch;
public class ChocolateFactory1 {
private static ChocolateFactory1 singlefactory=null;
private ChocolateFactory1() {};
public static synchronized ChocolateFactory1 getInstance() {
if(singlefactory==null)
{singlefactory=new ChocolateFactory1();}
return singlefactory;
}
}
使用上面相同的测试代码,测试结果如下:
// An highlighted block
集合大小:1
design.single.gys.synch.ChocolateFactory2@38e772
如我们所愿,只出现了一个工厂。是否这就满足了我们的要求呢?为什么工厂的运作速度这么慢?因为对整个方法使用synchronized同步,对系统的开销太大。
好,继续改进:
// An highlighted block
package design.single.gys.synch;
public class ChocolateFactory2 {
private static ChocolateFactory2 singlefactory=null;
private ChocolateFactory2() {};
public static ChocolateFactory2 getInstance() {
if(singlefactory==null) {
synchronized(ChocolateFactory2.class) {
if(singlefactory==null)
{singlefactory=new ChocolateFactory2();}
}
}
return singlefactory;
}
}
在这里的代码中,我们不对整个方法进行同步,我们对方法内进行双重检查加锁机制,仍然使用if进行判断,接着使用synchronized同步,进行原子操作,里面再次进行if判断,是否已经存在工厂的实例。
// An highlighted block
集合大小:1
design.single.gys.synch.ChocolateFactory2@19758f
emmmm,现在我们的巧克力工厂能很好的运行了,巧克力爱好者们欢呼雀跃。