目录
12.notify()和 notifyAll()有什么区别?
14. runnable 和 callable 有什么区别?
16.线程池中 submit()和 execute()方法有什么区别?
18.String、StringBuffer和StringBuilder的区别
3.活锁死锁问题分析,什么是活锁,什么是死锁?出现的原因是什么?如何避免?
6.HashMap的底层详细介绍下?(1.7和1.8前后区别)
7.ArrayList和LinkdList的区别?(使用场景和特性分析)
8.ConCurrentHashMap和HashMap的区别,分段锁机制?(理论)
11.介绍下Spring框架。(框架简介、框架核心、框架特性等)
12.介绍下Spring框架IOC和AOP的实现原理。(理论)
IOC(Inverse of Control):控制反转,也可以称为依赖倒置。
13.Spring框架中beanFactory和FactoryBean有什么区别?
16.阐述下security框架的授权以及认证流程(带入项目中角色管理模块的实现思路,来阐述此题)
17.Redis的常见数据类型有哪些?(带入具体类型具体使用场景)
18.Redis的缓存雪崩和击穿如何解决,穿透问题遇到过吗?(带入具体的项目场景)
19.数据库中有2000w条数据,而Redis当中只有20w条数据,如何保证Redis中的数据一定是热点数据?(可考虑设置缓存过期时间)
20.Spring框架中出现的ABA问题(循环依赖)如何解决?(原理,缓存思路)
22.SpringMVC框架的工作原理(带入项目中功能请求来阐述,例如登录请求、查询请求)
23.SpringMVC框架从发起请求到接收请求发生了什么?(网络相关)
24.MyBatis框架如何处理sql注入问题?(MyBatis中#和$符号区别)
25.MyBatis框架有几级缓存,默认开启几级?(理论性)
27.MySql中索引有几种,索引怎么使用?(带入具体的项目场景)
28.什么是事务?事务的特性?MySql中事务的隔离级别?(理论内容)
一、面试题
1.SpringMVC的流程?
- 用户发送请求至前端控制器DispatcherServiet;
- 前端控制器收到请求后,调用处理器映射器HandlerMapping,请求获取Handler;
- 处理器映射器根据请求URL找到具体的处理器Handler,生成处理器对象及处理器拦截器(如果有则生成),一并返回给前端控制器;
- 前端控制器调用处理器适配器HandlerAdapter,请求执行Handler;
- 处理器适配器经过适配调用具体处理器进行处理业务逻辑;
- 处理器执行完成返回ModelAndView;
- 处理器适配器将处理器结果ModelAndView返回给前端控制器;
- 前端控制器将ModelAndView传给视图解析器ViewResolver进行解析;
- 视图解析器解析后返回具体视图View;
- 前端控制器对View进行渲染视图(即将模型数据填充至视图中);
- 前端控制器响应用户。
2.SpringMVC怎么设定 重定向 和 转发 的?
- 重定向:在返回值前面加“redirect”,譬如“redirect:http://www.baidu.com”
- 转发:在返回值前面加“forward”,譬如“forward:user.do?name=method4”
-
转发与重定向的区别
1.地址栏
转发:不变,不会显示出转向的地址
重定向:会显示转向之后的地址2.请求
转发:一次请求
重定向:至少提交了两次请求3.数据
转发:对request对象的信息不会丢失,因此可以在多个页面交互过程中实现请求数据的共享
重定向:request信息将丢失4.原理
转发:是在服务器内部控制权的转移,是由服务器区请求,客户端并不知道是怎样转移的,因此客户端浏览器的地址不会显示出转向的地址。
重定向:是服务器告诉了客户端要转向哪个地址,客户端再自己去请求转向的地址,因此会显示转向后的地址,也可以理解浏览器至少进行了两次的访问请求。
3.SpringMVC常用的注解有哪些?
- @RequestMapping:用于处理请求url映射的注解,可用于类或方法上,用于类上,则表示类中的所有响应请求的方法都是以该地址作为父路径。
- @Controller:用于标记在一个类上,使用它标记的类就是一个SpringMVC Controller 对象。分发处理器将会扫描使用了该注解的类的方法,并检测该方法是否使用了@RequestMapping 注解。@Controller 只是定义了一个控制器类,而使用@RequestMapping 注解的方法才是真正处理请求的处理器。
@Controller 标记在一个类上还不能真正意义上的说它就是SpringMVC 的一个控制器类,因为这个时候Spring 还不认识它。这个时候就需要我们把这个控制器类交给Spring 来管理。有两种方式可以管理:<!--方式一--> <bean class="com.cqvie.handler.HelloWorld"/> <!--方式二--> < context:component-scan base-package = "com.cqvie" /> <!-- 路径写到controller的上一层 -->
-
@PathVariable:用于将请求URL中的模板变量映射到功能处理方法的参数上,即取出uri模板中的变量作为参数。
-
@Resource和@Autowired:都是做bean的注入时使用,其实@Resource并不是Spring的注解,它的包是javax.annotation.Resource,需要导入,但是Spring支持该注解的注入。
- @RequestBody:注解实现接收http请求的json数据,通过添加导入的jason或其他依赖将json转换为java对象。
- @ResponseBody:注解实现将controller方法返回对象转化为json对象响应给客户。
4.Equal 和 == 区别
-
==运算符的使用
(1)可以使用在基本数据类型变量和引用数据类型变量中 。
(2)如果比较的是基本数据类型变量:比较两个变量保存的数据是否相等。(不一定类型要相同)
(3)如果比较的是引用数据类型变量:比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体。
(4)补充:== 符号使用时,必须保证符号左右两边的变量类型一致。
-
equals()方法的使用
(1)是一个方法,而非运算符
(2)只能适用于引用数据类型。
(3)Object类中equals()的定义:
public boolean equals(Object obj){
return (this == obj);
}
说明:Object类中定义的equals()和==的作用是相同的,比较两个对象的地址值是否相同,即两个引用是否指向同一个对象实体。
(4)像String、Date、File、包装类等都重写了Object类中的equals()方法。
不是比较两个引用的地址是否相同,而是比较两个对象的“实体内容”是否相同。
(5)通常情况下,我们自定义的类如果使用equals()的话,也通常是比较两个对象的"实体内容"是否相同。那么,我们就需要对Object类中的equals()进行重写。
重写的原则:比较两个对象的实体内容是否相同。
5.synchronized 和 lock 的异同
相同点:
-
都可以解决线程安全问题
不同点:
-
首先synchronized是java内置关键字,在jvm层面,Lock是个java类;
-
synchronized机制在执行完相应的同步代码以后,自动的释放同步监视器;lock需要手动的启动同步(lock()),同时结束同步也需要手动的实现(unlock());
- 用synchronized关键字的两个线程1和线程2,如果当前线程1获得锁,线程2线程等待。如果线程1阻塞,线程2则会一直等待下去,而Lock锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了;
-
synchronized有代码块锁和方法锁,lock只有代码块锁;
-
synchronized的锁可重入、不可中断、非公平,而Lock锁可重入、可中断、可公平(两者皆可);
-
Lock锁适合大量同步的代码的同步问题,synchronized锁适合代码少量的同步问题。
- 使用lock锁,JVM可以花费更少时间调度线程,性能更好。并且具有更好的扩展性(提供更多的子类)。
6.多线程中sleep()和wait()的异同
相同点:
- 一旦执行方法,都可以使得当前线程进入阻塞状态
不同点:
- sleep()申明在Thread类中,wait()申明在Object类中;
- 调用要求不同:sleep()可以在任何需要场景下调用;wait()必须使用在同步代码块或同步方法中。
- 如果两个方法都使用在同步代码块或同步方法中,sleep()不会释放同步监视器(锁),而wait()会。
7.创建多线程的方式
(1)继承Thread类
- 创建一个继承于Thread类的子类
- 重写Thread的run()方法 ——> 将此线程的方法声明在run()中
- 创建Thread类的子对象
- 通过此对象调用start()
class MyThread extends Thread{
//重写Thread类的run()
@Override
public void run() {
for(int i = 1;i < 100;i++){
if(i % 2 == 0){
System.out.println(i);
}
}
}
}
public class ThreadTest {
public static void main(String[] args) {
//3.创建Thread类的子对象
MyThread t1 = new MyThread();
//4.通过此对象调用start():①启动当前线程 ②调用当前线程的run()
t1.start();
//如下操作仍在main线程中执行的
for(int i = 1;i < 100;i++){
if(i % 2 == 0){
System.out.println(i + "***main()***");
}
}
}
}
(2)实现Runnable接口
- 创建一个实现了Runnable接口得类
- 实现类去实现Runnable中的抽象方法:run()
- 创建实现类的对象
- 将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
- 通过Thread类的对象调用start()
class MThread implements Runnable{
//2.实现类去实现Runnable中的抽象方法:run()
@Override
public void run() {
for(int i = 0;i < 100;i++){
if(i % 2 == 0){
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
public class ThreadTest1 {
public static void main(String[] args) {
//3.创建实现类的对象
MThread m1 = new MThread();
//4.将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象
Thread t1 = new Thread(m1);
//5.通过Thread类的对象调用start():①启动线程 ②调用当前线程的run() --> 调用了Runnable类型的target的run()
t1.start();
//再启动一个线程,遍历100以内的偶数
Thread t2 = new Thread(m1);
t2.setName("线程2");
t2.start();
}
}
(3)实现Callable接口
- 创建一个实现Callable的实现类
- 实现call方法,将此线程需要执行的操作声明在call()中
- 创建Callable接口实现类的对象
- 将此Callable接口实现类的对象作为传递到FutureTask构造器中,创建FutureTask的对象
- 将FutureTask的对象作为参数传递到Thread类的构造器中,创建Thread对象,并调用start()
- 获取Callable中call方法的返回值,get()返回值即为FutureTask构造器参数Callable实现类重写的call()的返回值。
class NumThread implements Callable{
@Override
public Object call() throws Exception {
int sum = 0;
for(int i = 1;i <= 100;i++){
if(i % 2 == 0){
System.out.println(i);
sum += i;
}
}
return sum;
}
}
public class ThreadNew {
public static void main(String[] args) {
NumThread numThread = new NumThread();
FutureTask futureTask = new FutureTask(numThread);
new Thread(futureTask).start();
try {
Object sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
(4)创建线程池
-
newCachedThreadPool(),它是用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置时间超过60秒,则被终止并移除缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用SynchronousQueue作为工作队列。
private static void createCachedThreadPool() { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { final int index = i; executorService.execute(() -> { // 获取线程名称,默认格式:pool-1-thread-1 System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index); // 等待2秒 sleep(2000); }); } }
因为SynchronousQueue队列不保持它们,直接提交给线程,相当于队列大小为0,而最大线程数为Integer.MAX_VALUE,所以线程不足时,会一直创建新线程,等到线程空闲时,又有60秒存活时间,从而实现了一个可缓存的线程池。
-
newFixedThreadPool(int nThreads),重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有nThreads个工作线程是活动的。这意味着,如果任务数量超过了活动线程数目,将在工作队列中等待空闲线程出现;如果工作线程退出,将会有新的工作线程被创建,以补足指定数目nThreads。
private static void createFixedThreadPool() { ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { final int index = i; executorService.execute(() -> { // 获取线程名称,默认格式:pool-1-thread-1 System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index); // 等待2秒 sleep(2000); }); } }
因为核心线程数与最大线程数相同,所以线程池的线程数是固定的,而且没有限制队列的大小,所以多余的任务均会被放到队列排队,从而实现一个固定大小,可控制并发数量的线程池。
-
newSingleThreadExecutor(),它的特点在于工作线程数目限制为1,操作一个无界的工作队列,所以它保证了所有的任务都是被顺序执行,最多会有一个任务处于活动状态,并且不予许使用者改动线程池实例,因此可以避免改变线程数目。
private static void createSingleThreadPool() { ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i = 0; i < 10; i++) { final int index = i; executorService.execute(() -> { // 获取线程名称,默认格式:pool-1-thread-1 System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index); // 等待2秒 sleep(2000); }); } }
因为核心线程数与最大线程数相同,均为1,所以线程池的线程数是固定的1个,而且没有限制队列的大小,所以多余的任务均会被放到队列排队,从而实现一个单线程按指定顺序执行的线程池。
-
newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),创建的是个ScheduledExecutorService,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。
private static void createScheduledThreadPool() { ScheduledExecutorService executorService = Executors.newScheduledThreadPool(3); System.out.println(DateUtil.now() + " 提交任务"); for (int i = 0; i < 10; i++) { final int index = i; executorService.schedule(() -> { // 获取线程名称,默认格式:pool-1-thread-1 System.out.println(DateUtil.now() + " " + Thread.currentThread().getName() + " " + index); // 等待2秒 sleep(2000); }, 3, TimeUnit.SECONDS); } }
因为使用了延迟队列,只有在延迟期满时才能从中提取到元素,从而实现定时执行的线程池。而周期性执行是配合上层封装的其他类来实现的,可以看ScheduledExecutorService类的scheduleAtFixedRate方法。
-
newWorkStealingPool(int parallelism)简单翻译是任务窃取线程池,Java 8 才加入这个创建方法,其内部会构建ForkJoinPool,利用Work-Stealing算法,并行地处理任务,不保证处理顺序。
使用ForkJoinPool的好处是,把1个任务拆分成多个“小任务”,把这些“小任务”分发到多个线程上执行。这些“小任务”都执行完成后,再将结果合并。
当线程发现自己的队列没有任务了,就会到别的线程的队列里获取任务执行。可以简单理解为”窃取“。
一般是自己的本地队列采取LIFO(后进先出),窃取时采用FIFO(先进先出),一个从头开始执行,一个从尾部开始执行,由于偷取的动作十分快速,会大量降低这种冲突,也是一种优化方式。
①无参创建方式:public static ExecutorService newWorkStealingPool() { return new ForkJoinPool //Runtime.getRuntime().availableProcessors()是获取当前系统可以的CPU核心数。 (Runtime.getRuntime().availableProcessors(), ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true); }
②有参创建方式
public static ExecutorService newWorkStealingPool(int parallelism) { return new ForkJoinPool (parallelism, ForkJoinPool.defaultForkJoinWorkerThreadFactory, null, true); }
-
ThreadPoolExecutor类创建线程
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { // 省略... }
① 共7个参数如下:
(1)corePoolSize:核心线程数,线程池中始终存活的线程数。
(2)maximumPoolSize: 最大线程数,线程池中允许的最大线程数。
(3)keepAliveTime: 存活时间,线程没有任务执行时最多保持多久时间会终止。
(4)unit: 单位,参数keepAliveTime的时间单位,7种可选。
(5)workQueue: 一个阻塞队列,用来存储等待执行的任务,均为线程安全,7种可选。
较常用的是LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
(6)threadFactory: 线程工厂,主要用来创建线程,默及正常优先级、非守护线程。
(7)handler:拒绝策略,拒绝处理任务时的策略,4种可选,默认为AbortPolicy。
线程池的执行规则:
(1)当线程数小于核心线程数时,创建线程。
(2)当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
(3)当线程数大于等于核心线程数,且任务队列已满:
若线程数小于最大线程数,创建线程。
若线程数等于最大线程数,抛出异常,拒绝任务。
阿里代码规范《阿里巴巴Java开发手册》中明确不建议使用Executors类提供的这4种方法:
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 Executors返回的线程池对象的弊端如下: FixedThreadPool和SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。 CachedThreadPool和ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
所以我们应该使用ThreadPoolExecutor类来创建线程池,根据自己需要的场景来创建一个合适的线程池。
8.线程安全问题
(1)Runnable的线程安全问题
/**
* 例子:创建三个窗口卖票,总票数为100张.使用实现Runnable接口的方式
* 1.卖票过程中出现重票、错票 ---》出现了线程的安全问题
* 2.问题出现的原因:当某个线程操作车票的过程中,尚未操作完成时,其他线程参与进来,也操作车票
* 3.如何解决:当一个线程在操作ticket的时候,其他线程不能参与进来。直到线程a操作完ticket时,其他线程才可以操作ticket。这种情况即使线程a出现了阻塞,也不能被改变。
* 4.在java中,我们通过同步机制,来解决线程的安全问题。
* 5.同步的方式,解决了线程的安全问题。---好处
* 操作同步代码时,只能有一个线程参与,其他线程等待。相当于是一个单线程的过程,效率低。---局限性
*
* 方式一:同步代码块
* synchronized(同步监视器){
* //需要被同步的代码
* }
*
* 说明:1.操作共享数据的代码,即为需要被同步的代码 --->不能包含代码多了,也不能包含代码少了。
* 2.共享数据:多个线程共同操作的变量。比如:ticket就是共享数据
* 3.同步监视器,俗称:锁。任何一个类的对象,都可以来充当锁。
* 要求:多个线程必须要共用同一把锁。
*
* 补充:在实现Runnable接口创建多线程的方式中,我们可以考虑使用this充当同步监视器。
*
* 方式二:同步方法
* 如果操作共享数据的代码完整的声明在一个方法中,我们不妨将此方法声明同步的
*
*/
class Windows1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while(true){
synchronized (this) {//此时的this:唯一的windows1的对象
if (ticket > 0) {
try{
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为: " + ticket);
ticket--;
} else {
break;
}
}
}
}
}
public class WindowsTest1 {
public static void main(String[] args) {
Windows1 w = new Windows1();
Thread t1 = new Thread(w);
Thread t2 = new Thread(w);
Thread t3 = new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
/**
* 2.使用同步方法解决实现Runnable接口的线程安全问题
*
* 关于同步方法的总结:
* 1. 同步方法仍然涉及到同步监视器,只是不需要我们显式的声明。
* 2. 非静态的同步方法,同步监视器是:this
* 静态的同步方法,同步监视器是:当前类本身
*/
class Windows3 implements Runnable {
private int ticket = 100;
@Override
public void run() {
while (true) {
show();
}
}
public synchronized void show() { //方法上加synchronized
// synchronized (this){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ":卖票,票号为: " + ticket);
ticket--;
}
// }
}
}
public class WindowsTest3 {
public static void main(String[] args) {
Windows3 w3 = new Windows3();
Thread t1 = new Thread(w3);
Thread t2 = new Thread(w3);
Thread t3 = new Thread(w3);
t1.setName("窗