java单例模式的弯弯绕

来源:互联网 发布:智能语音合成软件 编辑:程序博客网 时间:2024/04/28 10:13
单例模式,java中最简单的设计模式,每个写java的都知道,也是结构最简单的,最好理解的,但是要深挖写出高效没有漏洞的单例也是十分复杂的一个事,这两天看了很多这方面的资料,涉及的点十分繁多,特来整理整理,记录一下。
标准单例:
/**标准的单例
* Created by LMT on 2016/5/17.
*/
public class SingletonTest {
private static SingletonTest mInstance;

private SingletonTest(){
}

public static SingletonTest getInstance(){
if (mInstance == null){
mInstance = new SingletonTest();
}
return mInstance;
}
}
这种方式是最常用的,最简单的实现方式,但是在并发情况下是有严重问题的,是无法真正实现单例的(因为创建了多个对象),下面这几种方式可以适用于并发的单例。
1. 饥饿模式(Eager initialization):
/**饥饿模式的单例
* Created by LMT on 2016/5/17.
*/
public class SingletonTest {
private static SingletonTest mInstance = new SingletonTest();

private SingletonTest(){
}

public static SingletonTest getInstance(){
return mInstance;
}
}
这种实现方式会在这个类第一次被使用时也就是类被加载时就创建单例对象,虽然不会出现多线程的并发问题,但这样做效率很低,因为创建的对象的时候其实并不一定需要,平白占用内存,这是最简单的实现方式,因为java类的加载机制的原理,每个类只会被加载一次,这样就解决的并发的问题,但并不是最优方案,而且是有局限的,如果创建对象需要其他参数的动态配置,就无法使用这种方式。
2. 懒惰模式(Lazy initialization):
在我们想要获取单例的时候再创建实例是比较高效理想的方式,但实现起来要考虑的方面就比较多了,特别是同步问题,而且很容易产生一些意想不到的bug,这样反而不美,所以必须仔细斟酌,而且根据情况的不同恰当地选择最合适的实现方式也是十分重要的。
     1)简单加锁模式:      
/**简单加锁模式的单例
* Created by LMT on 2016/5/17.
*/
public class SingletonTest {
private static SingletonTest mInstance;

private SingletonTest(){
}

public synchronized static SingletonTest getInstance(){
if (mInstance == null){
mInstance = new SingletonTest();
}
return mInstance;
}
}
     这种模式虽然实现了同步,但效率比较低下,每次不论是否创建新的实例都要加锁,等待锁,但如果调用不频繁的话是一种比较简单的实现方式。
     2)双重检查锁模式:
/**双重检查锁模式的单例
* Created by LMT on 2016/5/17.
*/
public class SingletonTest {
private static volatile SingletonTest mInstance;

private SingletonTest(){
}

public static SingletonTest getInstance(){
if (mInstance == null){
synchronized(SingletonTest.class){
if (mInstance == null){
mInstance = new SingletonTest();
}
}
}
return mInstance;
}
}
     这种方式高效,首先如果实例已经创建则可以并发获取,而当实例未创建时加了锁,在第一个线程创建完实例后,从第二个开始的线程都会因为第二次的实例是null的检查而避免了重复创建,完美的实现了线程安全的单例,但这里有需要注意的地方,和其他方式不同,这里实例的成员变量前增加了volatile关键字,这个关键字有两个作用,一是可见性也是这里使用这个关键字的原因,为了使创建实例的线程把数据实时写回主内存中去,这样其他线程访问时就可以正确知晓实例已经被创建了,不然创建实例的线程只是在其工作内存中把变量赋了值,而主内存中的变量mInstance还是null,等下一个线程访问时就又去创建实例了,bug也就妥妥地产生了。二是为了防止编译器优化改变命令的执行顺序,这功能在jdk1.5之后才能正常工作,安卓就不用考虑都是1.5以上,比如new一个实例并赋予变量mInstance,在java文件中这只是一个语句,但jvm执行的时候确实多条命令,大致分为3步:1. 生成实例并分配内存。2. 调用实例的构造方法。 3. 把生成的实例的内存地址赋予mInstance实例(这时mInstance被访问时才不为null)。如果编译器优化后可能会改变2,3命令的执行顺序,如果在3被执行完,而2还没执行时,其他线程就调用了instance对象并使用,那肯定会出现错误(构造方法还没执行就想用...)(但我使用最新的jdk和jre反编译字节码命令并没有发现不同,存疑,如有高手知晓望指点,万分感谢!),像这种bug和编译器的优化有关,又是并发问题,很难复现及调试,必须极力避免。
     3)静态内部类实例化模式:
/**静态内部类实例化模式的单例
* Created by LMT on 2016/5/17.
*/
public class SingletonTest {

private SingletonTest(){
}

public static SingletonTest getInstance(){
return Holder.mInstance;
}

private static class Holder{
private static SingletonTest mInstance = new SingletonTest();
}
}
     这种模式和第一种饥饿模式有点相像,都是借助类的实例化原理来实现并发控制的,只是这种可以把控触发实例化的时机(第一次调用Holder类的时候就会创建单例的对象)。
    
     4)枚举模式:
/**枚举模式的单例
* Created by LMT on 2016/5/17.
*/
public enum SingletonEnumTest {
Instance;
}
     这种模式是既简单又安全还防止反射(反射调用枚举的构造方法然后实例化会报异常Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects)而且序列化也自动完成,是java中最好的实现方式,但是也有个小缺点就是占用内存比静态变量要大,如果是服务器端或者PC端估计不太需要考虑这个问题,但是手机端不一样,安卓官方就有申明尽量少使用枚举,它比静态要多用差不多两倍的内存,这是有多苦逼啊。。。其实如果对象不大不理他感觉问题也不大

注意:
1. 单例模式都有序列化的问题(除了枚举),因为静态成员变量是不会被序列化的(它属于类不属于对象),因此序列化和反序列化需要自己进行保存和读取,不然序列化可能会破坏单例。
2. 反射破坏单例,java提供的反射机制有时也是个隐藏的坑,如果使用反射机制,即使构造方法为private也还是可以直接访问,这样单例模式形同虚设,因此比较保险的方式就是在构造方法中判断对象是否已经被创建,如果是,就抛出异常,打断构造方法。
private SingletonTest() throws Exception {    if (mInstance != null){        throw new Exception("请不要使用反射破坏单例");    }}


Reference:
1. http://blog.csdn.net/u013256816/article/details/50427061
2. http://zha-zi.iteye.com/blog/903332
3. http://blog.csdn.net/a_asinceo/article/details/7973282
4. http://mp.weixin.qq.com/s?__biz=MzA3MDMyMjkzNg==&mid=2652261637&idx=1&sn=f069445f9e5df982ecfd47c9d40cc016&scene=0#wechat_redirect
0 0
原创粉丝点击