Java开辟篇——设计模式(2)单例模式你真的了解吗? 单例模式几乎快成了面试官张口就来的一个题目了,特别是面试一些初级岗位的java开辟者大部分都会被问到过单例模式,为什么呢?虽然单例模式虽然看起来简单但是它实现的过程中包含了线程安全、类加载机制、内存模型等一些比较核心的知识,通过对单例模式的先容面试官就可以看出面试者的基本功是否扎实。而每每技术底子不扎实的同砚每每回答的时候就零零散散的简单说下就以为可以了,殊不知你已经踩到了面试官给你挖的一个坑前。 下面我们就来先容下单例模式,看看什么样的先容都可以让面试官夸赞不已。 一.单例模式先容 单例模式指的是在程序的整个运行时域,一个类只有一个实例对象对外部提供调用访问。 普通的明白那就是在古代,整个王朝就一个皇帝。如何确保一个皇帝?这就是单例模式了。 单例设计模式的特点: 1. 单例类只能有一个实例对象; 2. 单例类外部无法创建对象只能由本类创建唯一实例对象; 3. 单例类要提供对外访问的公共方式; 单例设计模式的好处和不足: 好处:由于单例只为类创建一个对象资源斲丧较少,避免了类频繁的创建对象导致的内存飙升、耗费时间等题目,进步了对象的访问速度,降低了体系内存的使用频率,减轻了GC(java中的垃圾回收器)压力; 比方:我们程序访问数据库的操作,创建毗连数据库对象是一个非常耗时耗资源的过程,如果我们把这个对象设计为单例模式,那么我们就创建一次并重复使用这个对象即可,特别在高并发访问下大大进步了效率。 并且程序中也会出现某些特定场合一个类只能创建使用一个对象的情况,比方:证券体系在整个体系中只能有一个证券交易类负责交易等相干操作; 不足:单例类没有抽象类不能扩展,不适用于变化的对象,并且根据单例实现方式的不同大概存在多线程访问下安全和效率题目; 那么在Java中,怎么去实现单例模式呢? 二.单例的实现 单例模式因其创建对象时机的不同,它的实现主要分为懒汉式和饿汉式两种范例; 饿汉式:在类加载的时候就创建对象 懒汉式:类加载的时候不创建对象,在外部第一次调用的时候才创建对象 下面我们先看下最基本的饿汉式和懒汉式的实现。 1. 饿汉式 public class SingleTon { //初始化singleTon实例对象 private static SingleTon singleTon = new SingleTon(); private SingleTon() {} //构造器私有化 //外部调用的公共方法 public static SingleTon getInstance() { return singleTon; 分析:饿汉式中SingleTon 类的构造方法使用了private修饰,那么其他的类没办法通过new来创建singleton对象实例,其他类只能通过调用静态的方法getInstance来访问,这样就可以保证singleTon的实例对象只有一个singleTon类中创建的。 特点: 饿汉式是典型的捐躯空间换取时间的方式,类一加载就创建了实例对象,也不管我们在程序中是否使用;所以会占用更多的内存,但是访问对象的速度比较快;并且在多线程访问情况下也没有线程安全的题目。 2. 懒汉式(简单实现) public class SingleTon { private static SingleTon singleTon; private SingleTon() { public static SingleTon getInstance() { if (singleTon == null) { singleTon = new SingleTon(); return singleTon; 分析:懒汉式跟饿汉式不同的地方是懒汉式在getInstance方法中创建的对象,即在使用对象的时候才去创建对象,这种加载对象的方式也被称为懒加载。 特点: (1)懒汉式由于是在外部使用的时候才调用,所以要更加节省内存一些,但是第一次访问的时候由于必要创建对象所以要比饿汉式慢; (2)线程不安全 多线程访问下,如果出现两个以上线程在没有new SingleTon()的时候就举行了singleTon == null的判断都会返回true,那么就会出现创建了多个实例的情况,这样就违背了单例模式的设计思想。 那么怎么才能使其线程安全呢?有的人就说了,这还不简单?加上 synchronized就好了,确实,这样可以剖析线程安全的题目。 懒汉式—synchronized同步锁 public class SingleTon { private static SingleTon singleTon; private SingleTon() {} public static synchronized SingleTon getInstance() { if (singleTon == null) { singleTon = new SingleTon(); return singleTon; 分析:在getInstance添加了synchronized 确实保证了线程安全,但是由于 synchronized加到方法上,一次性把整个方法给加上了锁,锁的粒度有点大,这样意味 着在多线程访问情况下如果有一个线程访问了方法getInstance获取了锁,其他线程 就要处于等待状态,这样就大大降低了访问效率;所以实际开辟中这种实现方式是不可 取的。那有没有效率更高的实现方式呢? 懒汉式—DCL双重校验锁(保举) Synchronized同步方法的实现效率低是由于锁的粒度太大,那能不能通过减小锁的粒度来进步效率呢?这时候可以使用DCL(Double Check Lock)双重校验锁的方式来实现。 代码如下: public class SingleTon { private static SingleTon singleTon; private SingleTon() {} public static SingleTon getInstance() { if (singleTon == null) {//代码1 synchronized (SingleTon2.class) {//代码2 if (singleTon == null) {//代码3 singleTon = new SingleTon();//代码4} return singleTon; 分析:DCL的实现加锁的粒度变小,在多线程访问getInstance方法的时候不必要竞争获取锁,都可以进入getInstance方法。此时执行代码1举行第一次判空,如果对象实例还没创建那么开始竞争获取锁,竞争到锁的线程A就举行创建singleTon 对象,如果当线程A刚获取锁的同时另外一个线程B也正好符合代码1的判空,那么线程A创建了singleTon 对象之后线程B也要获取锁并创建对象,为了办理这个题目就加入了代码3举行了第二次判空处理。 DCL的实现锁粒度小允很多线程访问getInstance方法,所以效率比同步方法的实现要高。 但是上面的代码真的美满吗?如果是老的程序员看到这个代码就会眉头一皱,内心不禁的会想到。 上面到底题目出在了哪里呢?这里就要说了JVM给出的happens-before通用原则,这里就不具体先容happens-before原则了,它主要规定了jvm多线程原子性、可见性和有序性的一些原则。而上面DCL代码实现中singleTon = new SingleTon();在指令操作中不是一步完成的不属于原子性操作,它的指令操作分为下面三步: (1)memory =allocate(); 先为singleTon 分配内存空间 (2)ctorInstance(memory); 然后初始化singleton对象 (3)instance = memory;最后将singleton指向分配好的内存空间 在真正执行时,JVM假造机为了进步执行效率,在保证结果的情况下大概会举行指令重排,比如JVM认为指令按照 1->3->2执行效率会更高,如果按照这个顺序,假设线程A刚好执行到第三步指令的时候那么此时singleTon 还未初始化仍旧是null,此时线程B执行到了代码1,判断singleTon ==null返回的却是false认为已经创建了singleTon ,那么此时就出现了一个严峻的题目,线程B直接return返回了null,出现了线程不安全的情况。
|