分享总结常见的5种单例模式:
第一、单例模式的使用场景
A、Windows的任务管理器、回收站、文件系统如F盘,都是很典型的单例模式 ;
B、项目中,读取配置文件的类,一般也是单例模式,没有必要每次读取都重新new一个对象加载
C、数据库的连接池也是单例模式,因为数据库链接是一种数据库资源
D、在Spring中,每个bean默认都是单例的,可以很方便的交给Spring来管理
E、在servlet编程中,每个Servlet也是单例模式
还有像360浏览器每次打开都是新的客户端,就不是单例模式;
第二、单例模式的核心是什么?
一个类 只有 一个对象实例;
提供一个访问接口。
// 其实,单例模式,说白了,就是控制一个对象的实例个数
//比方说,你可以实现 一个类只能存在5个实例个数,超过5个后,就不允许创建,这也是可以实现的。
第三、常见的5种单例模式?
1、饿汉式(线程安全,调用效率高,非延时加载)
2、懒汉式(线程安全,调用效率不高,延时加载)
3、双重检测锁式(此种方式不建议使用)
4、静态内部类式(线程安全,调用效率高,延时加载)
5、枚举单例式(线程安全,调用效率高,非延时加载, 可以防止反射,反序列化漏洞)
第四、具体介绍
4.1 饿汉式
4.1.1 优点:
多线程安全
4.1.2 问题
非延迟加载。
如果加载后,以后都没有使用的话,就白加载了,而且加载过程有可能很耗时。
4.1.3 代码:
package com.xingej.patterns.singleton.example1; /** * 单例模式:饿汉式 * * @author erjun 2017年11月7日 下午7:38:40 */ public class HungryMethod { // static 变量会在类装载时初始化,不会涉及到多线程问题。 // 虚拟机保证只会装载一次,肯定不会发生并发访问的问题。 // 因此,可以省略synchronized关键字的 private static HungryMethod instance = new HungryMethod(); private HungryMethod() { } // 问题:如果只是加载了本类,而没有调用getInstance(),甚至永远没有调用,则会造成资源浪费 public static HungryMethod getInstance() { return instance; } }
4.2 懒汉式
4.2.1 优点:
多线程安全、延迟加载、资源利用率提高了
4.2.2 问题
并发效率低
由于:每次调用getInstance()方法,都会同步
4.2.3 代码:
package com.xingej.patterns.singleton.example2; /** * 懒汉式;延迟加载; * * @author erjun 2017年11月7日 下午7:54:33 */ public class LazyMethod { private static LazyMethod instance; private LazyMethod() { } // 方法同步,调用效率低 public static synchronized LazyMethod getInstacne() { if (null == instance) { // 只有使用时,才调用 instance = new LazyMethod(); } return instance; } }
4.3 双重检测锁式
4.3.1 优点:
延迟加载、资源利用率提高了(第一次同步,以后不再同步)
4.3.2 问题
由于编译器优化原因,JVM底层内部模型原因,偶尔会出问题,
因此,不建议使用
4.3.3 代码:(注意:还有另外一种写法,就是添加了两个同步模块,但是同样不建议使用)
package com.xingej.patterns.singleton.example3; /** * 双重检测实现单例模式 //-------不建议使用------此方法哦 * * @author erjun 2017年11月7日 下午8:08:43 */ public class DoubleCheck { private static DoubleCheck instance; private DoubleCheck() { } // 提高了效率,只是在第一次需要同步;以后就不需要同步了 // 但是 // 由于编译器优化原因和JVM底层内部模型元素,偶然会出问题。 // -------不建议使用------ public static DoubleCheck getInstance() { if (null == instance) { synchronized (DoubleCheck.class) { if (null == instance) { return instance = new DoubleCheck(); } } } return instance; } }
4.4 静态内部类
4.4.1 优点:
多线程安全、延迟加载
4.4.2 代码:
package com.xingej.patterns.singleton.example4; /** * 静态内部类方式创建单例模式 * * 第一、当你初始化SingletonDemo01的时候,并不会立即加载静态内部类的。 外部类没有static属性,则不会像饿汉式那样立即加载对象。 * 第二、只有真正调用getiInstance(),才会加载静态内部类。 * 第三、加载类时是线程安全的, instance的类型是static final, * 从而保证了内存中只有这样一个实例存在,而且只能被赋值一次, 从而保证了线程安全性 具有高并发和延迟加载的优点 * * @author erjun 2017年11月8日 上午11:38:31 */ public class StaticInnerClass { // 静态内部类,并不会立即加载的,只有调用时才加载的 private static class SingletonClassInstance { private static final StaticInnerClass instance = new StaticInnerClass(); } private StaticInnerClass() { } public static StaticInnerClass getInstance() { return SingletonClassInstance.instance; } }
4.5 枚举单例模式
4.5.1 优点:
实现简单、线程安全
枚举类本身就是单例模式,由JVM从根本上提供保障,避免通过反射和反序列化的漏洞!
4.5.2 缺点
无延迟加载
4.5.3 代码
package com.xingej.patterns.singleton.example5; /** * 枚举的方式 实现 单例模式 * * 《优点》: 1、实现简单;2、枚举类本身就是单例模式,由JVM从根本上提供保障。避免通过反射和反序列化的漏洞进行创建实例对象 。《缺点》:无延迟加载 * * * @author erjun 2017年11月8日 上午11:53:40 */ public enum EnumMethod { INSTANCE; // 枚举类可以有自己的行为模式的 //下面这个方法,可以没有的; public void show() { System.out.println("----我是枚举类型的单例模式哦------"); } // 测试用例 public static void main(String[] args) { EnumMethod aDemo01 = EnumMethod.INSTANCE; EnumMethod bDemo01 = EnumMethod.INSTANCE; // 校验是否是同一个对象 System.out.println(aDemo01 == bDemo01); } }
第五、选择何种的单例模式呢?
场景一:占用资源少,不需要延时加载:
枚举类 好于 饿汉式
场景二:占用资源大,需要延时加载
静态内部类 好于 懒汉式
第六、多线程方式测试5种单例模式的效率
package com.xingej.patterns.singleton; import java.util.concurrent.CountDownLatch; import org.junit.Test; import com.xingej.patterns.singleton.example1.HungryMethod; import com.xingej.patterns.singleton.example2.LazyMethod; import com.xingej.patterns.singleton.example3.DoubleCheck; import com.xingej.patterns.singleton.example4.StaticInnerClass; import com.xingej.patterns.singleton.example5.EnumMethod; /** * 多线程方式来测试5种单例模式的效率 * * @author erjun 2017年11月8日 下午4:07:14 */ public class MulThread { private static final int threadNum = 10; private static final int count = 10 * 10000; // 调用单例 对象 10万次 private static CountDownLatch countDownLatch = new CountDownLatch(threadNum); // 饿汉式 效率测试 @Test public void testEHanShi() throws Exception { long startTime = System.currentTimeMillis(); for (int i = 0; i < threadNum; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < count; j++) { HungryMethod.getInstance(); } countDownLatch.countDown(); } }).start(); } // 其实,所有的这些阻塞,内部其实都有一个死循环去监控的 countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println("----饿汉式---执行时间---:\t" + (endTime - startTime) + " 毫秒"); } // 懒汉式 效率测试 @Test public void testLanHanShi() throws Exception { long startTime = System.currentTimeMillis(); for (int i = 0; i < threadNum; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < count; j++) { LazyMethod.getInstacne(); } countDownLatch.countDown(); } }).start(); } // 其实,所有的这些阻塞,内部其实都有一个死循环去监控的 countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println("----懒汉式---执行时间---:\t" + (endTime - startTime) + " 毫秒"); } // 双重检测 效率测试 @Test public void test2Check() throws Exception { long startTime = System.currentTimeMillis(); for (int i = 0; i < threadNum; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < count; j++) { DoubleCheck.getInstance(); } countDownLatch.countDown(); } }).start(); } // 其实,所有的这些阻塞,内部其实都有一个死循环去监控的 countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println("----双重检测---执行时间---:\t" + (endTime - startTime) + " 毫秒"); } // 静态内部类 单例模式 效率测试 @Test public void testStaticInnerClass() throws Exception { long startTime = System.currentTimeMillis(); for (int i = 0; i < threadNum; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < count; j++) { StaticInnerClass.getInstance(); } countDownLatch.countDown(); } }).start(); } // 其实,所有的这些阻塞,内部其实都有一个死循环去监控的 countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println("----静态内部类方式---执行时间---:\t" + (endTime - startTime) + " 毫秒"); } // 枚举类方式的单例模式 效率测试 @Test public void testEnumMethod() throws Exception { long startTime = System.currentTimeMillis(); for (int i = 0; i < threadNum; i++) { new Thread(new Runnable() { @Override public void run() { for (int j = 0; j < count; j++) { // -------------------------------注意------------------------------- // ----枚举类,必须有接收值,就是等式左边这些Object object, 不然有问题哦 Object object = EnumMethod.INSTANCE; } countDownLatch.countDown(); } }).start(); } // 其实,所有的这些阻塞,内部其实都有一个死循环去监控的 countDownLatch.await(); long endTime = System.currentTimeMillis(); System.out.println("----枚举类方式---执行时间---:\t" + (endTime - startTime) + " 毫秒"); } }
第七、实际案例:使用单例模式,读取配置文件
代码:
package com.xingej.patterns.singleton.example6; import java.io.IOException; import java.io.InputStream; import java.util.Properties; public class SingleMode { private SingleMode() { // 创建对象时,会默认加载配置文件 readConfig(); } private static class InnerSingle { private static SingleMode instance = new SingleMode(); } public static SingleMode getInstance() { return InnerSingle.instance; } // ---------------------上面是单例模式---------------------------- private Properties properties; private InputStream inputStream = null; private void readConfig() { properties = new Properties(); try { // 注意,配置文件的路径 inputStream = SingleMode.class.getResourceAsStream("/single.properties"); properties.load(inputStream); } catch (IOException e) { e.printStackTrace(); } finally { if (null != inputStream) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } // 提供对外的接口, // 查询配置文件的属性信息 public String getProperties(String key) { return properties.getProperty(key); } }
测试:
package com.xingej.patterns.singleton.example6; import org.junit.Test; public class SingleModeTest { // 通过单例方式,来读取配置文件 @Test public void testReadConfigBySingleMothod() { SingleMode instance = SingleMode.getInstance(); String name = instance.getProperties("name"); System.out.println("---name:\t" + name); } }
总结:
设计模式一定抛开代码,重点在思想
想想现实生活中,哪些是单例模式形式存在的
-------------------------------------------------------------------------------------------
-------------------------------------------------------------------------------------------
下面这些内容,
可以不用关心,
一般情况下,不会用到的;
如果想扩展知识,可以看一下。
上面提到的5种单例模式中,除了枚举单例模式外,其他4种单例模式都有bug的,
也就是说,可以通过
反射的方式
或者
反序列化的方式
创建两个以上的对象,从而打破了单例模式只有一个对象的限定。
第一、反射的方式 破解单例模式的限定
添加测试用例,选择懒汉式单例模式进行测试
如何反破解呢?
第二、反序列化的方式 破解单例模式的限定
使用反序列化方式的话,
前提条件时,单例模式必须实现Serializable接口,不然不能序列化的。
测试用例:
如何反破解呢?
只需要在单例模式内,添加一个私有方法readResolve()即可。
具体代码如下:
package com.xingej.patterns.singleton.example4; import java.io.Serializable; /** * 测试 通过 反序列化的方式,来破解单例模式 * * @author erjun 2017年11月8日 下午3:41:29 */ public class StaticInnerClass2 implements Serializable { private static final long serialVersionUID = 1L; // 静态内部类,并不会立即加载的,只有调用时才加载的 private static class SingletonClassInstance { private static final StaticInnerClass2 instance = new StaticInnerClass2(); } private StaticInnerClass2() { } public static StaticInnerClass2 getInstance() { return SingletonClassInstance.instance; } // 反序列化时,自动调用这个方法的 // 直接返回对象,不需要再单独创建新的对象了 // 使用下面的方式,就可以 // 阻止,通过序列化的方式来破解单例模式 private Object readResolve() { return SingletonClassInstance.instance; } }
代码已上传到git上:
https://github.com/xej520/xingej-design-patterns
本文出自 “XEJ分布式工作室” 博客,请务必保留此出处http://xingej.blog.51cto.com/7912529/1980184
原文地址:http://xingej.blog.51cto.com/7912529/1980184