Java设计模式(1)--单例模式详解

来源:互联网 发布:坐标定位软件 编辑:程序博客网 时间:2024/05/21 16:59

单例设计模式是最常用到的设计模式之一。
主要作用是在程序中某一个实例需要保证只有一个,以达到程序所需的目的。通常一些管理器和控制器常被设计成单例模式。或者说: 这些类,在应用中如果有两个或者两个以上的实例会引起错误,又或者我换句话说,就是这些类,在整个应用中,同一时刻,有且只能有一种状态。

单例模式的优点:
1、提供了实例的唯一的受控访问,单例控制了实例的唯一性,所以可以控制程序如何访问实例的方法,确保程序的正确性。
2、节约资源,由于实例的唯一性,避免了许多不必要的实例占用内存控件,大大减少了内存消耗以及实例加载的时间。
单例模式的缺点:
1、单例模式没有抽象层,如果单例类需要拓展,比较麻烦。
2、单例类职责不明确,或者职责过多,以方便充当工厂,提供访问方法,另一方面充当角色,业务逻辑方法也在单例类中实现。
3、滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

单例模式的集中写法及优缺点
1、饿汉式:

public class Single{    private static Single single = new Single();//声明,实例化    private Single(){};//构造方法    public static Single getInstance(){//获取单例        return single;      }}

这种方式的优点是:简单,最快速地得到单例对象。从以下几个方面保证实例的唯一性。
1).静态实例,带有static关键字的属性在每一个类中都是唯一的。
2).限制客户端随意创造实例,即私有化构造方法,此为保证单例的最重要的一步。
3).给一个公共的获取实例的静态方法,注意,是静态的方法,因为这个方法是在我们未获取到实例的时候就要提供给客户端调用的,所以如果是非静态的话,那就变成一个矛盾体了,因为非静态的方法必须要拥有实例才可以调用。
4).由于是静态实例,它只在类加载时被加载一次,不会存在多线程创建多个实例的问题(JVM层控制并发问题)。

当然,这种写法的缺点也很明显,假设该类还有其他对外访问的方法,那么调用该方法,也势必创建一个实例,但是这个实例我们并不一定需要,造成资源浪费。

2、懒汉式

public class Single{    private static Single single = null;    private Single(){};    public static Single getInstance(){        if( single ==null){            single = new Single();          }        return single;    }}

与饿汉式不同的在于,类加载时,实例并没有创建,而是需要调用getInstance方法时才会被创建,那么这样就解决了饿汉式类加载就创建实例造成资源浪费的问题了。当然,更为严重的问题也来了,这鬼东西无法在多线程下跑,也就是并发情况下会出问题。是如何出问题的呢?
假设线程A进来,getInstance()方法中判断实例single==null,那么,进入if判断当中进行new,也就是实例化对象,此时,就是这么巧,A线程就差这么一丢丢创建好实例时,B线程也进入到if判断,发现single实例还没有被创建,那么B线程也去创建实例去了,这就造成了两个线程创建了两个不同的实例,那问题来了,说好的单例呢?
懒汉式再加个锁?

2.1、懒汉式(加锁)

public class Single{    private static Single single = null;    private Single(){};    public static synchronized  Single getInstance(){        if( single ==null){            single = new Single();          }        return single;    }}

很好,这波很强势,成功把并发问题解决了,但是,新的问题出现了,假设100个线程进来了,而getInstance()方法只让一个线程进入,其他线程进不来,全在等待状态,资源浪费就更大了,而且访问量大的话,性能实在太差劲了。
既然这样,那就再变通一下,把锁放在方法内部,创建实例的地方行不行?

2.2、懒汉式(双重锁定)

public class Single{    private static Single single = null;    private Single(){};    public static Single getInstance(){        if( single ==null){            synchronized(Single.class){                if( single ==null){                    single = new Single();                  }               }        }        return single;    }}

先来理解一下这个双重锁定。为什么同步的代码里面还需要加一层if的判断呢?
来假设一下,如果没有同步代码里面的这个if( single ==null)会有什么样的效果,并发模式下,线程A进来,先判断第一次为空,进入同步代码段,然后实例化single,正常的跑下去,返回了一个实例,而恰好,在return single之前的0.000001秒,线程B刚刚进入第一个判断,由于A还没返回实例,那么自然,B判断依然为空,而A已经释放了锁,B进入了同步代码块,再次创建了一个实例。这又走上一个例子的老路了。所以,第二个if是必须的。这种方式也叫做延迟加载。
很好,这种方式看起来缺失解决了资源浪费和并发访问的两个问题(缺点也很明显,代码一大堆)。但是真的就解决了吗?答案是没有,依然存在风险的可能,只是可能。下面应用网上其他博主写的一段解释,我觉得解释非常清楚,也非常好。原博地址:http://www.cnblogs.com/zuoxiaolong/p/pattern2.html
经过刚才的分析,貌似上述双重加锁的示例看起来是没有问题了,但如果再进一步深入考虑的话,其实仍然是有问题的。
如果我们深入到JVM中去探索上面这段代码,它就有可能(注意,只是有可能)是有问题的。
因为虚拟机在执行创建实例的这一步操作的时候,其实是分了好几步去进行的,也就是说创建一个新的对象并非是原子性操作。在有些JVM中上述做法是没有问题的,但是有些情况下是会造成莫名的错误。
首先要明白在JVM创建新的对象时,主要要经过三步。
1.分配内存
2.初始化构造器
3.将对象指向分配的内存的地址
这种顺序在上述双重加锁的方式是没有问题的,因为这种情况下JVM是完成了整个对象的构造才将内存的地址交给了对象。但是如果2和3步骤是相反的(2和3可能是相反的是因为JVM会针对字节码进行调优,而其中的一项调优便是调整指令的执行顺序),就会出现问题了。
因为这时将会先将内存地址赋给对象,针对上述的双重加锁,就是说先将分配好的内存地址指给single,然后再进行初始化构造器,这时候后面的线程去请求getInstance方法时,会认为single对象已经实例化了,直接返回一个引用。如果在初始化构造器之前,这个线程使用了single,就会产生莫名的错误。

上面所说的这个问题,我们貌似无法避免,那么只能要求JVM帮我们控制并发和实例的唯一性了。下面就是比较标准的单例模式的写法,主要用的是静态内部类。

3、标准写法

public class Single{    private Single(){};    public static Single getInstance(){        return SingleInnerClass.single;     }       //静态内部类    private static class SingleInnerClass{        static Single single = new Single();    }}

这种方式为何会避免了上面莫名的错误,主要是因为一个类的静态属性只会在第一次加载类时初始化,这是JVM帮我们保证的,所以我们无需担心并发访问的问题。所以在初始化进行一半的时候,别的线程是无法使用的,因为JVM会帮我们强行同步这个过程。另外由于静态变量只初始化一次,所以single仍然是单例的。这就是JVM帮我们做的控制,完美!

总结一下标准写法的几个优势:
1.Single最多只有一个实例,在不考虑反射强行突破访问限制的情况下。
2.保证了并发访问的情况下,不会发生由于并发而产生多个实例。
3.保证了并发访问的情况下,不会由于初始化动作未完全完成而造成使用了尚未正确初始化的实例。

over

0 0
原创粉丝点击