vlambda博客
学习文章列表

清晨尝鲜,什么是单例模式?


单例模式

简单聊聊,BAT面试高频考点,如何手写一个线程安全的单例模式。

什么是单例模式

保证一个类只有一个实例,并且提供一个访问该全局访问点,保证对象唯一性。

单例优缺点

说两个最核心的优缺点。

  • 优点:节约内存,方便重用
  • 缺点:存在线程安全问题

单例创建方式

  1. 饿汉式:类初始化时,会立即加载该对象,线程天生安全,调用效率高。

    • 私有构造函数
    • 创建全局静态私有对象
    • 创建静态返回对象的方法

注意:不存在线程安全问题为什么呢?

static的成员是在类加载的时候就生成了。饿汉模式在类被初始化时就已经在内存中创建了对象,而不是生成对象的时候,故不存在线程安全问题。

✍上代码:

package com.xianglei;

/**
 * @author xianglei
 */


/**
 *  单例模式 饿汉式   线程安全
 */

public class SingletonDemo01 {
    /**
     *  1.私有构造方法,就是不让你new对象
     */

    private SingletonDemo01(){
     System.out.println("饿汉开始初始化了!!!");
    }
    /**
     * 2.创建对象,饿? 一创建实例就获取对象
     */

    private static final SingletonDemo01 SINGLETON_DEMO_01=new SingletonDemo01();
    /**
     * 3.创建返回对象的方法
     */

    public static SingletonDemo01 getInstance(){
        return SINGLETON_DEMO_01;
    }
    public  static  void  main (String[] args){
        SingletonDemo01 singletonDemo01=SingletonDemo01.getInstance();
        SingletonDemo01 singletonDemo02=SingletonDemo01.getInstance();
        System.out.println(singletonDemo01==singletonDemo02);
    }
}

✍小变种:

package com.xianglei;

/**
 * 饿汉变种
 *
 * @author xianglei
 * @version 1.0
 * @date 2018年12月23日 15:56:20
 */

public class SingletonDemo06 {
    private static SingletonDemo06 instance = null;

    private SingletonDemo06() {

    }
    static {
        instance=new SingletonDemo06();
    }

    public static SingletonDemo06 GetInstance(){
        return instance;
    }
    public static void main(String []args){
        SingletonDemo06 s1=SingletonDemo06.GetInstance();
        SingletonDemo06 s2=SingletonDemo06.GetInstance();
        System.out.println(s1==s2);
    }


}

表面上看起来差别挺大,其实差不多,都是在类初始化即实例化instance。只不过用了一个静态代码块在加载时是第一个加载的知识点。

  1. 懒汉式: 类初始化时,不会初始化该对象,真正需要使用的时候才会创建该对象,具备懒加载功能。

    • 私有构造函数
    • 创建全局静态私有对象。
    • 创建静态返回对象的方法,如果为空就创建一个对象,注意处理线程安全问题

✍上代码:

package com.xianglei;

/**
 * @author xianglei
 * 懒汉式 单例模式
 */

public class SingletonDemo02 {
    /**
     * 1. 私有构造方法
     */

    private SingletonDemo02() {

    }
    /**
     * 2.创建静态对象
     */

    private static SingletonDemo02 singletonDemo02;

    /**
     * 3.创建返回对象的方法 注意处理线程安全问题
     */

    public synchronized  static  SingletonDemo02 getInstance() {
        if (singletonDemo02 == null) {     // 判断是否已经存在对象,在这里加同步锁会导致不管是否应该创建了对象每个线程创建的时候会等待别的线程释放锁,从而降低了效率
            singletonDemo02=new SingletonDemo02();
        }
        return singletonDemo02;
    }

    public static void main(String[] args) {
        SingletonDemo02 singletonDemo01=SingletonDemo02.getInstance();
        SingletonDemo02 singletonDemo02=SingletonDemo02.getInstance();
        System.out.println(singletonDemo01==singletonDemo02);
    }
}

注意:

懒汉式是天生线程不安全,为什么呢?如果有多个线程在访问的时候可能会创建多个对象,因为在进行判断可以会被判为空,违背单利的原则。你需要自己加上sychronized关键字。相比于饿汉式效率比较低。

  1. 静态内部方式:结合了懒汉式和饿汉式各自的优点,真正需要对象的时候才会加载,加载类是线程安全的。

    • 私有构造函数
    • 创建静态内部类 获取实例对象
    • 创建返回对象的静态方法,注意方法没有同步
  • 优势:兼顾了懒汉模式的内存优化(使用时才初始化)以及饿汉模式的安全性(不会被反射入侵)。

  • 劣势:需要两个类去做到这一点,虽然不会创建静态内部类的对象,但是其 Class 对象还是会被创建,而且是属于永久带的对象。

✍上代码:

package com.xianglei;

/**
 * @author xianglei
 * 静态内部类方法 创建单例模式
 */

public class SingletonDemo03 {
    /**
     * 1.私有构造方法
     */

    private SingletonDemo03() {
        System.out.println("初始化..");
    }

    /**
     * 2.创建静态内部类 获取实例对象
     */

    public static class SingletonClassInstance {
        private static final SingletonDemo03 singletonDemo03 = new SingletonDemo03();
    }

    /**
     *  3.创建返回对象的静态方法,注意方法没有同步,
      */


    public static SingletonDemo03 getInstance() {

        return SingletonClassInstance.singletonDemo03;
    }

    public static void main(String[] args) {
        SingletonDemo03 s1 = SingletonDemo03.getInstance();
        SingletonDemo03 s2 = SingletonDemo03.getInstance();
        System.out.println(s1 == s2);
    }

}

注意:

  • 这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,由于在调用 SingletonClassInstance.singletonDemo03 的时候,才会对单例进行初始化,而且通过反射,是不能从外部类获取内部类的属性的。所以这种形式,很好的避免了反射入侵。

  • 如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。

  1. 枚举单例: 使用枚举实现单例模式 优点:实现简单、调用效率高,枚举本身就是单例,由jvm从根本上提供保障!避免通过反射和反序列化的漏洞, 缺点没有延迟加载。

    枚举本身是单例的,一般用于项目中定义常量,天生具备JVM保障单例,防止反射攻击。

    • 创建私有构造函数
    • 创建静态枚举,创建构造方法,创建返回对象方法
    • 创建静态返回方法,返回枚举里面返回的对象

✍上代码:

package com.xianglei;

/**
 * @author xianglei
 * 枚举创建单例模式
 */

public class SingletonDemo04 {
    /**
     * 1. 私有化构造方法
     */

    private SingletonDemo04() {

    }

    /**
     * 2.创建枚举方法
     */

    private static enum SingletonDemo {
        /**
         * 枚举元素为单例
         */

        INSTANCE;

        private SingletonDemo04 SingletonDemo04;

        private SingletonDemo() {
            System.out.println("SingletonDemo04");
            SingletonDemo04 = new SingletonDemo04();
        }

        public SingletonDemo04 getInstance() {
            return SingletonDemo04;
        }
    }

    /**
     *3.拿到枚举里面创建的对象
     */

    public static SingletonDemo04 getInstance() {
        return SingletonDemo.INSTANCE.getInstance();
    }

    public static void main(String[] args) {
        SingletonDemo04 singletonDemo01 = SingletonDemo04.getInstance();
        SingletonDemo04 singletonDemo02 = SingletonDemo04.getInstance();
        System.out.println(singletonDemo01 == singletonDemo02);

    }

}

注意:使用枚举实现单例模式 优点:实现简单、枚举本身就是单例,由jvm从根本上提供保障!避免通过反射和反序列化的漏洞 缺点没有延迟加载.这种方式是Effective Java作者Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏。

  1. 双重检测锁方式

    在懒汉式的基础上,采用双重锁检验,只阻塞一次

    • 创建私有构造函数
    • 创建静态对象
    • 创建获取返回对象方法 注意双重锁检测

✍上代码:

package com.xianglei;

/**
 * @author xianglei
 * 采用双重检测锁机制实现 单例模式   实质山是基于懒汉模式
 */

public class SingletonDemo05 {
    /**
     * 1.私有化构造方法
     */

    private SingletonDemo05() {

    }

    /**
     * 2.创建静态对象
     */

    private static SingletonDemo05 singletonDemo05;

    /**
     * 3.创建获取返回对象方法 注意双重锁检测
     */

    public static SingletonDemo05 getInstance() {
        if (singletonDemo05 == null) {
            synchronized (SingletonDemo05.class) {
                if (singletonDemo05 == null) {
                    singletonDemo05 = new SingletonDemo05();
                }
            }
        }

        return singletonDemo05;
    }

    public  static  void  main(String [] args){
        SingletonDemo05 singletonDemo01=SingletonDemo05.getInstance();
        SingletonDemo05 singletonDemo02=SingletonDemo05.getInstance();
        System.out.println(singletonDemo01==singletonDemo02);
    }
}

注意:

只有在对象需要被使用时才创建,第一次判断 singletonDemo05 == null 为了避免非必要加锁,当第一次加载时才对实例进行加锁再实例化。这样既可以节约内存空间,又可以保证线程安全。

如何选择单例创建方式

  • 如果不需要延迟加载单例,可以使用枚举或者饿汉式,相对来说枚举性好于饿汉式。

  • 如果需要延迟加载,可以使用静态内部类或者懒汉式,相对来说静态内部类好于懒汉式。


end




清晨尝鲜,什么是单例模式?
清晨尝鲜,什么是单例模式?

扫描二维码更精彩

清晨尝鲜,什么是单例模式?



点亮 ,告诉大家你也在看