拼多多一面 单例引发的思考和总结

拼多多一面(报的是客户端方向),显示问了项目,然后问我一些图像处理方面的知识(硕士研究方向是这个,做的软件也稍微涉及,但是一直在做开发工作,图像方面涉及到的也是最简单的知识,没有系统的上过图像处理的课程),问我如何生成灰度图(只用过OpenCV里的接口,不晓得原理),如何高斯模糊,如何进行卷积,问我形态学里的黑帽等概念熟不熟悉。这里跪掉。
然后发了一个二重校验锁的代码给我,让我说一下两个判空的意义。让我挑出代码中的错误。
在这里插入图片描述

class Resource{//类名和实例化名字不一致

    //没有构造函数,并且构造函数设置成私有的,不允许类在外部实例化
    private Resource(){}  
    //实例设置成volatile,一是保证了可见性,二是避免了指令重排的优化
    private static volatile Resource resource = null;
    //方法设置成static ,因为在类内部直接实例化且外部无法实例化,故必须设为static形式,使得在外部可以直接通过类名.方法调用而不实例化
    public static Resource getResource(){ 
      if(resource == null){
         synchronized{
          if(resource == null)
              resourse = new Resource();
          }
       }
      return resource;
     }
 }

这一块答的不好,被问加上static和volatile的作用,不加会怎样。下面系统的整理一下单例以及static 以及volatile。

单例模式

单例模式的特点

  • 单例类只能有一个实例;
  • 单例类必须自己创建自己的唯一实例;
  • 单例类必须给所有其他对象提供这一实例。

几种常见的单例模式

单例模式的写法有好几种,主要介绍一下懒汉式单例、饿汉式单例、登记式单例

饿汉式单例

类初始化的时候就立刻实例化,天生线程安全

public class Singleton {
    private Singleton() {}
    private static final Singleton singleton = new Singleton();//直接实例化
    public static Singleton getInstance() {
        return singleton;
    }
}

缺点:系统启动时就进行实例化,占用资源;如果后期这个单例没有被使用,会造成资源浪费

懒汉式单例

不着急实例化,需要用的时候才初始化

最原始的方式

public class Singleton {
    private Singleton() {}
    private static volatile Singleton singleton = null;
    public static Singleton getInstance() {
         if (singleton== null) {  
             singleton= new Singleton();
         }  
        return singleton;
    }
}

缺点:

  • 在反射面前没什么用,因为java反射机制是能够实例化构造方法为private的类的;
  • 线程不安全,因为线程访问具有随机性,并发环境可能出现多个singleton实例
线程同步式

public class Singleton {
    private Singleton() {}
    private static volatile Singleton singleton = null;

    //为了保证多线程环境下正确访问,给方法加上同步锁synchronized
    public static synchronized Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

缺点:在方法调用上加了同步,虽然线程安全了,但是每次都有同步,会影响性能,毕竟99%是不需要同步的

双重锁检查式,也就是被问的一个

public class Singleton {
    private Singleton() {}
    private static Singleton singleton = null;

    //双重锁检查
    public static Singleton getInstance() {
        if (singleton == null) {                     //1
            synchronized (Singleton.class) {         //2
                if (singleton == null) {             //3
                    singleton = new Singleton3();    //4
                }
            }
        }
        return singleton;
    }
}

优点:线程安全,且确保了只有第一次调用单例时才会做同步,避免了每次都同步的性能损耗;
缺点:双重锁降低了程序响应速度和性能

在多线程的情况下,只有1,没有2,3,就可能导致创建多个实例。例如,线程A和线程B调用getInstance方法,线程A先判断了1,然后时间片结束了,切换到线程B,线程B判断1,因为判断的时候两个线程都是没有判断到实例,故分别创建了一个实例了。破坏了单例的结构。

为此加了synchronized保证只有一个线程进入临界区。那只有2,没有3,可以吗?还是考虑和前面一模一样的场景,这次线程A和线程B都判断了1了,进入2,线程A先进入临界区,线程B发现线程A进入了临界区,就挂在了Singleton.class等等待队列中,等待线程A执行完成。线程A继续执行,创建了一个singleton实例。退出了临界区。然后线程B被唤醒,进入临界区,又创建了一个singleton实例。结果又创建了两个singleton实例。

在上面例子中,如果线程B发现实例已经被创建了(singleton不等于null),就直接退出临界区了。那1和3的作用似乎有点重合了,1似乎就不是必须了。

2,3确实就足够保证单例了。但是加锁是比较消耗资源的,1就是为了减少资源的消耗。

同时:

volatile

instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)

volatile可以禁止jvm对指令的优化,若是指令重排,可能会出现先初始化变量,再分配内存,但是在分配内存执行之前要是有线程过来访问的话,就会得到一个没有分配内存的实例,禁止指令重排,读操作就不会重排序到内存屏障之前。

Static

static 确保只在类加载的时候才初始化一次。
单例模式是运行的当前虚拟机中有且只有一个需要的对象,不存在重复(通过private构造方法控制不让外界访问)。
static 是给类静态成员变量使用的,属于类的属性,一般是一些常量之类的东西,从加载上来说对于类和对象之间,在类加载到内存时候静态成员变量就存在了,而对象还不存在。

静态方法只能调用静态成员变量,为了配合实现单列。

说到底,成员变量需要用static修饰是被迫的

静态内部类式

public class Singleton {
    private Singleton() {}

    //内部类的初始化需要依赖主类,需要先等主类实例化之后,内部类才能开始实例化
    private static class LazyHolder {
        //这里加final是为了防止内部将这个属性覆盖掉
        private static final Singleton INSTANCE = new Singleton();
    }

    //这里加final是为了防止子类重写父类
    public static final Singleton getInstance() {
        return LazyHolder.INSTANCE;
    }
}

优点:利用了classloader机制来保证初始化instance时只有一个线程,线程安全且没有性能损耗

注册登记式
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 注册登记式单例
 * 类似Spring里面的方法,将类名注册,下次从里面直接获取
 */
public class Singleton {
    //Spring最底层的这个容器就是一个map,说白了,IOC容器就是一个map
    private static Map<String, Singleton> map = new ConcurrentHashMap<String, Singleton>();

    //每个class对应一个map的key,也就是唯一的id
    static {
        Singleton singleton = new Singleton();
        map.put(singleton.getClass().getName(), singleton);
    }

    //保护默认构造函数
    protected Singleton() {}

    //静态工厂方法,返回此类唯一的实例
    public static Singleton getInstance(String name) {
        if (name == null) {
            name = Singleton.class.getName();
        }
        if (map.get(name) == null) {
            try {
                map.put(name, (Singleton) Class.forName(name).newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return map.get(name);
    }
}

相当于有一个容器装载所有实例,在实例产生之前先检查下容器有没有,如果有就直接取出来,如果没有就先new一个放进去,然后给后面的人用,SpringIoc容器就是一种注册登记式单例

登记式单例实际上维护了一种单例类的实例,将这些实例存放在一个Map中,对于已经登记过的实例,则从Map直接返回,没有登记的,则先登记,然后返回;

登记式单例内部实现其实还是用的饿汉式,因为其中的static方法块,它的的单例在类被装载时就被实例化了

Volatile

Java语言提供了一种稍弱的同步机制,即volatile变量,用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。

访 v o l a t i l e 使 线 v o l a t i l e s y c h r o n i z e d \color{#FF0000}{在访问volatile变量时不会执行加锁操作,因此也就不会使执行线程阻塞,因此volatile变量是一种比sychronized关键字更轻量级的同步机制。}
  在这里插入图片描述

当对非 volatile 变量进行读写的时候,每个线程先从内存拷贝变量到CPU缓存中。如果计算机有多个CPU,每个线程可能在不同的CPU上被处理,这意味着每个线程可以拷贝到不同的 CPU cache 中。

v o l a t i l e J V M C P U c a c h e \color{#FF0000}{而声明变量是 volatile 的,JVM 保证了每次读变量都从内存中读,跳过 CPU cache 这一步。}

当一个变量定义为 volatile 之后,将具备两种特性:

  1. 保证此变量对所有的线程的可见性,这里的“可见性”,如本文开头所述,当一个线程修改了这个变量的值,volatile 保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。但普通变量做不到这点,普通变量的值在线程间传递均需要通过主内存(详见:Java内存模型)来完成。

  2. 禁止指令重排序优化。有volatile修饰的变量,赋值后多执行了一个“load addl $0x0, (%esp)”操作,这个操作相当于一个内存屏障(指令重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问内存时,并不需要内存屏障;(什么是指令重排序:是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理)。


博客部分内容参考下面的博文:

设计模式–单例模式(二)双重校验锁模式 https://blog.csdn.net/hlanting/article/details/78346280

Java中Volatile关键字详解(这个文章讲的通俗易懂,俺嚼着写好蛮好)https://www.cnblogs.com/zhengbin/p/5654805.html#autoid-0-1-0

JAVA单例模式:就是把构造方法弄成私有的 https://blog.csdn.net/lovestudy_girl/article/details/51735773

几种单例模式实现方式及其优缺点分析 https://www.cnblogs.com/wenzhihong/p/10600234.html

发布了218 篇原创文章 · 获赞 636 · 访问量 45万+

猜你喜欢

转载自blog.csdn.net/qunqunstyle99/article/details/99640609
今日推荐