集合面试题

集合面试题

arrayList

  • 继承AbstractList,实现了List接口,意味着ArrayList元素是有序的,可以重复的,可以有null元素的集合.
  • 实现了RandomAccess接口标识着其支持随机快速访问,因为ArrayList底层是数组,那么随机快速访问是理所当然的,访问速度O(1)。
  • 实现了Cloneable接口,标识着可以它可以被复制.注意,ArrayList里面的clone()复制其实是浅复制。
  • 实现了Serializable 标识着集合可被序列化

ArrayList扩容机制

  • 初始化:
    ArrayList提供了三个构造函数来对elementData数组初始化:
    无参构造函数:初始化一个空的数组,添加元素时再对数组elementData扩容。初始容量为10,不够时1.5倍增加.
    指定容量的构造函数:直接初始化数组为指定的大小。
    带有一个集合参数的构造函数:把指定集合中的数据通过Arrays.copyOf拷贝到elementData中,容量和指定集合容量相同。

扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。

Fail-Fast机制

modCount 用来记录 ArrayList 结构发生变化的次数。在进行序列化或者迭代等操作时,需要比较操作前后 modCount 是否改变,如果改变了需要抛出 ConcurrentModificationException

LinkedList

LinkedList是一个实现了List接口和Deque接口的双端链表。主要体现在内部私有类Node,前驱节点,后驱节点,节点
LinkedList底层的链表结构使它支持高效的插入和删除操作,另外它实现了Deque接口,使得LinkedList类也具有队列的特性;

Hashmap

JDK1.8之前
JDK1.8之前HashMap底层是数组和链表结合在一起。HashMap通过key的hashCode来计算hash值,当hashCode相同时,通过“拉链法”解决冲突。(链表的数组)
JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
HashMap的实例有两个参数影响其性能:

  • 初始容量:哈希表中桶的数量
  • 加载因子:哈希表在其容量自动增加之前可以达到多满的一种尺度
    当哈希表中条目数超出了当前容量*加载因子(其实就是HashMap的实际容量)时,则对该哈希表进行rehash操作,将哈希表扩充至两倍的桶数。
    Java中默认初始容量为16,加载因子为0.75。

HashMap是线程不安全的,在并发环境下,可能会形成环状链表

HashTable

HashTable是线程安全的。get/put所有相关操作都是synchronized的,这相当于给整个哈希表加了一把大锁,多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,性能就会非常差。

ConcurrentHashMap

HashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想.
ConcurrentHashMap的主干是个Segment数组。Segment继承了ReentrantLock,所以它就是一种可重入锁。Segment里维护了一个HashEntry数组,并发环境下,对于不同Segment的数据进行操作是不用考虑锁竞争的。

Spring

IoC

  • IoC (Inversion of control )控制反转/反转控制。控制指的是对象创建的权力,反转 :控制权交给外部环境(Spring 框架、IoC 容器)
    例如:现有类 A 依赖于类 B
    传统的开发方式 :往往是在类 A 中手动通过 new 关键字来 new 一个 B 的对象出来
    使用 IoC 思想的开发方式 :不通过 new 关键字来创建对象,而是通过 IoC 容器(Spring 框架) 来帮助我们实例化对象。

好处: 1. 对象之间依赖程度降低;2.资源变的容易管理;

  • IoC(Inverse of Control:控制反转)是一种设计思想,最常见的实现方式叫做依赖注入(DI).
    application是主容器,存储了所有的bean,bean的id不能重复,class通过反射机制创建出来放到容器中,通过getBean方法可以将对象拿出来,里面用了反射,代理,依赖注入等一些技术.
    底层主要是Refresh中执行的三个操作:
    定位: 通过BeanDefinitionReader读取配置文件,扫描需要被spring管理的类
    加载: 拿到BeanDefinitionReader读取过来的beanClass集合
    注册: 将BeanDefinitionReader的beanClass集合封装成BeanDefinition的map集合,
    依赖注入: 拿到BeanDefinition的map集合,通过反射拿到该类,放入BeanWrapper中,

面向切面编程AOP

  • AOP 是 OOP(面向对象编程)的一种延续。OOP 编程思想可以解决大部分的代码重复问题。但是有一些问题是处理不了的。比如在同一类中的多个方法的相同位置出现了重复的代码,OOP 就解决不了。这种重复的代码叫横切逻辑代码.

在不改变原有业务逻辑的情况下,增强横切逻辑代码,根本上解耦合,避免横切逻辑代码重复。

springmvc工作原理

(1)客户端(浏览器)发送请求,直接请求到 DispatcherServlet。

(2)DispatcherServlet 根据请求信息调用 HandlerMapping,解析 Handler。

(3)解析到对应的 Handler(也就是我们平常说的 Controller 控制器)后,开始由 HandlerAdapter 适配器处理。

(4)HandlerAdapter 会根据 Handler 来调用真正的处理器开处理请求,并处理相应的业务逻辑。

(5)处理器处理完业务后,会返回一个 ModelAndView 对象,Model 是返回的数据对象,View 是个逻辑上的 View。

(6)ViewResolver 会根据逻辑 View 查找实际的 View。

(7)DispaterServlet 把返回的 Model 传给 View(视图渲染)。

(8)把 View 返回给请求者(浏览器)

springboot自动配置

Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。

springboot面试题

mybatis

Mybatis是一个半ORM框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接等繁杂的过程。程序员直接编写原生态sql,可以严格控制sql执行性能.
1.读取配置文件缓存到Configuration对象,用于创建SqlSessionFactory,SqlSessionFactory是一个接口,提供了一个默认的实现类DefaultSqlSessionFactory
2.通过SqlSessionFactory的openSession去获取我们的sqlSession,通过sqlSession的getMapper方法获得相应的mapper,底层调用的是configuration的getMapper方法,configuration使用的是MapperRegistry(mapper注册器)中的getMapper方法,最终获得mapperProxy的代理类,
3.mapperProxy才是具体去执行操作的类,mapperProxy调用相应的Executor,Executor中通过configuration构建了一个StatementHandler,通过StatementHandler和ParameterHandler对象帮助我们处理语句集和参数的处理,底层我们就是我们平时写的JDBC!然后将这个执行后的Statement交给resultSetHandler处理结果集.

springCloud

Eureka

自动注册、发现、状态监控。
失效剔除:每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。
自我保护:当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。Eureka就会把当前实例的注册信息保护起来,不予剔除。
Robbin:
添加@LoadBalanced注解
负载均衡策略: 轮询
重试机制:使用spring-retry,当访问到某个服务超时后,它会再次尝试访问下一个服务实例,如果不行就再换一个实例

Hystix(熔断器)

服务端判断某些服务超时或反映慢,能主动熔断,直接进行失败回滚,服务降级处理,当情况出现好转会自动重连.

Feign

Feign可以把Rest的请求进行隐藏,你不用再自己拼接url,拼接参数等等操作.
Feign集成了Ribbon依赖和自动配置
Feign默认也有对Hystix的集成

Zuul

动态路由,身份认证(ZuulFilter过滤器),Zuul就是我们服务的统一入口
Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。默认熔断时间为1s.

多线程

synchronized

synchronized关键字解决的是多个线程之间访问资源的同步性,synchronized关键字可以保证多线程访问只能有一个线程执行。
在 Java 早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统.幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized 较大优化,如自旋锁、偏向锁等.
自旋锁: 让后面来的请求获取锁的线程等待一会而不被挂起,为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋)
偏向锁: 偏向于第一个获得它的线程,如果在接下来的执行中,该锁没有被其他线程获取,那么持有偏向锁的线程就不需要进行同步!
synchronized关键字最主要的三种使用方式:
修饰实例方法
修饰静态方法
修饰代码块

底层原理: synchronized 同步语句块的情况(使用的是 monitorenter 和 monitorexit 指令,monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。当执行 monitorenter 指令时,线程试图获取monitor锁,当计数器为0则可以成功获取,获取后将锁计数器加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,锁被释放。)

synchronized 修饰的方法使用ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法.

volatile

在 JDK1.2 之前,Java的内存模型实现总是从主存读取变量,
而在当前的 Java 内存模型下,线程可以把变量保存本地内存中,而不是直接在主存中进行读写。造成数据的不一致.

能保证可见性,不能保证原子性,保证原子性使用synchronized关键字加锁。

  • volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块
  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞

ThreadLocal

  • 每一个线程都有自己的专属本地变量

Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。默认情况下这两个变量都是null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们,实际上调用这两个方法的时候,我们调用的是ThreadLocalMap类对应的 get()、set()方法。

内存泄露问题 : ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

Lock

Lock接口的实现类
ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock

  • ReentrantLock(排他锁),可以结合Condition实例可以实现“选择性通知”,
  • ReentrantReadWriteLock,多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥
  • 谈谈 synchronized和ReentrantLock 的区别:
    • 两者都是可重入锁
    • synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API
    • ReentrantLock 比 synchronized 增加了一些高级功能:①等待可中断;②可实现公平锁;③可实现选择性通知

Atomic

即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
AtomicInteger原理: CAS (compare and swap) volatile 和 native 方法来保证原子操作,CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的 objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。

AQS

一个用来构建锁和同步器的框架.比如ReentrantLock,ReentrantReadWriteLock.

  • AQS 原理: 如果被请求的共享资源空闲,线程直接请求,将共享资源设置为锁定状态。如果被请求的共享资源被占用,需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。AQS使用一个voliate int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。

CLH队列是一个虚拟的双向队列。AQS是将每条线程封装成一个CLH锁队列的一个结点来实现锁的分配。

  • AQS定义了两种资源获取方式:独占(只有一个线程能访问执行,又根据是否按队列的顺序分为公平锁和非公平锁,如ReentrantLock) 和共享(多个线程可同时访问执行,如CountDownLatCh )。ReentrantReadWriteLock 可以看成是组合式,允许多个线程同时对某一资源进行读。

  • AQS底层使用了模板方法模式, 自定义同步器在实现时只需要实现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在上层已经帮我们实现好了。

线程池

有点: 1 提高效率 2 提高线程的可管理性

  • 实现Runnable接口和Callable接口的区别?
    Runnable 接口不会返回结果或抛出检查异常.Callable可以.
  • 执行execute()方法和submit()方法的区别是什么呢?
    1. execute()方法用于提交不需要返回值的任务,无法判断任务是否被线程池执行成功与否;
    2. submit()方法用于提交需要返回值的任务。线程池会返回一个 Future 类型的对象,通过这个 Future 对象可以判断任务是否执行成功.
  • 如何创建线程池
    1. 通过构造方法实现
    2. 通过工具类Executors来实现
      FixedThreadPool : 该方法返回一个固定线程数量的线程池。
      SingleThreadExecutor: 方法返回一个只有一个线程的线程池。
      CachedThreadPool: 该方法返回一个可根据实际情况调整线程数量的线程池。
      newScheduledThreadPool: 创建一个大小无限的线程池。
  • 假设一个服务器完成一项任务所需时间为:T1 创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。

如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能

Executors 返回线程池对象的弊端如下:
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致OOM。

  • ThreadPoolExecutor构造函数重要参数分析
    ThreadPoolExecutor 3 个最重要的参数:
    corePoolSize : 核心线程数线程数定义了最小可以同时运行的线程数量。
    maximumPoolSize : 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。
    workQueue: 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,信任就会被存放在队列中。
    ThreadPoolExecutor其他常见参数:

keepAliveTime:当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
unit : keepAliveTime 参数的时间单位。
threadFactory :executor 创建新线程的时候会用到。
handler :饱和策略。关于饱和策略下面单独介绍一下。

  • ThreadPoolExecutor 饱和策略,当线程已满,工作队列也满了的时候,会被调用。被用来实现各种拒绝策略。
    • ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
    • ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。影响程序的整体性能。
    • ThreadPoolExecutor.DiscardPolicy: 不处理新任务,直接丢弃掉。
    • ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。

你可能感兴趣的:(java,面试,集合,数据结构,线程)