纠结了好久,要不要写一篇博客记录自己学习单例模式的过程。网上相关博客多的很,好像没什么必要重复造一个老轮子。
但是最近面试、看面试书,发现单例模式还是经常会被考到的,而且作为设计模式中相对来说比较简单的一种,掌握好还是很有必要的。
而掌握知识的最好途径不是看别人的文章,而是自己亲手造一个,想必那样体会更深。
好了废话不多说。
单例模式的作用从名字上就可以看出来:保证某个类的实例只能被创建一次,以后都是调用这个实例。
常见的用到单例模式的情况有:
1.windows下的任务管理器,只允许创建一个实例,否则同时打开多个任务管理器,这个里结束了某个进程,另外一个管理器中还可以看到,不是很安全;
2.Android中的Application一般情况下都是只有一个,需要在唯一的Application中创建一些成员变量;
3.总之,单例模式就是为了保证某个状态的一致性。
明白了他的重要性后,就开始实现吧。
SingletonPattern有多种实现方式,网上最多的有七八种。不由得让我想到茴香豆的七种写法。同样的专注,不同的是每一种写法都是一种优化。
今天我要实现的是相对简单的,容易理解的三种:
1.最简单的“懒汉模式”:
package net.sxkeji.singnleton;
/**
* 按照四人团的说法: <br/>
* Singleton模式的工作方式是:保证一个类仅有一个实例,并提供一个访问它的全局访问点<br/>
* 懒汉单例模式适用于单线程情况下
*/
public class Singleton1 {
private static Singleton1 instance = null;
private String name;
/**
* 为了确保对象实例的唯一性,将构造函数定义为私有的、或者保护的
*/
private Singleton1(){
//一些操作..
}
private Singleton1(String str){
name = str;
}
/**
* 最简单的实现 <br/>
* 缺点:线程不安全,当多线程同时访问时可能会导致创建多个实例
* @param str
* @return Singleton1实例
*/
public static Singleton1 getInstance(String str){
if(instance == null){
instance = new Singleton1(str);
}
return instance;
}
public void showName(){
System.out.println(name);
}
}
构造
函数的访问权限设置为private,是为了在类外部不能通过new一个类来创建实例。想要得到该类的对象只能通过getInstance方法。第一次创建实例时将instance设置为非空,以后就直接返回这个实例了。
测试方法:
public static void testSingleton1(){
Singleton1 first = Singleton1.getInstance("熊大");
Singleton1 second = Singleton1.getInstance("熊二");
first.showName();
second.showName();
}
运行结果:
可以看到在单线程下以后不管传入什么参数,我们得到的都是首次创建时的实例。
但是在多线程下,两个线程可能“同时”要创建这个类的对象,这时instance都是null,就会创建2个实例,这就是懒汉模式的弊端--线程不安全。
测试方法:
/**
* 多线程情况下懒汉实现导致的问题
*/
public static void multiThreadSingleton1(){
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Singleton1 second = Singleton1.getInstance("熊二");
second.showName();
}
});
thread.start();
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Singleton1 second = Singleton1.getInstance("熊大");
second.showName();
}
});
thread2.start();
}
测试结果:
可以看到创建出了2个实例,这不是我们想要的结果。
2.懒汉模式的优化---加锁控制的单例模式(江湖人称double-checked-locking)
为了避免在多线程下创建多个实例的情况出现,我们需要进行锁控制。
有一种方法是直接在懒汉模式的基础上,对getInstance方法前加个synchronized。这样每次只允许一个线程访问,保证了线程安全。
但是导致了一个新问题:如果有多线程同时访问,其他线程都需要等到前一个访问的线程对该方法的操作结束后才可以进行操作,2等1,3等2,4等3,得等到猴年马月。时间上效率太低。
所以需要进一步优化:当该类并没有创建实例时才进行锁控制,如果已经创建过实例,以后直接调用实例就好了,用不着再等待。
代码如下:
/**
* double-checked-locking模式 <br/>
* 将非必须的锁定优化掉,避免因为同步锁导致的大量等待时间<br/>
* 支持多线程环境
*/
public class Singleton2 {
private static Singleton2 instance = null;
private String name;
/**
* 为了确保对象实例的唯一性,将构造函数定义为私有的、或者保护的
*/
private Singleton2(){
//一些操作..
}
private Singleton2(String str){
name = str;
}
/**
* 2个 if 提高效率
*/
public static Singleton2 getInstance(String str){
//第一个检查是否为空是为了在除首次创建时直接返回实例,避免等待加锁浪费时间
if(instance == null){
//同步锁避免【多线程同时创建多个实例】
doSync(str);
}
return instance;
}
/**
* 加了锁的实例化操作
* @param str
*/
public synchronized static void doSync(String str){
if(instance == null)
instance = new Singleton2(str);
}
public void showName(){
System.out.println(name);
}
}
运行结果:
咦?虽然保证了实例的唯一性,但是为什么会出现2种结果呢?
这是因为该模式只能保证实例的唯一性,并没有控制创建实例的先后顺序。由于我测试方法是在两个线程里,他俩创建实例的先后顺序不确定,可能“熊大”的先创建,也可能“熊二”的先创建。
正常情况下我们是在主线程里对类对象的初始化创建,其他线程访问我们创建好的。这样上面的先后问题就不存在。
如果需要在多线程里访问,就需要在类方法创建时初始化,而不是调用时才初始化。由此引出了第三种,饿汉式单例。
3.饿汉式单例,根据static预先加载的特性
在实现这种单例时我发现自己对static的理解还不够。
被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的所有实例共享。只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象。
这种方式基于classloder机制避免了多线程的同步问题.在类加载的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的。
代码如下:
**
*饿汉式单例,根据static预先加载的特性 <br/>
*饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的
*/
public class Singleton3 {
<span style="color:#ff0000;">private static final Singleton3 instance = new Singleton3("熊妹");</span>
private String name;
/**
* 为了确保对象实例的唯一性,将构造函数定义为私有的、或者保护的
*/
private Singleton3(){
//一些操作..
}
private Singleton3(String str){
name = str;
}
public static Singleton3 getInstance(String str){
return instance;
}
public void showName(){
System.out.println(name);
}
}
注意到我红色标注的地方同时用static和final修饰了对象instance,他俩有什么作用呢?
对于被static和final修饰过的实例常量,实例本身不能再改变了。也就是以后都不允许修改,保证了一致性。
测试用例和上面多线程访问的那个方法一样。
运行结果:
可以看到在类加载时就创建对象并初始化,以后得到的都是这唯一的实例。
不过我觉得有个缺点,不能从外部初始化,都是写死的。
但是看了很多博客,大牛们都推荐这种方法,看来还是相对来说算是性能比较好的。
总结:
菜鸟还是重复造轮子学得多,好记性不如敲键盘,况且在自己实现的过程中还可以发现其他的问题,比如我对static的不理解,多线程的不熟悉等等。总之是收获很多。下次再实现别的模式,这一阶段还是赶紧把Java基础和算法夯实了。