清明小长假,因为没有回老家探亲,趁着可贵的三天假期,能够好好地丰富下本身的知识储备。今天是第一天,上午花了半天时间看了下单例模式,正好解决了最近手头自动化测试工做中碰到的困扰,也顺便了解了下volatile关键字的使用。html
也许有人会说,网上关于设计模式的文章不少,为何还要写设计模式。可是,那毕竟是人家的,没有通过本身的理解、实践、总结、沉淀,是很难化为己用的。至于我写博客的目的,更不是为了博得他人的关注和承认,主要是为了将本身学习过的知识能加深理解,吸取前人的优秀经验和巧妙设计思想,在本身平日的工做中看有没有能够借鉴的地方。固然,若是能有经验丰富的人看了个人博客,不论是在学习工做方式上仍是知识内容上给我些许诚恳的提点和意见,本人将感激涕零。我的博客园地址:http://www.cnblogs.com/znicy/java
另外,随着知识的积累,不少知识在一段时间不接触后会遗忘,写博客的一大好处就是随时能够找到以前曾经接触的这一片区域,而且还能够抓到当时写博时的思路,很快地回忆起知识的内容。web
开始介绍单例模式以前,必需要先描述下使用场景,以及本身在代码编写时遇到的痛点。数据库
在不少时候,有些对象咱们但愿在整个程序只有一个实例,如线程池、数据库链接池、缓存、日志对象、注册表等。而最近,在个人实际工做中,在编写接口自动化代码时就遇到了下列两种场景:设计模式
借用单例模式或借鉴其思想就能够解决上述问题。api
单例模式确保一个类只有一个实例,并提供一个全局访问点。缓存
public class Singleton{ private static Singleton uniqueInstance; private Singleton(){} public static Singleton getInstance(){ if (null==uniqueInstance){ uniqueInstance = new Singleton(); } return uniqueInstance; } }
Singleton类拥有一个静态变量uniqueInstance来记录Singleton的惟一实例,注意它的构造函数是private的,这就注定了只有Sinleton类内才可使用该构造器。在其余类中咱们没法经过new Singleton()的方式类获取一个Singleton的实例,只能经过Singleton.getInstance()的方式获取。而且因为uniqueInstance是一个静态变量,属于Singleton这个类,因此保证了其惟一性。安全
经典模式有个好处,就是它的对象的实例化只有等到getInstance方法被调用时才会被jvm加载,若是getInstance始终没有被调用,jvm就不会生成该实例。若是该对象的实例化须要消耗较多的资源,这种“延迟实例化”的方式能够减少jvm的开销。session
可是,上述的实现方式很容易会想到存在一个严重的缺陷,就是“非线程安全”。当多个线程同时调用Singleton.getInstance()来获取实例时,uniqueInstance对象就可能被屡次实例化。最简单的方式就是经过synchronized关键字来实现线程同步:多线程
public static synchronized Singleton getInstance(){ if (null==uniqueInstance){ uniqueInstance = new Singleton(); } return uniqueInstance; }
在经典单例模式中加入了synchronized关键字后,咱们能够发现整个getInstance方法是线程同步的操做,当一个线程在调用该方法时,其余全部线程都会被阻塞。若是getInstance方法的执行时间开销很小,那么咱们是可使用这种方式的。可是,若是getInstanc方法的执行时间开销很大,就会极大地下降并发效率。在这种状况下,能够考虑将实例化的操做提早到Singleton类加载的时候,即“急切实例化”方式:
public class Singleton{ private static Singleton uniqueInstance= new Singleton(); private Singleton(){} public static Singleton getInstance(){ return uniqueInstance; } }
利用这种方式,咱们能够依赖jvm在加载这个类时立刻建立此惟一的单例,jvm会保证在任何线程访问uniqueInstance静态变量以前,必定先建立此实例。
综合上述两种方式,为了平衡实例建立开销和并发量受限的代价,“双重检查加锁”经过部分同步的方式同时解决了二者的问题。
public class Singleton{ private volatile static Singleton uniqueInstance; private Singleton(){} public Singleton getInstance(){ if (null == uniqueInstance){ synchronized (Singleton.class){ if( null == uniqueInstance){ uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
能够看到,这种方式也是将实例化延迟到了getInstance方法被调用时,区别于经典单例模式,该方式引入了“双重检查”,在多线程并行执行到同步代码块时,会再次判断uniqueInsance是否为null,有效防止了屡次实例化的发生。而且这种方式并无对整个getInstance方法加锁,只是在第一次生成Singleton的惟一实例时进行了一次同步,并无下降程序的并发性。
而对于volatile关键字的使用,查阅了《Thinking in Java》,做者的解释是“volatile定义的域在发生修改后,它会直接写到主存中,对其余任务可见”。
用volatile修饰的变量,线程在每次开始使用变量的时候,都会读取变量修改后的最新的值。可是这并不表明,使用volatile就能够实现线程同步,它只是在线程“开始使用”该变量时读取到该变量的最新值。当线程访问某一个对象时候值的时候,首先经过对象的引用找到对应在堆内存(主存)的变量的值,而后把堆内存变量的具体值load到线程本地内存(本地缓存)中,创建一个变量副本,以后线程就再也不和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完以后的某一个时刻(线程退出以前),自动把线程变量副本的值回写到对象在堆中变量。下面这幅流程图描述了一个共享变量在线程中被使用时其线程工做内存与主内存的交互方式。
图片转自博客:God is Coder
最后再介绍一下静态内部类的方式也能够实现同时知足性能和并发要求的单例模式。
public class Singleton{ private static class Holder{ private static Singleton INSTANCE = new Singleton(); } private Singleton(){} public static final Singleton getInstance(){ return Holder.INSTANCE; } }
能够看到,该方式实际上是第二种“急切实例化”方式的变种,该实例只有在jvm加载类Holder时会被实例化,而且能够保证在各个线程获取Holder.INSTANCE变量以前完成。在保证线程安全的同时,又能够延迟实例化,而且没有下降并发性。
在介绍了几种单例模式后,如今来解决咱们在“使用场景”中碰到的两个问题。
使用“静态内部类”方法建立SessionFactory类:
public class SessionFactory { private static String sessionId; private static BaseConfig baseConfig = BaseConfigFactory.getInstance(); private static class SessionidHolder{ private final static SessionFactory INSTANCE = new SessionFactory(); } public static final SessionFactory getInstance(){ return SessionidHolder.INSTANCE; } private SessionFactory(){ LoginApi api = new LoginApi(); String username = baseConfig.getLoginUsername(); String password = baseConfig.getLoginPassword(); sessionId= api.login(username, password).getValue("session.id"); } public String getSessionId() { return sessionId; } }
使用Testng编写测试代码:
public class SessionTest { @Test(threadPoolSize=10, invocationCount=10) public void sessionTest(){ SessionFactory sessionFactory = SessionFactory.getInstance(); System.out.println("Thread id="+ Thread.currentThread().getId()+ ", session.id=" + sessionFactory.getSessionId()); } }
测试结果:
Thread id=13, session.id=36afe1a1-19df-4400-8fbf-4687293d7294 Thread id=18, session.id=36afe1a1-19df-4400-8fbf-4687293d7294 Thread id=11, session.id=36afe1a1-19df-4400-8fbf-4687293d7294 Thread id=16, session.id=36afe1a1-19df-4400-8fbf-4687293d7294 Thread id=12, session.id=36afe1a1-19df-4400-8fbf-4687293d7294 Thread id=17, session.id=36afe1a1-19df-4400-8fbf-4687293d7294 Thread id=10, session.id=36afe1a1-19df-4400-8fbf-4687293d7294 Thread id=15, session.id=36afe1a1-19df-4400-8fbf-4687293d7294 Thread id=14, session.id=36afe1a1-19df-4400-8fbf-4687293d7294 Thread id=19, session.id=36afe1a1-19df-4400-8fbf-4687293d7294
能够看到,10个线程并发执行时,session.id是惟一的,说明sessionFactory是惟一的,只被实例化了一次。
或许你会问,能不能在SessionFactory中将getSessionId方法设为静态方法,直接调用SessionFactory.getSessionId()来获取sessionId?固然能够,可是前提是你仍是必须要先经过调用SessionFactory.getInstance()方法来将SessionFactory类实例化,不然你会发现获取到的sessionId就是null,能够看出,jvm在加载一个类时,若是该类没有被实例化就不会去主动调用它的构造方法。
借用“双重检查,部分同步”的思想,能够设计伪代码逻辑以下(篇幅考虑使用伪代码代替):
try { sendHttpsRequest(); }catch(ConnectException e){ numRquestFail++; synchronized (BaseApi.class) { if (isWebHostChanged()){ return; } switchWebHost(); } }
即,将切换webhost部分的代码进行同步,而且在切换时先经过调用isWebHostChanged()方法判断是否已经被其余线程切换。防止host屡次发生切换。同时,这种方式不会影响到sendHttpsRequest方法的并发。
其实,写到这里,从早上开始拿起手头的《Head First 设计模式》看单例模式,到翻书查资料理解相关的知识(volatile、jvm内存管理) 到重构自动化的代码,到反复测试各类条件下的程序执行状况,到写完整篇总结,已经花了一成天的时间,虽然说花的时间有点多,可是知识的扫盲自己就不是一蹴而就的,尤为基础的东西理解地深入我相信对之后的学习确定是有帮助的。