单例模式的四种实现--应该是你见过的最全的单例模式实现啦。

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://lixuekai.blog.csdn.net/article/details/50978916

一般说到单例模式,老铁们,都是知道老师讲的:饿汉式,饱汉式,双重旋锁式。然后呢,这些个实现,老师讲的不全,马马虎虎,觉得给你说一下,就够用啦。但是,你实际被问到的时候,就觉得,当年老师说的全的,是OK的,其实不是的。老师讲的不全,毕竟老师少有很讲究刨根问底的。

在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。

Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”

一、什么时候使用单例模式:

当实例存在多个会引起程序逻辑错误的时候

二、好处:

1、减少内存的占用
       2、单例模式会阻止其他对象实例化其自己的单例对象的副本,从而确保所有对象都访问唯一实例。
       3、因为类控制了实例化过程,所以类可以灵活更改实例化过程   

      三、单例模式的三种模式:

      1、饿汉式

package com.lxk.designPattern.singleton;

/**
 * 饿汉式--就是屌丝,穷,不给准备好,担心饿死。类加载就给准备好
 * <p>
 * Created by lxk on 2017/3/23
 */
public class SingletonPattern1 {
    //有的会加final修饰符(更为严谨),添加final修饰符之后,指向的引用不能再做更改。
    //这是final的用法:final成员变量表示常量,只能被赋值一次,赋值后值不能再改变。
    //这句话得这么理解:
    //对于一个final变量。
    // 如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
    // 如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
    private static final SingletonPattern1 singletonInstance = new SingletonPattern1();

    private SingletonPattern1() {
    }

    public static SingletonPattern1 getSingletonInstance() {
        return singletonInstance;
    }
}

     2、饱汉式(也叫懒汉式)--延迟加载

package com.lxk.designPattern.singleton;

/**
 * 饱汉式(懒汉式)----就是有钱,豪,用的时候再new(线程不安全)
 * <p>
 * Created by lxk on 2017/3/23
 */
public class SingletonPattern2 {
    //这个就不能加final,因为要在其他地方给他再次赋值呢。
    //加了final,那就默认一直是null啦,而且还不能再次给此属性赋值。
    //此属性是静态,那么就是共享数据,多线程并发操作共享数据是有可能的。那么就会出现下面的线程不安全现象。
    private static SingletonPattern2 singletonInstance;

    private SingletonPattern2() {
    }

    public static SingletonPattern2 getSingletonInstance() {
        if (singletonInstance == null) {
            //在这个地方,多线程的时候,
            //可能A线程挂起,此属性还是null,那么B线程可能也判断条件OK也进来啦。
            //然后A线程可以执行的时候就会new个对象,线程B也会new个对象。
            //就不能保证内存的唯一性。也就是线程不安全
            singletonInstance = new SingletonPattern2();
        }
        return singletonInstance;
    }

    ///**
    // * 为了应对上述的不安全,可以简单的如下操作给方法添加[synchronized],使之成为同步函数。
    // * 但是:
    // * 在很多线程的情况下,就每个线程访问都得判断锁,效率就是问题。所以,才有后面的[双重锁形式]
    // */
    //public static synchronized SingletonPattern2 getSingletonInstance() {
    //    if (singletonInstance == null) {
    //        singletonInstance = new SingletonPattern2();
    //    }
    //    return singletonInstance;
    //}
}

    3、双重锁形式

     这个模式将同步内容下放到if内部,提高了执行的效率,不必每次获取对象时都进行同步,只有第一次才同步,创建了以后就没必要了。避免土豪模式下创建单例,可能存在的线程不安全问题。

package com.lxk.designPattern.singleton;

/**
 * 双重锁形式
 * 这个模式将同步内容下放到if内部,提高了执行的效率,不必每次获取对象时都进行同步,
 * 只有第一次才同步,创建了以后就没必要了。避免土豪模式下创建单例,可能存在的线程不安全问题。
 * <p>
 * Created by lxk on 2017/3/23
 */
public class SingletonPattern3 {
    private static SingletonPattern3 singletonInstance;

    private SingletonPattern3() {
    }

    /**
     * 静态方法同步的时候,使用的锁,就不能是this,而是类.class
     */
    public static SingletonPattern3 getSingletonInstance() {
        if (singletonInstance == null) {
            //这个地方可能有多个线程,在这排队,ABCD..。
            synchronized (SingletonPattern3.class) {
                if (singletonInstance == null) {
                    //假设第一次A线程走到这,然后,呈挂起状态。这个时候,单例对象还未创建;
                    // 假设此时,B线程也来了判断单例对象==null成立,但是,因为A线程已经给里层的if判断上锁,所以,B只能在外等着。
                    //假设A线程被唤醒,那么,单例就会下面语句赋值,单例对象就创建啦。然后释放锁。B就可以进来啦。
                    //B线程进来之后,先判断单例对象是否为null,发现已经不是null啦,那么就不需要创建啦。
                    //CD线程同样,
                    //再往后面来的,第一个if就进不来啦,那就不会判断锁了。
                    singletonInstance = new SingletonPattern3();
                }
            }
        }
        return singletonInstance;
    }
}

上面大道理讲完了,那么你想想你在哪里用到过。

我记得我当时在安卓开发那会,整个项目会去控制一个contextInstance就是:对外提供整个应用生命周期的Context,整个用的就是这个单例模式来控制的。

对外提供Application Context,就是只对外提供获得这个单例的方法。就保证整个app就这么一个Context。

当然,用的虽然是饿汉式的模式,但是也差不了多少,就是在这个类加载的时候,给那个单例对象赋值,然后对外提供方法来访问这个单例对象。其实和那个直接new然后等于是一样的。如下:

    /**
     * 对外提供整个应用生命周期的Context
     */
	private static Context contextInstance;    
	@Override
	public void onCreate() {
		super.onCreate();
		contextInstance = this;
		initImageLoader(getApplicationContext());
		JuheSDKInitializer.initialize(getApplicationContext());
	}
	/**
	 * 对外提供Application Context
	 * @return
	 */
	public static Context getContextInstance() {
		return contextInstance;
	}

重点就是:
1.私有(private)静态(static)的单例对象(object)
2.构造函数(struct)-----可在构造方法初始化单例对象
3.提供对外(public),静态方法获得单例对象

自己总结下。

(2018.08.29更新)

哎,上面的三种,你要是都能说出来,你还是比较牛逼的,前提是你刚刚毕业的时候,这么说是没问题的,但是,单例模式,不仅仅止于这个层面,还有更高级的。当时老师没说,我也不知道,果然还是面试官是牛x,上面的都是小儿科,仅仅是个开始,就算你会上面的第三种,双重旋锁,也不够。因为你没使用到一个关键字:  volatile

这个volatile,开始往后继续问,才是开始。多线程啊,jmm,jvm啊,线程安全你知道多少,就开始啦。

下面看差别。

package com.lxk.designPattern.singleton;

/**
 * 双重锁形式
 * 这个模式将同步内容下放到if内部,提高了执行的效率,不必每次获取对象时都进行同步,
 * 只有第一次才同步,创建了以后就没必要了。避免土豪模式下创建单例,可能存在的线程不安全问题。
 * <p>
 * @author lxk on 2017/3/23
 */
public class SingletonPattern3 {
    /**
     * 加上volatile,禁止指令重排,造成的bug。
     * singletonInstance = new SingletonPattern3();
     * 看似一句话,但分三个步骤。
     * 1,memory = allocate(); //1:分配对象的内存空间
     * 2,ctorInstance(memory); //2:初始化对象
     * 3,instance = memory; //3:设置instance指向刚分配的内存地址
     * 但是经过重排序后如下:
     * 1,memory = allocate(); //1:分配对象的内存空间
     * 2,instance = memory; //3:设置instance指向刚分配的内存地址,此时对象还没被初始化,此时,instance已经不为null啦。
     * 3,ctorInstance(memory); //2:初始化对象
     * 在线程A初始化完成这段内存之前,线程B虽然进不去同步代码块,
     * 但是在同步代码块之前的判断就会发现instance不为空,但是在第一个IF判断就不为空了。
     * 此时线程B获得instance对象进行使用就可能发生错误。
     *
     */
    private static volatile SingletonPattern3 singletonInstance;

    private SingletonPattern3() {
    }

    /**
     * 静态方法同步的时候,使用的锁,就不能是this,而是类.class
     */
    public static SingletonPattern3 getSingletonInstance() {
        if (singletonInstance == null) {
            //这个地方可能有多个线程,在这排队,ABCD..。
            synchronized (SingletonPattern3.class) {
                if (singletonInstance == null) {
                    //假设第一次A线程走到这,然后,呈挂起状态。这个时候,单例对象还未创建;
                    // 假设此时,B线程也来了判断单例对象==null成立,但是,因为A线程已经给里层的if判断上锁,所以,B只能在外等着。
                    //假设A线程被唤醒,那么,单例就会下面语句赋值,单例对象就创建啦。然后释放锁。B就可以进来啦。
                    //B线程进来之后,先判断单例对象是否为null,发现已经不是null啦,那么就不需要创建啦。
                    //CD线程同样,
                    //再往后面来的,第一个if就进不来啦,那就不会判断锁了。
                    singletonInstance = new SingletonPattern3();
                }
            }
        }
        return singletonInstance;
    }
}

要说,也没啥,就是加啦一个关键字。

然后,你就得说明白,这个关键字是干啥的,有啥作用。

1,各个线程之间使之内存可见。

2,禁止指令重排。

哎,我这就简单描述一下,这2个作用,面试官肯定不会就因为你说这2句总结性的话,就stop的,你得具体点,这2句话怎么理解啊。详细说说。不懂的自己查查吧,看到师兄的文章,也得涨涨姿势啊,是不是,不能都跟网上的一样,千篇一律的,没啥长进,不好,不好。

2019-04-09 更新

package com.lxk.designPattern.singleton;

/**
 * 单例模式第四种
 *
 * @author lxk on 2018/5/16
 */
public class SingletonPattern4 {
    /**
     * 我们把Singleton实例放到一个静态内部类中,这样可以避免了静态实例在Singleton类的加载阶段
     * (类加载过程的其中一个阶段的,此时只创建了Class对象)
     * 就创建对象,毕竟静态变量初始化是在SingletonInner类初始化时触发的,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的。
     *
     * 考虑反射:
     *   由于在调用 Holder.singleton 的时候,才会对单例进行初始化,而且通过反射,是不能从外部类获取内部类的属性的。
     *   所以这种形式,很好的避免了反射入侵。
     *
     * 考虑多线程:
     *   由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,所以可以保证其线程安全性。
     *
     * 总结:
     *   优势:
     *          兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。
     *   劣势:
     *          需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。
     *          创建的单例,一旦在后期被销毁,不能重新创建。
     */
    private static class Holder {
        static {
            System.out.println("静态内部类的静态代码块执行");
        }

        private static SingletonPattern4 singleton = new SingletonPattern4();
    }

    private SingletonPattern4() {
        if (Holder.singleton != null) {
            throw new IllegalStateException();
        }
    }

    public static SingletonPattern4 getSingletonInstance() {
        System.out.println("");
        return Holder.singleton;
    }
}

还有就是这个实现形式,基本上没见过。

使用静态内部类的形式来实现单例模式。

详细解释:参见上面的代码注释。

 

展开阅读全文

没有更多推荐了,返回首页