戏说设计模式(三)单例模式
来源:互联网 发布:星型网络 编辑:程序博客网 时间:2024/05/16 18:19
0. 前言
找一个人惺惺相惜 找一颗心心心相印 在这个宇宙我是独一无二
上面这是一句歌词哈,来自梁静茹的《给未来的自己》,我很喜欢听这首歌。当然,今天我并不是要说音乐,而是讨论设计模式,上面的歌词中有句“在这个宇宙我是独一无二”,而“独一无二”就是今天主角。很明显,除开平行宇宙之类的东西,每个人在全宇宙都是独一无二的。那么,在Java的世界里,怎么让一个实例成为独一无二的呢。答:使用单例模式。下面我就介绍一下单例模式。
1. 单例模式
单例模式是很常见的一种设计模式,从名字可以看出来,是为了实现一个类只有一个实例。
1.1 定义
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
1.2 实现
要怎么实现一个类只有一个实例呢?其实有两种方式可以实现单例模式,分别是懒汉式和饿汉式。现在假设我有一个Singleton类要求只能有该类的一个实例。
1.2.1 懒汉式
1.首先,要想一个类只有一个实例,那么就得控制一下创建实例的权限,不能暴露出去给外部的类,只能自己用。那怎么才能让外部不能创建一个类的实例呢?很简单,把构造方法设为私有(private)的就好了。如
private Singleton(){}
2.那现在又有个问题,你把构造方法私有了,外部的类还怎么拿到你的实例?这时候就需要提供一个方法可以返回这个类的实例。如
public Singleton getInstance(){ return new Singleton();}
3.那么问题又来了,获取实例的方法getInstance()是只有实例才可以调用的,你一方面不给人家创建实例,一方面又要让人家用实例调用这个方法,不是玩人家嘛。所以,应该将上面那个方法加上一个static,就变成了这样
public static getInstance(){ return new Singleton();}
4.好了,获取实例的方法有了,但是还是有点问题,这样子每次调用获取实例的方法还是会创建一个新的实例。由于获取实例的方法是静态方法,所以我们可以用一个静态变量来保存一个唯一实例。
private static Singleton instance = null;
5.然后获取实例的方法就可以这样子写了
public static Singleton getInstance(){ if(instance == null){ //如果是第一次使用这个方法,instance为null,则创建一个实例 instance = new Singleton(); } return instance;}
那么,整个类的代码如下
public class Singleton { private static Singleton instance = null; private Singleton(){} public static Singleton getInstance(){ if(instance == null){ //如果是第一次使用这个方法,instance为null,则创建一个实例 instance = new Singleton(); } return instance; }}
1.2.2 饿汉式
饿汉式实现单例模式有两个地方与懒汉式不同,一个是静态变量,一个是获取实例方法。
1.静态变量instance,让类加载的时候初始化。
private static Singleton instance = new Singleton();
2.获取实例方法,不用再判断静态变量是否为null。
public static Singleton getInstance(){ return instance;}
整个类的代码如下
public class Singleton { private static Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ return instance; }}
1.3 两种实现方式的思考
- 懒汉式是典型的时间换空间。听名字就知道它很懒了,不到不得已的时候都不会干活的。懒汉式只有用到获取实例方法的时候,才会创建一个实例,但是每次都要判断一下有没有,没有才创建,有就直接返回。
- 饿汉式是典型的空间换时间。听名字就知道它很饿了,所以一加载类的时候赶紧创建一个实例,这样子每次调用实例方法的时候就不用再进行判断了。
2. 具体应用
我就用一个案例来实现单例模式吧。在Java工程中,经常会用到配置文件,无论是properties还是xml文件,都必须被读入到内存中,而且可能会有很多地方都会用到一个配置文件。那我现在假设有两个类,一个是A一个是B,都需要用到配置文件config.properties,并且我还有一个类Config,让配置文件的参数映射到类中,并且提供读取配置文件的方法。那么代码如下。
2.1 未使用单例模式
配置文件config.properties
param1=Aparam2=B
Config类
import java.io.InputStream;import java.util.Properties;public class Config { //存放配置文件中参数param1的值 private String param1; //存放配置文件中参数param2的值 private String param2; public String getParam1() { return param1; } public String getParam2() { return param2; } public Config(){ readProperties(); } /** * 读取config.properties文件,并将对应的参数填充到实例中 * @throws Exception */ private void readProperties(){ Properties prop = new Properties(); InputStream inStream = null; try{ //加载配置文件 inStream = Config.class.// getClassLoader().// getResourceAsStream("config.properties"); prop.load(inStream); //将配置文件对应的参数填充到实例中 this.param1 = prop.getProperty("param1"); this.param2 = prop.getProperty("param2"); }catch (Exception e) { e.printStackTrace(); }finally { try{ //关闭流 inStream.close(); }catch (Exception e) { e.printStackTrace(); } } }}
A类
public class A { public static void main(String[] args) { Config config = new Config(); String param1 = config.getParam1(); System.out.println(param1); }}
B类
public class B { public static void main(String[] args) { Config config = new Config(); String param2 = config.getParam2(); System.out.println(param2); }}
A类运行结果
A
B类运行结果
B
那么这里就有个问题,可能到时候我不至只有两个类要读取这个配置文件,万一有几百个类都要用到呢,那我每次就读一下,创建一个新的实例,而且这个配置文件又不会变的,这不是极大的浪费嘛!!!所以此时就需要单例模式登场了,让这个配置文件对应的实例只有一个,大家都用这一个,不也是很美妙的事情嘛。
2.2 使用单例模式
使用单例模式(懒汉式实现)后,Config类就变成下面这样子
import java.io.InputStream;import java.util.Properties;public class Config { private static Config instance = null; public static Config getInstance(){ if(instance == null){ instance = new Config(); } return instance; } //存放配置文件中参数param1的值 private String param1; //存放配置文件中参数param2的值 private String param2; public String getParam1() { return param1; } public String getParam2() { return param2; } //私有化构造参数 private Config(){ readProperties(); } /** * 读取config.properties文件,并将对应的参数填充到对象中 * @throws Exception */ private void readProperties(){ Properties prop = new Properties(); InputStream inStream = null; try{ //加载配置文件 inStream = Config.class.// getClassLoader().// getResourceAsStream("config.properties"); prop.load(inStream); //将配置文件对应的参数填充到对象中 this.param1 = prop.getProperty("param1"); this.param2 = prop.getProperty("param2"); }catch (Exception e) { e.printStackTrace(); }finally { try{ //关闭流 inStream.close(); }catch (Exception e) { e.printStackTrace(); } } }}
那么A类和B类调用就变成了这样
A类
public class A { public static void main(String[] args) { //将等号右边的new Config();变为Config.getInstance(); Config config = Config.getInstance(); String param1 = config.getParam1(); System.out.println(param1); }}
结果
A
B类
public class B { public static void main(String[] args) { // 将等号右边的new Config();变为Config.getInstance(); Config config = Config.getInstance(); String param2 = config.getParam2(); System.out.println(param2); }}
结果
B
这样子就用单例模式解决了一个具体应用了!等等,没那么简单,其实,这其中还有一个十分严重的问题,那就是线程安全问题。
3. 单例模式线程安全问题
这里只考虑跟单例模式有关的东西,不考虑类中的其它方法,那么单例模式的线程安全问题出自哪呢?出自获取实例的方法上。
3.1 饿汉式
饿汉式是线程安全的,因为在饿汉式中,类一加载便创建了一个实例,而在装载一个类的时候是不会发生并发的。
3.2 懒汉式
不加同步的懒汉式是线程不安全的,为什么这么说呢?假设我有两个线程,一个线程t1一个线程t2同时调用了getInstance()方法,就可能导致并发问题。
假设某一时刻程序运行情况是下图这样子的
然后程序继续运行,变成了下面这样子
很明显,当两个线程都执行完这个方法后,会有两个不同的Config实例,这样子单例模式就失效了。
3.3 解决方案
3.3.1 解决方案一
一旦涉及到线程安全问题,synchronized就会登场。可以把获取实例方法设为同步的,于是就变成下面这样子。
public static synchronized Singleton getInstance(){ if(instance == null){ instance = new Singleton(); } return instance;}
但是synchronized有个问题,就是性能问题,那我们可以优化一下,缩小同步范围。
3.3.2 解决方案二
使用“双重检查加锁”的方式实现,这样子既能实现线程安全,又使性能不会有太大影响。那么双重检查加锁是个什么东东呢?
所谓双重检查加锁,就是指并不是每次进入getInstance方法都需要同步,而是先进入方法,然后检查实例是否存在,不存在再进入同步块,这是第一重检查。进入同步块后再检查一下实例是否存在,这是第二重检查,不存在再真正地创建实例。
注意,双重检查加锁机制的实现会使用一个关键字volatile,它的意思是被volatile修饰的变量的值不会被本地线程缓存,所有对此变量的读写操作都是直接操作共享内存的。关于为什么要加这个关键字在贴出代码后再解释。想了解更多关于volatile关键字的知识,大家可以去这里看看http://blog.csdn.net/timheath/article/details/53352572
具体实现如下
public class Singleton { //注意,这里新加了一个volatile private volatile static Singleton instance = new Singleton(); private Singleton(){} public static Singleton getInstance(){ //先检查实例是否存在,不存在才进入同步块 if(instance == null){ //同步块 synchronized (Singleton.class) { //再次检查实例是否存在,如果不存在才真正地创建实例 if(instance == null){ instance = new Singleton(); } } } return instance; }}
3.3.3 解决方案三
上面的方案,要么性能不行,要么实现麻烦,那么有没有一种更优的方案呢?有!那就是Lazy initialization holder class模式,这个东西我之前有转载过,大家可以直接去这里看http://blog.csdn.net/timheath/article/details/53355073
4. 关于懒汉式的一点思考
4.1 延迟加载思想
其实懒汉式中,有一种延迟加载的思想。什么是延迟加载呢?就是一开始不要加载数据,一直等到要用到的时候才去加载,这样子够懒吧,躲不过了才去加载。延迟加载思想在很多地方都会用到,最常见的像Hibernate框架也有用到。
4.2 缓存思想
在懒汉式中,用一个字段去缓存一个实例。每次都先去找这个字段有没有指向一个实例,有就直接用,没有就创建一个并且将引用赋给这个字段,这样子下次再要用直接用就好了。像缓存思想用到的也很多,像基于内存的数据库Redis。
如果上面的内容有错误的地方或者讲的不好的地方,还请大家指点一下,我好及时修改。
- 戏说设计模式(三)单例模式
- 设计模式(三)单例模式
- 设计模式(三)单例模式
- 设计模式(三):单例模式
- 设计模式(三)单例模式
- 设计模式(三)单例模式
- 设计模式(三) 单例模式
- 设计模式(三):单例模式
- 设计模式(三)单例模式
- 设计模式三(单例设计模式)
- 戏说设计模式(一)外观模式
- 戏说设计模式(二)适配器模式
- Java单例设计模式(三)
- 戏说设计模式(转帖)
- 戏说设计模式
- 戏说设计模式
- 戏说GoF设计模式
- 设计模式笔记三:单例模式
- 安卓开发之添加按钮音效(使用SoundPool 添加音乐)
- Android 自定义View(基础)
- 11月份英语总结
- 第1章 游戏之乐 让cpu 占用率曲线听你的指挥
- 语法:Python中的引用和副本
- 戏说设计模式(三)单例模式
- 142. Linked List Cycle II
- 黑马4到9天
- POJ 1595 Prime Cuts 未完成
- 玲珑学院ACM比赛 Round #5 Aplus B
- Java Web J2EE下的两大框架SSH和SSM对比
- Longest Substring Without Repeating Characters 最长不重复子串【中】
- bellman ford 算法 判断是否存在负环
- Linux编程:进程间通信--消息队列