https://www.nowcoder.com/discuss/108888?type=2&order=0&pos=45&page=1
一、Spring IOC和AOP MVC流程
IOC:控制反转
控制:创建对象的控制权
反转:交给spring去创建管理
DI:依赖注入
将创建好的对象通过注入维护关系
借助于第三方实现具有依赖关系的对象之间的解耦,比如四个齿轮是四个类互相依赖才能转动,第三方就是在四个齿轮中间加齿轮,转动中间的齿轮即可
当A依赖于B时,在A初始化的时候必须手动创建对象B
有IOC容器后,A与B会解耦,在A运行需要对象B时才会创建对象B
A获得依赖对象B的过程,由主动变为了被动过程,控制权反转了,所以就叫控制反转
maven:项目管理工具,比如不用每次使用jar包去导入,会自动导入有依赖的jar包
bean:创建对象
id:对象名
class:类的路径,用于让Spring去创建对象
scope:使用什么模式创建对象
singleton:单例
prototype:多例
AOP:面向切面编程
实现一般是基于代理模式,可以分离系统的业务逻辑和系统服务,并在其中加入一些功能比如日志信息、拦截器。
通知:
before:在方法执行之前调用
after:之后,无论是否执行成功
after-returning:成功之后通知
after-throwing:抛出异常后通知
around:之前+之后
try{
前置通知
目标方法
后置通知
}catch(){
异常通知
}finally{
最终通知
}
反射:根据给出的类名(字符串方式)来动态的生成对象
MVC流程
用户发送信息到dispatcherServlet前端控制器,加载xml配置文件
找到handlerMapper处理器映射器,进行路径映射结果返回到dispatcherServlet
通过路径映射找到handlerAdapter处理器适配器,通过处理器运行
返回一个ModelAndView对象到handlerAdapter再到dispatcherServlet
通过ViewReslover视图分析返回一个View对象到dispatcherServlet
将数据放到request域中让页面加载
二、HashSet介绍一下
底层实现基本就是直接创建HashMap,调用HashMap中的方法。与HashMap不同的地方是
1、只存储元素,hashmap存储键值对
2、放入HashSet中的元素一般都要重写hashcode方法,通过hashcode判断之后再调用equals判断
3、在放入新元素时,HashSet将元素作为key放入hashmap中,如果放入相同的元素,新的value会覆盖原来的value,看起来就是没有变化
这也满足了元素不重复的特性。
4、set内部无序,没有get方法,只能迭代获得元素
TreeSet
和set的区别是存放元素是有序的(按关键字大小排序,不是插入顺序),元素也不能重复,实现自己排序可以重写compare方法
三、HashMap介绍一下
底层是数组+链表,通过对key的hashcode进行搅动运算处理后将key-value键值对存储到数组中,如果hashcode值和key相等,就会
直接覆盖,不相同就会存储到当前数组位置的链表中。不过如果链表越来越长查询效率就会低,JDK1.8之后如果链表长度大于8链表
就会转换为红黑树,以减少搜索时间。不是线程安全的。
底层用到的几个参数:
容量,默认值为16
填充因子,默认值为0.75,控制元素存放的疏密程度,越接近1越密集,默认值0.75是一个比较好的临界值
大小临界值=填充因子 x 原定长度,如果当前大小大于这个长度就要考虑扩容
底层用到的几个方法:
put():添加元素,添加前需要判断
1、如果hashcode定位到的位置没有元素,就直接插入元素
2、如果有元素,判断key值是否相同,相同直接覆盖value值,不同再判断是否为树节点,如果是就调用方法插入树中,不是就遍历插入链表末尾,jdk1.7是采用头插法,将元素插到首部
get():通过key找到元素,其中会判断是数组或者链表或者树,并按照各自的方法遍历
resize():扩容数组,会将之前数组的元素复制到新数组中再hash比较费时间,如果当前大小大于临界值也就是初始大小 x 扩充因子时,就会考虑调用此方法。
1、判断有没有超过Integer的最大值,超过就不再扩充,没超过就将临界值扩充为原来的2倍
2、把原来的元素都放在新数组中,新数组的位置是原来的hash值&新长度-1,长度最好为2的倍数,因为减1之后必定为111这种,
可以不浪费空间保证hashcode的均匀分布。所以hashmap的长度都是2的幂
四、对线程池的了解
线程池可以有很多用处:
1、在要完成任务时可以用创建好的线程去完成,节省了时间
2、将所有的线程统一管理
3、提高稳定性
底层有几个参数:
corePoolSize:核心线程池大小
keepAliveTime:在线程空闲时线程的驻留时间,默认在当前线程池大小大于核心线程池时才会起作用,也可以通过调用方法使得
当前线程池大小不够核心线程池大小时也能起作用。
maximumSize:线程池最大容量
WorkQueue:任务缓存阻塞队列
threadFactory:线程工厂,用来创建线程。
handler:表示拒绝任务的策略
几个状态:
RUNNING:初始化,会接受新任务并处理阻塞队列中任务
SHUTDOWN:不接受新任务,会等待阻塞队列所有任务完成
STOP:不接受新任务也不会处理阻塞队列任务,且会尝试终止当前运行中的任务
TEAMINATED:在SHUTDOWN或者STOP状态,且所有的工作进程已经销毁,任务队列也清空的状态
几个方法:
executor():将任务交给线程
submit():也是向线程提交一个任务,不同的是会返回任务的返回值,底层调用的还是execute方法
shutdown():线程处于SHUTDOWN状态,不再接受新任务,会等待当前任务队列所有任务完成。
shutdownNow():线程处于STOP状态,不再接受新任务,并会去尝试终止当前任务
五、对synchronized的了解
是java关键字,解决的是多个线程之间的同步性,可以保证被修饰的代码块或方法等在任意时刻只有一个线程在运行
作用的方式:
1、修饰实例方法,作用于当前对象实例,进入同步代码块之前要获得当前对象实例的锁
2、修饰静态实例方法,作用于当前类对象加锁,进入同步代码块前要获得当前类对象的锁,就是给当前类加锁
3、修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定的锁
具体使用举例:
双重校验锁单例模式:
public class Singleton{
private volatile static Singleton singleton=null;
private Singleton(){}
public static Singleton getInstance(){
if(singleton==null){
synchronized(Singleton.class){
if(singleton==null){
singleton=new Singleton();
}
}
}
return singleton;
}
}
六、对volatile的了解
防止指令重排
比如在执行创建对象实例时JVM虚拟机会分好几步进行,而且可能会将这几步打乱重排。
1、给singleton开辟内存空间
2、初始化该内存空间
3、将singleton指向该空间
打乱顺序后在多线程中可能会出错,比如线程1先执行1和3,线程2进来发现singleton已经指向了一个内存空间不为空就会直接返回,但是
此时还未初始化
具体操作是读写操作之前加相应的内存屏障,这样就会阻止指令重排
底层操作保持可见性
在JMM内存模型中,如果存储或操作一个静态变量,是先将数据存入到主内存中,线程再读入到工作内存中。
如果线程A对变量进行修改,会读取主内存的数据,修改之后再传到主内存。
此时如果线程B读取这个变量,可能读取到旧值也可能读到修改过的值,因为线程A要有一个将修改后变量传到主内存的时间。
这时就会读取出错,但是直接加锁又会影响系统性能,就用到了volatile关键字,可以被修饰的变量对所有线程的可见性。
因为有先行原则。即对一个volatile变量的写操作必定先行发生于后面的读操作。
不能保证原子性
比如一个数进行一个自增操作,但是底层操作时会分成四步:读取、定义、增加、同步到主内存。
如果只使用volatile关键字,线程可能执行这四步中的任何一步,比如别的线程已经自增了很多次,另一个线程才进行定义,就会更改结果并存到主内存造成错误。
什么时候适合用volatile关键字
1、运行结果不依赖变量的当前值,或者确保只有单一的线程修改变量的值。
2、变量不需要与其他的状态变量共同参与不变约束(比如改变变量大小之后再参与条件判断,可能发生数据读取错误导致判断出错)