设计模式之单例模式

来源:互联网 发布:知乎 恐怖 编辑:程序博客网 时间:2024/05/29 03:22


根据《设计模式的艺术------软件开发人员内功修养之道》一书作者刘伟老师的观点,对于软件开发人员来说,c、c++、java语言、开发环境等等都是一些招式,而内功则是数据结构、算法、设计模式、软件工程等等。由此可见,想成为高手,学习设计模式(也就是深厚的内功)是必不可少的。

经典的23种设计模式可分为三类:创建型模式,结构型模式,行为型模式。

本文就介绍创建型模式的第一种模式:单例模式,由此作为23种设计模式的开始。套用作者刘伟老师的一句话,“从它开始为大家逐一展现设计模式的魅力”。哈哈

单例模式可以用于系统中只需要创建一个对象的类,比如windows下的任务管理器。单例模式可以确保对象的唯一性。

单例模式的定义如下:确保某个类只有一个实例,而且自行实例化并向系统提供这个实例,这个类成为单例类,它提供全局访问的方法。

以下是一个典型的单例模式的例子。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. class TaskManager{  
  2.     private static TaskManager tm;  
  3.       
  4.     private TaskManager(){  
  5.           
  6.     }  
  7.       
  8.     public static TaskManager getInstance(){  
  9.         if(tm == null)  
  10.             tm = new TaskManager();  
  11.         return tm;  
  12.     }  
  13. }  

以下是单例模式的一个应用。

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. /* 
  2.  * 负载均衡器是一个单例类,用于处理并发请求时将请求分派给各个服务器 
  3.  * */  
  4. class LoadBalancer{  
  5.     private static LoadBalancer lb;  
  6.     private Vector<String> serverlist;  
  7.       
  8.     private LoadBalancer(){  
  9.         serverlist = new Vector<String>();  
  10.     }  
  11.       
  12.     public static LoadBalancer getLoadBalancer(){  
  13.         if(lb == null){  
  14.             lb = new LoadBalancer();  
  15.         }  
  16.         return lb;  
  17.     }  
  18.       
  19.     /* 
  20.      * 获取一个服务器用于处理请求 
  21.      * */  
  22.     public String getServer(){  
  23.         return serverlist.get(new Random().nextInt(serverlist.size()));  
  24.     }  
  25.       
  26.     /* 
  27.      * 添加一个服务器 
  28.      * */  
  29.     public void addServer(String server){  
  30.         serverlist.add(server);  
  31.     }  
  32.       
  33.     public void removeServer(String server){  
  34.         serverlist.remove(server);  
  35.     }  
  36. }  

注释讲的还算清楚吧。接下来是这个单例类的测试代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public static void main(String[] args) {  
  2.         // TODO Auto-generated method stub  
  3.         LoadBalancer lb1 = LoadBalancer.getLoadBalancer();  
  4.         LoadBalancer lb2 = LoadBalancer.getLoadBalancer();  
  5.           
  6.         System.out.println("lb1==lb2?"+(lb1==lb2));//测试取得的是否为同一个对象  
  7.           
  8.         lb1.addServer("server1");  
  9.         lb2.addServer("server2");  
  10.         lb1.addServer("server3");  
  11.         lb2.addServer("server4");  
  12.           
  13.         for(int i = 0;i < 20;i++){  
  14.             String server = lb1.getServer();  
  15.             System.out.println("请求分发至:"+server);  
  16.         }  
  17.     }  
  18.     /*<span style="white-space:pre">    </span>输出: 
  19.      *  <span style="white-space:pre">  </span>lb1==lb2?true 
  20.         请求分发至:server1 
  21.         请求分发至:server3 
  22.         请求分发至:server1 
  23.         请求分发至:server2 
  24.         请求分发至:server4 
  25.         请求分发至:server3 
  26.         请求分发至:server3 
  27.         请求分发至:server2 
  28.         请求分发至:server2 
  29.         请求分发至:server3 
  30.         请求分发至:server2 
  31.         请求分发至:server4 
  32.         请求分发至:server1 
  33.         请求分发至:server4 
  34.         请求分发至:server4 
  35.         请求分发至:server3 
  36.         请求分发至:server1 
  37.         请求分发至:server3 
  38.         请求分发至:server2 
  39.         请求分发至:server3 
  40.      * */  

其实,以上的代码还是存在问题的,如:在多线程的情况下,当A线程调用getLoadBalancer()时,执行到new LoadBalancer()时由于创建工作比较耗时导致B线程对lb静态变量做出判断时仍然为null,此后A和B线程便都创建了一个LoadBalancer对象。此时,JVM中就有了两个LoadBalancer对象,那么LoadBalancer也就不在是单例类了。

那么,为了防止多线程的同步,可以使用synchronized关键字,getLoadBalancer()代码改为:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public static synchronized LoadBalancer getLoadBalancer(){  
  2.         if(lb == null){  
  3.             lb = new LoadBalancer();  
  4.         }  
  5.         return lb;  
  6.     }  


这样又引发新的问题了,在多线程的环境下,每次调用该方法都要进行线程锁定判断,实在白费功夫。因此可以再改:
[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. public static synchronized LoadBalancer getLoadBalancerImprove(){  
  2.         if(lb == null){  
  3.             synchronized(LoadBalancer.class){  
  4.                 if(lb == null)  
  5.                     lb = new LoadBalancer();  
  6.             }  
  7.         }  
  8.         return lb;  
  9.     }  

需注意,这里要进行两次的null判断,道理和上面的同步差不多。另外,由于两次的null判断,所以需要将lb静态变量加个volatile关键字,阻止编译器代码优化。这样的设计叫做双重检查锁定。

以上讨论有一个共同点,那就是单例对象的创建是在需要使用时才创建的,也就是懒汉式单例了,或者说饱汉式单例。相对应的,则是饿汉式单例了:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. class LoadBalancer{  
  2.     private static LoadBalancer lb = new LoadBalancer();  
  3.     private Vector<String> serverlist;  
  4.       
  5.     private LoadBalancer(){  
  6.         serverlist = new Vector<String>();  
  7.     }  
  8.       
  9.     public static LoadBalancer getLoadBalancer(){  
  10.         if(lb == null){           
  11.             lb = new LoadBalancer();              
  12.         }  
  13.         return lb;  
  14.     }  
  15.       
  16.     /* 
  17.      * 获取一个服务器用于处理请求 
  18.      * */  
  19.     public String getServer(){  
  20.         return serverlist.get(new Random().nextInt(serverlist.size()));  
  21.     }  
  22.       
  23.     /* 
  24.      * 添加一个服务器 
  25.      * */  
  26.     public void addServer(String server){  
  27.         serverlist.add(server);  
  28.     }  
  29.       
  30.     public void removeServer(String server){  
  31.         serverlist.remove(server);  
  32.     }  
  33. }  

这样的设计,使得单例类在被加载时就创建一个单例对象,这样虽然增加了加载的时间,但是也省去了同步的工作。相比于饿汉式单例类,饱汉式单例类是使用时才加载并创建对象,这样的技术称为延迟加载技术。关于类加载的详细细节可以看看JVM知识或其他相关的知识,会有一个比较清晰的认识。

以上两种方式实现单例模式各有优缺点,一是同步,一是加载时间。那么有没有方式可以既解决同步的问题,又可以在加载时不创建对象,直到调用getLoadBalancer()时才创建呢?是的,有的。这就是IoDH(Initialization on Demand Holder)技术了,且看代码:

[java] view plaincopy在CODE上查看代码片派生到我的代码片
  1. class LoadBalancer{  
  2.     private static LoadBalancer lb;  
  3.     private Vector<String> serverlist;  
  4.       
  5.     private static class HolderClass{  
  6.         private final static  LoadBalancer instance = new LoadBalancer();  
  7.     }  
  8.       
  9.     private LoadBalancer(){  
  10.         serverlist = new Vector<String>();  
  11.     }  
  12.     public static synchronized LoadBalancer getLoadBalancer(){  
  13.         return HolderClass.instance;  
  14.     }  
  15.       
  16.     /* 
  17.      * 获取一个服务器用于处理请求 
  18.      * */  
  19.     public String getServer(){  
  20.         return serverlist.get(new Random().nextInt(serverlist.size()));  
  21.     }  
  22.       
  23.     /* 
  24.      * 添加一个服务器 
  25.      * */  
  26.     public void addServer(String server){  
  27.         serverlist.add(server);  
  28.     }  
  29.       
  30.     public void removeServer(String server){  
  31.         serverlist.remove(server);  
  32.     }  
  33. }  
巧妙地使用了静态内部类,使得单例类被加载时并没有创建对象,而调用getLoadBalancer()也不用做同步工作。

IoDH技术也是有缺陷的,它需要语言的支持。啊,缺陷真是无穷无尽,从头到尾都没有一个解决方案是完美的。可见,世间之事,难有十全十美。


讨论了这么多,也该对单例模式做一个思考与总结了。

首先,需要注意的是,单例到底是在哪个范围内只有一个对象。想了想,对于java,我的回答是,在JVM中单例类只有一个实例。由于构造方法是私有的,所以反射是完不成单例对象的创建了。另外,类加载器的双亲委托机制使得即使用不同的类加载器来加载单例类,最终也是只加载一次该单例类,而不会多次加载。

0 0
原创粉丝点击