建型模式的第一种模式:单例模式
扫描二维码
随时随地手机看文章
在上一篇中,介绍完了学习设计模式的预备知识,现在开始,便正式学习23种设计模式了。
根据《设计模式的艺术------软件开发人员内功修养之道》一书作者刘伟老师的观点,对于软件开发人员来说,c、c++、java语言、开发环境等等都是一些招式,而内功则是数据结构、算法、设计模式、软件工程等等。由此可见,想成为高手,学习设计模式(也就是深厚的内功)是必不可少的。
经典的23种设计模式可分为三类:创建型模式,结构型模式,行为型模式。
本文就介绍创建型模式的第一种模式:单例模式,由此作为23种设计模式的开始。套用作者刘伟老师的一句话,“从它开始为大家逐一展现设计模式的魅力”。哈哈
单例模式可以用于系统中只需要创建一个对象的类,比如windows下的任务管理器。单例模式可以确保对象的唯一性。
单例模式的定义如下:确保某个类只有一个实例,而且自行实例化并向系统提供这个实例,这个类成为单例类,它提供全局访问的方法。
以下是一个典型的单例模式的例子。
class TaskManager{ private static TaskManager tm; private TaskManager(){ } public static TaskManager getInstance(){ if(tm == null) tm = new TaskManager(); return tm; } }
以下是单例模式的一个应用。
/* * 负载均衡器是一个单例类,用于处理并发请求时将请求分派给各个服务器 * */ class LoadBalancer{ private static LoadBalancer lb; private Vectorserverlist; private LoadBalancer(){ serverlist = new Vector(); } public static LoadBalancer getLoadBalancer(){ if(lb == null){ lb = new LoadBalancer(); } return lb; } /* * 获取一个服务器用于处理请求 * */ public String getServer(){ return serverlist.get(new Random().nextInt(serverlist.size())); } /* * 添加一个服务器 * */ public void addServer(String server){ serverlist.add(server); } public void removeServer(String server){ serverlist.remove(server); } }
注释讲的还算清楚吧。接下来是这个单例类的测试代码:
public static void main(String[] args) { // TODO Auto-generated method stub LoadBalancer lb1 = LoadBalancer.getLoadBalancer(); LoadBalancer lb2 = LoadBalancer.getLoadBalancer(); System.out.println("lb1==lb2?"+(lb1==lb2));//测试取得的是否为同一个对象 lb1.addServer("server1"); lb2.addServer("server2"); lb1.addServer("server3"); lb2.addServer("server4"); for(int i = 0;i < 20;i++){ String server = lb1.getServer(); System.out.println("请求分发至:"+server); } } /*输出: * lb1==lb2?true 请求分发至:server1 请求分发至:server3 请求分发至:server1 请求分发至:server2 请求分发至:server4 请求分发至:server3 请求分发至:server3 请求分发至:server2 请求分发至:server2 请求分发至:server3 请求分发至:server2 请求分发至:server4 请求分发至:server1 请求分发至:server4 请求分发至:server4 请求分发至:server3 请求分发至:server1 请求分发至:server3 请求分发至:server2 请求分发至:server3 * */
其实,以上的代码还是存在问题的,如:在多线程的情况下,当A线程调用getLoadBalancer()时,执行到new LoadBalancer()时由于创建工作比较耗时导致B线程对lb静态变量做出判断时仍然为null,此后A和B线程便都创建了一个LoadBalancer对象。此时,JVM中就有了两个LoadBalancer对象,那么LoadBalancer也就不在是单例类了。
那么,为了防止多线程的同步,可以使用synchronized关键字,getLoadBalancer()代码改为:
public static synchronized LoadBalancer getLoadBalancer(){ if(lb == null){ lb = new LoadBalancer(); } return lb; }
这样又引发新的问题了,在多线程的环境下,每次调用该方法都要进行线程锁定判断,实在白费功夫。因此可以再改:
public static synchronized LoadBalancer getLoadBalancerImprove(){ if(lb == null){ synchronized(LoadBalancer.class){ if(lb == null) lb = new LoadBalancer(); } } return lb; }
需注意,这里要进行两次的null判断,道理和上面的同步差不多。另外,由于两次的null判断,所以需要将lb静态变量加个volatile关键字,阻止编译器代码优化。这样的设计叫做双重检查锁定。
以上讨论有一个共同点,那就是单例对象的创建是在需要使用时才创建的,也就是懒汉式单例了,或者说饱汉式单例。相对应的,则是饿汉式单例了:
class LoadBalancer{ private static LoadBalancer lb = new LoadBalancer(); private Vectorserverlist; private LoadBalancer(){ serverlist = new Vector(); } public static LoadBalancer getLoadBalancer(){ if(lb == null){ lb = new LoadBalancer(); } return lb; } /* * 获取一个服务器用于处理请求 * */ public String getServer(){ return serverlist.get(new Random().nextInt(serverlist.size())); } /* * 添加一个服务器 * */ public void addServer(String server){ serverlist.add(server); } public void removeServer(String server){ serverlist.remove(server); } }
这样的设计,使得单例类在被加载时就创建一个单例对象,这样虽然增加了加载的时间,但是也省去了同步的工作。相比于饿汉式单例类,饱汉式单例类是使用时才加载并创建对象,这样的技术称为延迟加载技术。关于类加载的详细细节可以看看JVM知识或其他相关的知识,会有一个比较清晰的认识。
以上两种方式实现单例模式各有优缺点,一是同步,一是加载时间。那么有没有方式可以既解决同步的问题,又可以在加载时不创建对象,直到调用getLoadBalancer()时才创建呢?是的,有的。这就是IoDH(Initialization on Demand Holder)技术了,且看代码:
class LoadBalancer{ private static LoadBalancer lb; private Vectorserverlist; private static class HolderClass{ private final static LoadBalancer instance = new LoadBalancer(); } private LoadBalancer(){ serverlist = new Vector(); } public static LoadBalancer getLoadBalancer(){ return HolderClass.instance; } /* * 获取一个服务器用于处理请求 * */ public String getServer(){ return serverlist.get(new Random().nextInt(serverlist.size())); } /* * 添加一个服务器 * */ public void addServer(String server){ serverlist.add(server); } public void removeServer(String server){ serverlist.remove(server); } }
巧妙地使用了静态内部类,使得单例类被加载时并没有创建对象,而调用getLoadBalancer()也不用做同步工作。
IoDH技术也是有缺陷的,它需要语言的支持。啊,缺陷真是无穷无尽,从头到尾都没有一个解决方案是完美的。可见,世间之事,难有十全十美。
讨论了这么多,也该对单例模式做一个思考与总结了。
首先,需要注意的是,单例到底是在哪个范围内只有一个对象。想了想,对于java,我的回答是,在JVM中单例类只有一个实例。由于构造方法是私有的,所以反射是完不成单例对象的创建了。另外,类加载器的双亲委托机制使得即使用不同的类加载器来加载单例类,最终也是只加载一次该单例类,而不会多次加载。