base:北京
到店事业群-平台技术部
2021.8.22 笔试
五道编程题。
2021.8.25 美团一面
1.自我介绍
我是某某,来自某地,就读于某学校。
2.Spring AOP底层原理
实现AOP的主要设计模式就是动态代理。
Spring的动态代理有两种:一是JDK的动态代理;另一个是cglib动态代理。
JDK动态代理的两个核心接口(类)分别是InvocationHandler和Proxy。
CGLIB动态代理的两个核心接口(类)分别是MethodInterceptor和Enhancer,可以代理类和接口。
对AOP的理解:
系统是由许多不同的组件所组成的,每一个组件负责一块特定的功能。除了实现自身核心功能之外,这些组件还经常承担着额外的职责。例如日志、事务管理和安全这样的核心服务经常融入到自身具有核心业务逻辑的组件中去。这些系统服务经常被成为横切关注点,因为它们会跨越系统的多个组件。当我们需要为分散的对象引入公共行为的时候,OOP则显得无能为力。也就是说,OOP允许定义从上到下的关系。例如日志功能。日志代码往往水平的散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。在OOP设计中,它导致大量的代码重复,不利于各个模块的重用。
AOP:将程序中的交叉业务逻辑(比如安全、日志、事务)等封装成一个切面,然后注入到目标对象(具体的业务逻辑)中去。AOP可以对某个对象的功能进行增强,比如对象中的方法进行增强,可以在执行某个方法之前额外的做一些事情。
3.HashMap
HashMap的特性:
(1)HashMap存储键值对实现快速存取,允许为null。key值不可重复,若key值重复则覆盖。
(2)非同步,线程不安全。
(3)底层是hash表,不保证有序(比如插入的顺序)。
3.1 底层数据结构
基于hashing的原理,jdk8后采用数组+链表+红黑树的数据结构。我们通过put和get存储和获取对象。当我们给put()方法传递键和值时,先对键做一个hashCode()的计算来得到它在bucket数组中的位置来存储Entry对象。当获取对象时,通过get获取到bucket的位置,再通过键对象的equals()方法找到正确的键值对,然后在返回值对象。
3.2 如何扩容
HashMap通过resize()方法进行扩容。扩容需要重新分配一个新数组,新数组是老数组的2倍长,然后遍历整个老结构,把所有的元素挨个重新hash分配到新结构中去。底层数据结构用到了数组,到最后会因为容量问题都需要进行扩容操作
4.ConcurrentHashMap
用到的数据结构是数组+链表+红黑树
4.1 如何实现线程安全
jdk1.7:Segment+HashEntry来进行实现的;
jdk1.8:放弃了Segment臃肿的设计,采用Node+CAS+Synchronized来保证线程安全;
4.2 size()方法是加锁的吗?如何实现的(baseCount+counterCells)
(1)无论是JDK1.7还是JDK1.8中,ConcurrentHashMap的size()方法都是线程安全的,不需要加锁,都是准确的计算出实际的数量,但是这个数据在并发场景下是随时都在变的。
(2)在1.8中ConcurrentHashMap的get操作全程不需要加锁,这也是它比其他并发集合比如hashtable、用Collections.synchronizedMap()包装的hashmap,安全效率高的原因之一。
(3)get操作全程不需要加锁是因为Node的成员val是用volatile修饰的和数组用volatile修饰没有关系。
(4)数组用volatile修饰主要是保证在数组扩容的时候保证可见性。
5.线程池参数
(1) corePoolSize:核心线程数的大小
(2) maximumPoolSize:最大线程数的大小
(3) keepAliveTime:线程的空闲时间
(4) TimeUnit:空闲时间的单位
(5) workQueue:阻塞队列
(6) threadFactory:线程工厂
(7) Handler:拒绝策略
6.线程池大小如何设置
如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1
如果是IO密集型任务,参考值可以设置为2*NCPU
当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进