ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【设计模式】单例模式

【设计模式】单例模式 主要讲解单例模式和常用的几种实现方式基于java。前言我理解单例应该是所有模式中最简单也是使用最多的一种我就简单总结一下首先了解一下单例模式的定义。确保某一个类只有一个实例而且自行实例化并向整个系统提供这个实例。由单例的定义可以分析出实现一个单例有以下几个要点构造函数必须私有化防止外部调用构造函数进行实例提供静态函数获得该单例。单例主要有两种种实现方式懒汉模式和饿汉模式。懒汉模式在类加载时不创建实例因此类加载速度快但运行时获取对象的速度慢代码如下public class penguin { private static volatile penguin m_penguin null; // 避免通过new初始化对象 private void penguin() {} public void beating() { System.out.println(打豆豆); }; public static penguin getInstance() { if (null m_penguin) { synchronized(penguin.class) { if (null m_penguin) { m_penguin new penguin(); } } } return m_penguin; } }懒汉模式实现要点单例使用volatile修饰单例实例化时要用synchronized 进行同步处理双重null判断。下面模拟一个简单的单例并发测试可以使用CountDownLatch使用await()等待锁释放使用countDown()释放锁从而达到并发的效果可以见下面的代码public static void main(String args[]) { final CountDownLatch latch new CountDownLatch(1); int threadCount 20; for (int i 0; i threadCount; i) { new Thread() { Override public void run() { try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(penguin.getInstance().hashCode()); } }.start(); } latch.countDown(); }打印出来的hashCode完全一样证明单例模式生效输出如下1449937592 1449937592 1449937592 1449937592 1449937592 1449937592 1449937592 1449937592 1449937592 1449937592饿汉模式在类加载时就完成了初始化所以类加载较慢但获取对象的速度快代码如下:public class penguin { private static penguin m_penguin new penguin(); private void penguin() {} public static penguin getInstance() { return m_penguin; } }两种实现模式各有优缺点综合来说个人比较偏向于懒汉模式。适用场景单例模式只允许创建一个对象因此节省内存加快对象访问速度因此对象需要被公用的场合适合使用如多个模块使用同一个数据源连接对象等等。如需要频繁实例化然后销毁的对象。创建对象时耗时过多或者耗资源过多但又经常用到的对象。有状态的工具类对象。频繁访问数据库或文件的对象。优缺点优点1.在单例模式中活动的单例只有一个实例对单例类的所有实例化得到的都是相同的一个实例。这样就 防止其它对象对自己的实例化确保所有的对象都访问一个实例2.单例模式具有一定的伸缩性类自己来控制实例化进程类就在改变实例化进程上有相应的伸缩性。3.提供了对唯一实例的受控访问。4.由于在系统内存中只存在一个对象因此可以 节约系统资源当 需要频繁创建和销毁的对象时单例模式无疑可以提高系统的性能。5.避免对共享资源的多重占用。缺点1.不适用于变化的对象如果同一类型的对象总是要在不同的用例场景发生变化单例就会引起数据的错误不能保存彼此的状态。2.由于单利模式中没有抽象层因此单例类的扩展有很大的困难。3.单例类的职责过重在一定程度上违背了“单一职责原则”。4.滥用单例将带来一些负面问题如为了节省资源将数据库连接池对象设计为的单例类可能会导致共享连接池对象的程序过多而出现连接池溢出如果实例化的对象长时间不被利用系统会认为是垃圾而被回收这将导致对象状态的丢失。volatile问题大家有没有注意到单例模式中用到了关键字volatile在PHP和Go中没有类似的关键字但是JAVA必须加当初还有疑问我们先看一下volatile的作用volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制同步块或方法和 volatile 变量相比于synchronizedsynchronized通常称为重量级锁volatile更轻量级因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差有时它更简单并且开销更低而且其使用也更容易出错。我直接总结一下volatile的作用它会强制将对缓存的修改操作立即写入主存让所有的线程可见它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置也不会把前面的指令排到内存屏障的后面即在执行到内存屏障这句指令时在它前面的操作已经全部完成如果是写操作它会导致其他CPU中对应的缓存行无效。再回顾一下单例模式代码public class penguin { private static volatile penguin m_penguin null; // 避免通过new初始化对象 private void penguin() {} public void beating() { System.out.println(打豆豆); }; public static penguin getInstance() { //1 if (null m_penguin) { //2 synchronized(penguin.class) { //3 if (null m_penguin) { //4 m_penguin new penguin(); //5 } } } return m_penguin; //6 } }在并发情况下如果没有volatile关键字在第5行会出现问题。instance new TestInstance();可以分解为3行伪代码a. memory allocate() //分配内存 b. ctorInstanc(memory) //初始化对象 c. instance memory //设置instance指向刚分配的地址上面的代码在编译运行时可能会出现重排序从a-b-c排序为a-c-b。在多线程的情况下会出现以下问题。当线程A在执行第5行代码时B线程进来执行到第2行代码。假设此时A执行的过程中发生了指令重排序即先执行了a和c没有执行b。那么由于A线程执行了c导致instance指向了一段地址所以B线程判断instance不为null会直接跳到第6行并返回一个未初始化的对象。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进