单例模式详解
2016-04-13 00:14
309 查看
一直以为自己对单例模式很了解,仔细研究才发现以前忽略了单例模式的很多难点。其中有如何保证在多线程中的单例模式。如何保证反序列化时的单例模式(因为反序列化时会创建一个新的实例)。
定义:在整个程序中,保证某个类只有一个实例化的对象,并自行对外提供这个实例。
使用场景:在定义中说的很清楚了,只要一个实例的时候。比如IO读写时,数据库操作时。多个对象的话就很有可能造成并发读写的操作,也增加了不必要的资源消耗。
恶汉单例模式
懒汉单例模式
上面两种就是我以前使用的单例模式。两者的区别在于恶汉在类一声明的时候就对唯一的对象进行类初始化。而懒汉是在第一次调用getInstance()时进行初始化操作。
之前运行的很好,直到我遇见了多线程。以懒汉为例,在多线程的操作中,假设A线程已经判断出对象为null但还没有来的及进行实例化操作时,进入了B线程。这时候对象仍然为null。于是B线程对对象进行了一次实例化。实例化完成后又回到了A线程,这时会对对象再次进行实例化。导致对一个对象进行多次实例化操作。这就违背了单例模式的定义。
保证在多线程中的单例模式
为此,我需要在多线程时对单例模式进行同步操作。使用synchronized关键字。
然而,这样会产生一个新的问题。就是在第一次调用getInstance()方法后。这时对象已经不为null。就是说已经不再需要同步这个方法了。但实际是,调用端在以后的每次调用getInstance()。都会对这个方法进行同步。这就违背了我们多线程的初衷。这时候,我们可以在getInstance()方法里多加一个判断。
package com.example.singleinstance;
public class LazySingle {
private static LazySingle mLazySingle = null;
private LazySingle(){}
}
这样做的好处就是,只会在第一次调用getInstance()方法时进行同步操作。以后再调用getInstance()都是多线程的异步操作了。
似乎这样做就完美解决了多线程的问题。然而并没有。在特定的情况下,这个程序仍然可能违背单例模式的定义。在说明这个问题前,我先说一下关于java实例化对象时的一个小知识。
实例化对象分为三个步骤:
为对象分配一个内存空间。
初始化这个对象,即初始化这个对象的成员。
将这个对象指向之前我们分配的内存空间。(这时候这个对象就不是null了)
很重要的一点是,java编译器是支持乱序执行的。就是说,java在实例化这个对象时的顺序并不是1.2.3.很有可能是1.3.2。现在我们假设是后者。当A线程执行完第一步和第三步时跑到了B线程。这时候这个对象已经不是null了。B线程不对对象进行实例化操作,而是直接调用这个对象里的成员变量或成员函数。这时候程序就会报错了。
为此,SUN公司在jdk1.5之后加上了volatile关键字。
在声明这个对象的时候,加上volatile关键字修饰,就不会出现上述问题了。至于为什么,就自己百度吧。
推荐的单例模式,静态内部类单例模式。
说了那么多,然而被推荐的是现在说的这种单例模式。
当第一次加载这个类时,并不会初始化唯一的对象,只有在调用getInstance()方法时会导致虚拟机加载内部静态类SingletonHolder。不仅能保证线程的安全,也保证了单例对象的唯一性。推荐使用。
如何保证反序列化时也是单例模式。
序列化就是读取时的操作。比如读取到一个磁盘上。序列化就是将对象存储到磁盘上的过程,反序列化就是从磁盘上将存储的对象取出来。注意这时候取的并不是原来的对象。而是和原来一模一样的一个新的对象。产生了新的对象,就不能保证对象的唯一性了。好在序列化给我们提供了一个很特别的函数readResolv()。通过这个函数,我们可以控制反序列化时返回的对象。
通过上面的讲解,发现简单的单例模式原来那么复杂。有没有一种简单有效的方法。答案是肯定的,那就是通过枚举
2. 枚举单例模式(简单有效)
枚举的好处是默认线程同步的,而且也不会受到反序列化的影响,而且最重要的是,在任何一种情况下它都是单例的。
使用枚举单例
是不是超简单。
总结:经过研究发现简单的一个单例模式如果用的不好,是很有可能发生错误的。java编程“博大精深”,只有不断的学习和积累,使自己的知识更加深入和广泛,才能从容的面对各种项目上的需求,寻找出最简单高效的解决方案。
定义:在整个程序中,保证某个类只有一个实例化的对象,并自行对外提供这个实例。
使用场景:在定义中说的很清楚了,只要一个实例的时候。比如IO读写时,数据库操作时。多个对象的话就很有可能造成并发读写的操作,也增加了不必要的资源消耗。
恶汉单例模式
package com.example.singleinstance; public class HungerSingle { private static HungerSingle mHungerSingle = new HungerSingle(); private HungerSingle(){} public static HungerSingle getInstance() { return mHungerSingle; } }
懒汉单例模式
package com.example.singleinstance; public class LazySingle { private static LazySingle mLazySingle = null; private LazySingle(){} public static LazySingle getInstance() { if (mLazySingle == null) { mLazySingle = new LazySingle(); } return mLazySingle; } }
上面两种就是我以前使用的单例模式。两者的区别在于恶汉在类一声明的时候就对唯一的对象进行类初始化。而懒汉是在第一次调用getInstance()时进行初始化操作。
之前运行的很好,直到我遇见了多线程。以懒汉为例,在多线程的操作中,假设A线程已经判断出对象为null但还没有来的及进行实例化操作时,进入了B线程。这时候对象仍然为null。于是B线程对对象进行了一次实例化。实例化完成后又回到了A线程,这时会对对象再次进行实例化。导致对一个对象进行多次实例化操作。这就违背了单例模式的定义。
保证在多线程中的单例模式
为此,我需要在多线程时对单例模式进行同步操作。使用synchronized关键字。
package com.example.singleinstance; public class LazySingle { private static LazySingle mLazySingle = null; private LazySingle(){} public static synchronized LazySingle getInstance() { if (mLazySingle == null) { mLazySingle = new LazySingle(); } return mLazySingle; } }
然而,这样会产生一个新的问题。就是在第一次调用getInstance()方法后。这时对象已经不为null。就是说已经不再需要同步这个方法了。但实际是,调用端在以后的每次调用getInstance()。都会对这个方法进行同步。这就违背了我们多线程的初衷。这时候,我们可以在getInstance()方法里多加一个判断。
package com.example.singleinstance;
public class LazySingle {
private static LazySingle mLazySingle = null;
private LazySingle(){}
public static LazySingle getInstance() { if (mLazySingle == null) { synchronized (LazySingle.class) { if (mLazySingle == null) { mLazySingle = new LazySingle(); } } } return mLazySingle; }
}
这样做的好处就是,只会在第一次调用getInstance()方法时进行同步操作。以后再调用getInstance()都是多线程的异步操作了。
似乎这样做就完美解决了多线程的问题。然而并没有。在特定的情况下,这个程序仍然可能违背单例模式的定义。在说明这个问题前,我先说一下关于java实例化对象时的一个小知识。
实例化对象分为三个步骤:
为对象分配一个内存空间。
初始化这个对象,即初始化这个对象的成员。
将这个对象指向之前我们分配的内存空间。(这时候这个对象就不是null了)
很重要的一点是,java编译器是支持乱序执行的。就是说,java在实例化这个对象时的顺序并不是1.2.3.很有可能是1.3.2。现在我们假设是后者。当A线程执行完第一步和第三步时跑到了B线程。这时候这个对象已经不是null了。B线程不对对象进行实例化操作,而是直接调用这个对象里的成员变量或成员函数。这时候程序就会报错了。
为此,SUN公司在jdk1.5之后加上了volatile关键字。
package com.example.singleinstance;
public class LazySingle {
private static volatile LazySingle mLazySingle = null;
private LazySingle(){}
public static LazySingle getInstance() { if (mLazySingle == null) { synchronized (LazySingle.class) { if (mLazySingle == null) { mLazySingle = new LazySingle(); } } } return mLazySingle; }
}
在声明这个对象的时候,加上volatile关键字修饰,就不会出现上述问题了。至于为什么,就自己百度吧。
推荐的单例模式,静态内部类单例模式。
说了那么多,然而被推荐的是现在说的这种单例模式。
package com.example.singleinstance; public class Singleton { private Singleton() { } public static Singleton getInstance() { return SingletonHolder.SINGLETON; } /** * 内部静态类 * * @author cyq */ private static class SingletonHolder { private static final Singleton SINGLETON = new Singleton(); } }
当第一次加载这个类时,并不会初始化唯一的对象,只有在调用getInstance()方法时会导致虚拟机加载内部静态类SingletonHolder。不仅能保证线程的安全,也保证了单例对象的唯一性。推荐使用。
如何保证反序列化时也是单例模式。
序列化就是读取时的操作。比如读取到一个磁盘上。序列化就是将对象存储到磁盘上的过程,反序列化就是从磁盘上将存储的对象取出来。注意这时候取的并不是原来的对象。而是和原来一模一样的一个新的对象。产生了新的对象,就不能保证对象的唯一性了。好在序列化给我们提供了一个很特别的函数readResolv()。通过这个函数,我们可以控制反序列化时返回的对象。
package com.example.singleinstance; public class Singleton { private Singleton() { } public static Singleton getInstance() { return SingletonHolder.SINGLETON; } /** * 内部静态类 * * @author cyq */ private static class SingletonHolder { private static final Singleton SINGLETON = new Singleton(); } private Object readResolv() { return SingletonHolder.SINGLETON; } }
通过上面的讲解,发现简单的单例模式原来那么复杂。有没有一种简单有效的方法。答案是肯定的,那就是通过枚举
2. 枚举单例模式(简单有效)
枚举的好处是默认线程同步的,而且也不会受到反序列化的影响,而且最重要的是,在任何一种情况下它都是单例的。
package com.example.singleinstance; public enum SingletonEnum { INSTANCE; public void doSomething() { System.out.println("do sth"); } }
使用枚举单例
package com.example.singleinstance; import android.app.Activity; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); SingletonEnum.INSTANCE.doSomething(); } }
是不是超简单。
总结:经过研究发现简单的一个单例模式如果用的不好,是很有可能发生错误的。java编程“博大精深”,只有不断的学习和积累,使自己的知识更加深入和广泛,才能从容的面对各种项目上的需求,寻找出最简单高效的解决方案。
相关文章推荐
- Python3写爬虫(四)多线程实现数据爬取
- PropertyChangeListener简单理解
- 什么是设计模式
- 设计模式之创建型模式 - 特别的变量问题
- 七、设计模式——装饰模式
- 设计模式总结
- 设计模式之创建型模式
- 浅谈设计模式的学习
- C#实现多线程的同步方法实例分析
- Ruby设计模式编程之适配器模式实战攻略
- 实例讲解Ruby使用设计模式中的装饰器模式的方法
- 设计模式中的模板方法模式在Ruby中的应用实例两则
- Ruby设计模式编程中对外观模式的应用实例分析
- 实例解析Ruby设计模式编程中Strategy策略模式的使用
- Ruby中使用设计模式中的简单工厂模式和工厂方法模式
- Ruby使用设计模式中的代理模式与装饰模式的代码实例
- 浅谈chuck-lua中的多线程
- 详解组合模式的结构及其在Ruby设计模式编程中的运用
- C#简单多线程同步和优先权用法实例