分享总结常见的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