java面试题笔记
- 1.并发三要素
- 2.实现可见性的方法
- 3.创建线程的几种方式
- 4.创建线程的几种方式的优缺点
- 5.线程的流转状态
- 6.什么是线程池?有哪几种创建方式?
- 7.四种线程池的创建:
- 8.线程池的优点
- 9.常用的并发工具类有哪些
- 10.CyclicBarrier 和 CountDownLatch 的区别
- 11.synchronized 的作用?
- 12.volatile 关键字的作用
- 13.什么是CAS
- 14.CAS的问题
- 15.什么是future
- 16.什么是AQS
- 17.AQS 支持两种同步方式:
- 18.ReadWriteLock 是什么
- 19.FutureTask 是什么?
- 20.synchronized 和 ReentrantLock 的区别
- 21.什么是乐观锁和悲观锁
- 22.线程 B 怎么知道线程 A 修改了变量
- 23.synchronized、volatile、CAS 比较
- 24.sleep()和wait() 有什么区别
- 25.ThreadLocal 是什么?有什么用?
- 26.线程的调度策略
- 27.ConcurrentHashMap 的并发度是什么?
- 28.ConcurrentHashMap与HashMap有什么区别?
- 29.说一下ConcurrentHashMap的工作原理,put()和get()的工作流程是怎样的?
- 30.concurrentHashMap和hashTable的效率哪个更高?为什么?
- 31.ConcurrentHashMap在jdk1.8中为什么要使用内置锁Synchronized来替换ReentractLock重入锁?
- 32.concurrentHashMap的get()方法需要加锁吗
- 33.什么是重入锁?
- 34.ConcurrentHashMap中的key和value可以为null吗?为什么?
- 35.Java如何避免死锁
- 36.死锁的原因
- 37.怎么唤醒一个阻塞的线程
- 38.不可变对象对多线程有什么帮助
- 39.什么是多线程的上下文切换
- 40.如果你提交任务时,线程池队列已满,这时会发生什么?
- 41.Java中用到的线程调度算法是什么
- 42.什么是线程调度器和时间分片
- 43.什么是自旋
- 44.什么是java反射
- 45.什么是spring通知?
- 46.Spring通知(Advice)有哪些类型?
- 46.spring通知(Advice)执行的顺序
- 47.什么是深拷贝?什么是浅拷贝?
- 48.final、finally、finalize有什么区别?
- 49.equals和hashcode的关系
- 50.string,stringBuffer,stringbuild的区别
- 51.springboot的自动装配原理
- 52.mybatis的一级缓存和二级缓存
- 53.线程池的7大参数
- 54.java中有哪些引用类型
- 55.在Java中为什么不允许从静态方法中访问非静态变量?
- 56.什么是java的内存模型?
- 57.在Java中,什么时候用重载,什么时候用重写?
- 58.实例化对象有几种方式。
- 59.Collection 和 Collections 有什么区别?
- 60.list与Set区别
- 61.HashMap 和 Hashtable 有什么区别?
- 62.说一下 HashMap 的实现原理?
- 63.在 Queue 中 poll()和 remove()有什么区别?
- 64.哪些集合类是线程安全的?
- 65.迭代器 Iterator 是什么?
- 66.Iterator 和 ListIterator 有什么区别?
- 67.如何确保一个集合不会被修改
- 68.队列和栈是什么?有什么区别?
- 69.JVM 的架构是怎样的?
- 70. JVM 中的堆和栈有什么区别?
- 71.如何保证redis的数据和数据库一致
- 72.什么是微服务?
- 73.Spring Bean 生命周期的执行流程。
- 74. spring是如何解决循环依赖的问题?
- 75.三级缓存的作用是什么?
- 76.spring哪些情况下,不能解决循环依赖问题。
- 77.什么是死锁?
- 78.如何知道一个线程池执行完毕了?
- 79.HashMap是怎么解决哈希冲突的?
- 80.什么叫阻塞队列的有界和无界?
- 81.ConcurrentHashMap的实现原理是什么?
- 82.spring的事务传播特性
- 83.什么是聚集索引和非聚集索引
- 84.spring中有哪些方式可以把bean注入到IOC容器中
- 85.redis如何进行rdb持久化
- 86.redis如何进行AOF持久化
- 87.redis过期键的删除策略
- 88、jvm的架构
- 89、jvm的运行时数据区域
- 90、什么是JMM , 它解决的是什么问题?
- 91、详解单例模式的优缺点、注意事项以及使用场景
- 92、垃圾收集算法
- 93、解释下双亲委派机制
- 94、sychronized锁升级过程
- 95、如何判断一个对象是否被引用
- 96、如何设计一个秒杀系统
- 97、spring中的事务是如何实现的
- 98、@Autowired和@Resource注解的区别和联系
1.并发三要素
- 原子性
- 在一次或多次操作中,要么所有的操作都执行并且不会受其他因素干扰而中断,要么所有的操作都不执行
- 可见性
- 可见性指多个线程操作一个共享变量时,其中一个线程对变量进行修改后,其他线程可以立 即看到修改的结果。
- 有序性
- 有序性,即程序的执行顺序按照代码的先后顺序来执行。
2.实现可见性的方法
synchronized 或者 Lock:保证同一个时刻只有一个线程获取锁执行代码,锁释放之前把最新的值刷新到主内存,实现可见性。
3.创建线程的几种方式
- 继承thread类
- 实现runnable
- 实现callable
- 实现线程池
4.创建线程的几种方式的优缺点
- 继承thread接口比较方便
- 实现runnable、callable接口,还可以继承其他类,更加的灵活,但是runnable接口不能有返回值,不能捕获异常。
- callable的实现接口是call,可以有返回值,可以捕获异常。
- 运行 Callable 任务可以拿到一个 Future 对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过 Future 对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
5.线程的流转状态
- 新建状态
- 当线程对象创建后,就进入了新建状态。
- 准备状态
- 当调用线程的start()方法时,线程就进入准备状态,处于准备状态的线程,只能说做好了准备,随时等待CPU的调度,并不是说执行了start()就立即去会被CPU执行。
- 运行状态
- 当CPU开始调度正在准备的线程时,此时线程才得以真正执行,即进入到运行状态。
- 阻塞状态
处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才有机会再次被CPU调用进入到运行状态,根据阻塞产生的原因不同,阻塞状态又可以分为三种- 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态。
- 同步阻塞:线程在获取synchronize同步锁失败(因为其他线程占用),它会进入同步阻塞状态;
- 其他阻塞:通过调用线程的sleep()或join()发出了I/O请求时,线程会进入到阻塞状态,当sleep()状态超时时、join()等待线程终止或者超时,或者I/O处理完毕时,线程重新转入就绪状态。
- 死忙状态
线程执行完或者因为异常退出run()方法,该线程结束生命周期。
6.什么是线程池?有哪几种创建方式?
线程池就是提前创建若干个线程,如果有任务需要处理,线程池里面的线程就会处理任务,处理完之后线程并不会被销毁,而是等待下一个任务,由于创建和销毁线程是消耗系统资源的,所以当你想要频繁的创建和销毁小城的时候,就可以考虑使用线程池来提升系统的性能,Java提供了一个Java.util.concurrent.Executor接口的实现用于创建线程池。
7.四种线程池的创建:
- newCachedThreadPool 创建一个可缓存线程池
- newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数。
- newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。
- newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务。
8.线程池的优点
- 重用存在的线程,减少了对象的创建销毁的开销
- 可有效的控制最大并大线程数,提高系统资源的使用效率,同时避免过多资源竞争,避免阻塞。
- 提供定时执行,定期执行,单线程,并发数控制等功能。
9.常用的并发工具类有哪些
- CountDownLatch
- 主线程等待所有子线程执行完毕后,再去执行后续代码。
- CyclicBarrier
- 用于协调多个线程同步执行操作的场合,所有线程等待完成,然后一起做事情( 相互之间都准备好,然后一起做事情 )
- Semaphore
- 使用场景是限制一定数量的线程能够去执行
- Exchanger
- 顾名思义就是用来做交换的。这里主要是两个线程之间交换持有的对象。
10.CyclicBarrier 和 CountDownLatch 的区别
- CountDownLatch 简单的说就是一个线程等待,直到他所等待的其他线程都执行完成并且调用 countDown()方法发出通知后,当前线程才可以继续执行。
- cyclicBarrier 是所有线程都进行等待,直到所有线程都准备好进入 await()方法之后, 所有线程同时开始执行!
- CountDownLatch 的计数器只能使用一次。而 CyclicBarrier 的计数器可以使用 reset() 方法重置。所以 CyclicBarrier 能处理更为复杂的业务场景,比如如果计算发生错误,可以重置计数器,并让线程们重新执行一次。
- CyclicBarrier 还提供其他有用的方法, 比如 getNumberWaiting 方法可以获得CyclicBarrier 阻塞的线程数量。isBroken 方法用来知道阻塞的线程是否被中断。如果被中断返回 true,否则返回 false。
11.synchronized 的作用?
在 Java 中,synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制synchronized 代码段不被多个线程同时执行。synchronized 既可以加在一段代码上,也可以加在方法上。
12.volatile 关键字的作用
Java 提供了 volatile 关键字来保证可见性,当一个共享变量被volatile修饰的时候,他会保证修改的值会立马更新到主存中,当有其他线程去读取时,会从主存中直接读取。从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性。
13.什么是CAS
CAS是compare and swap的缩写,就是我们说的比较交换。
CAS是一种基于锁的操作,而且是乐观锁,CAS操作包含了三个操作,内存位置(V),预期原值(A)和新值(B),如果内存地址中的值和A值一样,则会更新成新值B,CAS 是通过无限循环来获取数据的,如果在第一轮循环中,a线程获取地址里面的值被B修改了,那么A线程就会自旋,到下次循环才会有可能执行。
14.CAS的问题
- CAS 容易造成 ABA 问题
一个线程A将数值修改成了B,然后又被修改成了A,此时CAS认为是没有变化,其实已经发生变化了,而解决这个问题的方案可以使用版本号,每一次操作都+1。 - 不能保证代码块的原子性
CAS 机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证 3 个变量共同进行原子性的更新,就不得不使用 synchronized 了。 - CAS 造成 CPU 利用率增加
之前说过了 CAS 里面是一个循环判断的过程,如果线程一直没有获取到状态,cpu 资源会一直被占用。
15.什么是future
在并发中,我们经常用到非阻塞的模型,在之前多线程的三种实现中,不管是继承thread还是实现runnable接口,都无法保证获取到之前的执行结果,通过实现callable接口来,并用future来接受多线程的执行结果。Future 表示一个可能还没有完成的异步任务的结果,针对这个结果可以添加 Callback 以便在任务执行成功或失败后作出相应的操作。
16.什么是AQS
AQS 是AbustactQueuedSynchronizer 的简称,它是一个 Java 提高的底层同步工具类,用一个 int 类型的变量表示同步状态,并提供了一系列的 CAS 操作来管理这个同步状态。AQS 是一个用来构建锁和同步器的框架,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 ReentrantLock,Semaphore,其他的诸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基于 AQS 的。
17.AQS 支持两种同步方式:
- 独占式
- 共享式
这样方便使用者实现不同类型的同步组件,独占式如 ReentrantLock,共享式如 Semaphore, CountDownLatch,组 合 式 的 如 ReentrantReadWriteLock。总之,AQS 为使用提供了底层支撑,如何组装实现,使用者可以自由发挥。
18.ReadWriteLock 是什么
首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D 也在读数据,读数据是不会改变数据的, 没有必要加锁, 但是还是加锁了, 降低了程序的性能。因为这个, 才诞生了读写锁ReadWriteLock 。 ReadWriteLock 是 一 个 读 写 锁 接 口 , ReentrantReadWriteLock 是ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的, 读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
19.FutureTask 是什么?
这个其实前面有提到过,FutureTask 表示一个异步运算的任务。FutureTask 里面可以传入一个 Callable 的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于 FutureTask 也是 Runnable 接口的实现类,所以FutureTask 也可以放入线程池中。
20.synchronized 和 ReentrantLock 的区别
synchronize是关键字,ReentrantLock是一个类,这是二者本质的区别,既然ReentrantLock是类,那么他就提供了比synchronize更加灵活的特性,可以被继承,可以有方法,可以有各种各样的类变量,ReentrantLock 比 synchronized 的扩展性体现在几点上:
- ReentrantLock 可以对获取锁的等待时间进行设置,这样就避免了死锁
- ReentrantLock 可以获取各种锁的信息
- ReentrantLock 可以灵活地实现多路通知
21.什么是乐观锁和悲观锁
- 乐观锁:就像它的名字一样,对于并发间操作产生的线程安全问题持乐观状态,乐观锁认为竞争不总 是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
- 还是像它的名字一样,对于并发间操作产生的线程安全问题持悲观状态,悲观锁认为竞争总 是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像 synchronized,不管三七二十一,直接上了锁就操作资源了。
22.线程 B 怎么知道线程 A 修改了变量
- 使用valatile修饰变量。
- 使用synchronize修饰改变变量的方法。
- 使用wait()/notify()
23.synchronized、volatile、CAS 比较
- synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
- volatile 提供多线程共享变量可见性和禁止指令重排序优化。
- CAS 是基于冲突检测的乐观锁(非阻塞)
24.sleep()和wait() 有什么区别
- sleep()让线程停止不会释放锁,wait()会释放锁
- sleep时间到了之后,会直接执行线程,wait会去重新等待,重新竞争锁。
- sleep是thread类的方法,wait是object的方法。
25.ThreadLocal 是什么?有什么用?
ThreadLocal 是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用, 特别适用于各个线程依赖不通的变量值完成操作的场景。简单说 ThreadLocal 就是一种以空间 换 时 间 的 做 法 , 在 每 个 Thread 里 面 维 护 了 一 个 以 开 地 址 法 实 现 的
ThreadLocal.ThreadLocalMap,把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。
26.线程的调度策略
- 线程体中调用了 yield 方法让出了对 cpu 的占用权利
- 线程体中调用了 sleep 方法使线程进入睡眠状态
- 线程由于 IO 操作受到阻塞
- 另外一个更高优先级线程出现
- 在支持时间片的系统中,该线程的时间片用完
27.ConcurrentHashMap 的并发度是什么?
concurrentHashMap的并发度就是segment的大小,默认是16,这意味着最多同时可以有16条线程操作concurrentHashMap,这也是ConcurrentHashMap对hashtable的最大优势,任何情况下,hashtable能同时有两条线程获取hashtable中的数据。
28.ConcurrentHashMap与HashMap有什么区别?
- hashmap不是线程安全的,而concurrentHashMap是线程安全的
- concurrentHashMap采用锁分段技术,将整个hash进行了分段segment,也就是将这个大的数组分成了几个小的片段segment,而且每个小的片段segment上面都是有锁存在的,那么在插入元素的时候就需要先找到应该插入到哪一个片段segment,然后再在这个片段上面进行插入,而且这里面还需要获取segment锁。
- concurrentHashMap让锁的力度更加精细一些,并发性能更好。
29.说一下ConcurrentHashMap的工作原理,put()和get()的工作流程是怎样的?
存储对象时,将key和value传给put()方法:
- 如果没有初始化,就调用inItTable()方法对数组进行初始化
- 如果没有hash冲突则直接通过CAS进行无锁插入
- 如果需要扩容,就先进行扩容,扩容为原来的两倍
- 如果存在hash冲突,就通过加锁的方式进行插入,从而保证线程安全。(如果是链表就按照尾插法插入,如果是红黑树就按照红黑树的数据结构进行插入)
- 如果达到链表转红黑树条件,就将链表转为红黑树;
- 如果插入成功就调用addCount()方法进行计数并且检查是需要扩容;
注意:在并发情况下concurrentHashMap会调用多个工作线程一起帮助扩容,这样效率会更高。
30.concurrentHashMap和hashTable的效率哪个更高?为什么?
concurrentHashMap的效率要高于hashtable,因为hashtable是使用一把锁锁住整个链表结构从而实现线程安全。而ConcurrentHashMap的锁粒度更低。在JDK1.7中采用分段锁实现线程安全,在jdk1.8中采用CAS(无锁算法)+synchronized实现线程安全。
31.ConcurrentHashMap在jdk1.8中为什么要使用内置锁Synchronized来替换ReentractLock重入锁?
- 锁粒度降低了
- 官方对synchronized进行了优化和升级,使得synchronized不那么“重”
- 在大数据量的操作下,对基于API的ReentractLock进行操作会有更大的内存开销
32.concurrentHashMap的get()方法需要加锁吗
不需要,get操作可以无锁是由于node的元素val和指针next是使用volatile修饰的,在多线程环境下线程A修改节点的val或者新增节点的时候是对线程B可见。
33.什么是重入锁?
对于同一个线程,如果连续两次对同一把锁进行lock,那么这个线程会被卡死在那里,所以重入锁就是用来解决这个问题的,重入锁使同一个线程可以对同一把锁在不释放的前提下,反复的加锁不会导致线程卡死,唯一的一点就是需要保证unlock()和lock()的次数要一致。
34.ConcurrentHashMap中的key和value可以为null吗?为什么?
不可以,因为源码中判断进行put()操作的时候如果key为null或者value为null,会抛出NullPointerException空指针异常。
如果concurrentHashMap中存在一个key对应的value是null,那么当调用map.get(key)的时候,会返回null,那么这个null就有两个意思
- 这个key从来没有在map中映射过,也就是不存在这个key
- 这个key是真实存在的,只是在设置key的value值的时候,设置了null
这个二义在hashmap中可以通过map.containsKey(key)方法来判断,如果返回是TRUE,说明key存在只是value是null,如果返回false,说明这个key是不存在,这就是说为什么hashmap是可以有为null的键值,但是concurrentHashMap只用这个判断是判断不了的,为什么会判断不了,假设现在有AB两个线程,然后现在有一个key不存在concurrentHashMap中,线程A通过ConcurrentHashMap.containsKey(key)方法去做一个判断,我们期望的返回值是false,但是恰巧在A线程get(key)之后,调用constainsKey(key)方法之前B线程执行了ConcurrentHashMap.put(key,null),那么当A线程去执行containsKey(key)方法之后我们得到的结果是TRUE,与我们预期的结果就不相符了。
35.Java如何避免死锁
Java中的死锁是一种变成情况,其中两个或多个线程被永久阻塞,Java死锁情况出现至少两个线程和两个或者更多资源。Java发生死锁的根本原因是:在申请琐时发生了交叉闭环申请。
36.死锁的原因
- 是多个线程涉及到多个锁,这些锁存在着交叉,所以可能会导致一个锁依赖的闭环。例如:线程在获取锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获取B,在释放锁B之前又要先获取锁A,因此闭环就发生了,陷入死锁循环。
- 默认的锁申请操作sahib阻塞的,所以要避免死锁,就需要在遇到多个对象锁交叉的情况下,就要仔细审查这几个对象的类中的所有方法,是否存在着导致锁依赖的环路的可能性,总之是尽量避免在一个同步方法中调用其他对象的延时方法和同步方法。
37.怎么唤醒一个阻塞的线程
如果线程是因为调用了wait()、sleep()或者join()方法导致的阻塞,可以中断线程,并且通过抛出InterruptedException 来唤醒它,如果是线程遇到了io阻塞,那就无能为力了,因为io是操作系统实现的,Java代码并没有办法直接接触到操作系统。
38.不可变对象对多线程有什么帮助
前提有提到过一个问题,不可变对象保证了对象的内存可见性,对不可变对象的读取不需要进行额外的同步手段,提升了代码执行效率。
39.什么是多线程的上下文切换
多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过度。
40.如果你提交任务时,线程池队列已满,这时会发生什么?
- 如果选择的是无界队列 LinkedBlockingQueue,也就是无界队列的话,没欢喜,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue 可以近乎认为是一个无穷大的队列,可以无限存放任务
- 如果使用的是有界队列,比如 ArrayBlockingQueue ,任务首先会被添加到 ArrayBlockingQueue 中, ArrayBlockingQueue 满了会根据maximumPoolSize的值增加线程数量,如果增加线程数量还是处理不过来, ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy。
41.Java中用到的线程调度算法是什么
抢占式。一个线程用完CPU之后,操作系统会根据线程优先级,线程饥饿程度等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
42.什么是线程调度器和时间分片
线程调度器是一个操作系统服务,他负责为runnable状态的线程分配CPU时间,一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现,时间分片是指将可用的cpu时间分配给可用的runnable线程的过程,分配CPU时间可以基于线程优先级或者线程等待时间,线程调度并不受Java虚拟机控制,所以由应用程序控制它更好的选择(也就是说不要让你的程序依赖于线程的优先级)。
43.什么是自旋
很多 synchronized 里面的代码只是一些很简单的代码,执行时间非常快,此时等待的线程都加锁可能是一种不太值得的操作,因为线程阻塞涉及到用户态和内核切换的问题,既然synchronized 里面的代码执行得非常快, 不妨让等待锁的线程不要被阻塞, 而是在synchronized 的边界做忙循环,这就是自旋。如果做了多次忙循环发现还没有获得锁,再阻塞,这样可能是一种更好的策略。
44.什么是java反射
Java反射是指在运行时动态获取类的信息并操作类的方法、属性等。通过反射,可以在程序运行时获取类的信息,包括类名、方法名、属性等,并且可以在运行时动态地创建对象、调用方法、访问属性等。Java反射提供了一种机制,使得我们可以在编程过程中动态地操作Java对象,而不必在编译时就确定对象的类型。这种动态性使得Java程序的灵活性更高,同时也为一些框架和工具的实现提供了基础。
45.什么是spring通知?
在Spring AOP中,通知(Advice)是一种增强(interceptor),它在目标对象的方法调用前、调用后或发生异常时进行拦截,从而在目标对象的方法执行前后添加自己的代码逻辑,实现代码的横向扩展。
Spring框架提供了5种通知类型:Before通知、After returning通知、After throwing通知、After finally通知和Around通知。Before通知是在目标对象的方法执行前执行的通知,常用于进行一些前置处理,如设置字符集、权限验证等。After returning通知是在目标对象的方法正常执行完毕后执行的通知,常用于进行一些后置处理,如记录日志、清除缓存等。After throwing通知是在目标对象的方法抛出异常时执行的通知,常用于进行异常处理,如发送邮件、记录日志等。After finally通知是在目标对象的方法执行完毕之后执行的通知,无论是否发生异常都会执行,常用于进行清理工作。Around通知是在目标对象的方法执行前、执行后或发生异常时执行的通知,常用于进行复杂的横切逻辑,如事务管理、性能统计等。
使用Spring的通知机制,我们可以将横切逻辑与目标对象的业务逻辑分离开来,从而实现模块化、高内聚低耦合的代码结构,提高代码的健壮性和可维护性。
46.Spring通知(Advice)有哪些类型?
在Spring AOP中,通知(Advice)是指在目标方法织入的横切逻辑。Spring框架中提供了以下5种通知类型:
-
Before通知(Before advice):在目标方法之前执行的通知。Before通知可以修改方法的参数值。
-
After returning通知(After returning advice):在目标方法返回一个结果后执行的通知。After returning通知不能修改方法的结果。
-
After throwing通知(After throwing advice):在目标方法抛出异常后执行的通知。After throwing通知可以访问目标方法抛出的异常。
-
After finally通知(After finally advice):在目标方法结束之后执行的通知。After finally通知可以确保目标方法不论正常结束还是异常结束都必须执行。
-
Around通知(Around advice):在目标方法执行前和执行后都执行的通知。Around通知可以替换目标方法的返回值,也可以抛出异常中断目标方法的执行。
这些通知类型可以结合使用,以实现更复杂的横切逻辑。例如,我们可以将Before通知、After returning通知和Around通知结合使用,以确保方法的参数是合法的,并在方法执行前后记录日志。同时,我们还可以使用After throwing通知捕获异常并做出处理。使用这些通知类型,我们可以实现更加灵活和抽象的横切逻辑,提高代码复用和维护性。
46.spring通知(Advice)执行的顺序
- 没有异常的执行顺序
- around before advice
- before advice
- target method
- after advice
- around after advice
- afterRunning advice
- 有异常的执行顺序
- around before advice
- before advice
- target method 执行
- after advice
- around after advice
- afterThrowing advice
- java.lang.RuntimeException:异常发生
47.什么是深拷贝?什么是浅拷贝?
- 浅拷贝:浅拷贝就是复制对象的引用,不复制内容,两个对象共用部分或者全部内容,随便对其中一个修改的话,会影响到另外一个。
- 深拷贝:深拷贝就是将一个对象复制到一个新的对象里面,如果原对象里面有其他对象的引用,也会递归复制引用和内容,两个对象之前是独立的,修改其中一个不会修改另外一个。
48.final、finally、finalize有什么区别?
- final:修饰类、方法和变量,类不能被继承,方法不能被重写,变量不能被修改。
- finally:finally用于抛异常,finally代码块内语句无论是否发生异常,都会在执行finally,常用于一些流的关闭。
- finalize:是和垃圾回收有关的。一般情况下不需要我们实现finalize,当对象被回收的时候需要释放一些资源,比如socket链接,在对象初始化时创建,整个生命周期内有效,那么需要实现finalize方法,关闭这个链接。但是当调用finalize方法后,并不意味着gc会立即回收该对象,所以有可能真正调用的时候,对象又不需要回收了,然后到了真正要回收的时候,因为之前调用过一次,这次又不会调用了,产生问题。所以,不推荐使用finalize方法。
49.equals和hashcode的关系
- equals为true,hashcode一定相同
- equals为false,hashcode不一定形同
- hashcode相同,equals不一定为true
- hashcode不同,equals一定不为true
50.string,stringBuffer,stringbuild的区别
- string是被final修饰的,所以不能被修改,每次都string类型改变的时候,都会创建一个新的对象
- stringBuffer是可以被修改的,线程安全的,但是由于加了锁,效率相对应的就降低了
- stringbuild是可以被修改的,非线程安全的,所以相对于stringBuffer效率就高
不频繁操作字符串就使用是string,频繁但是对安全性要求不高的使用stringbuild,安全性要求高的使用stringbuffer。
51.springboot的自动装配原理
-
启动类上面有个@SpringBootApplication注解
-
点进去这个注解后有两个注解@SpringBootConfiguration和@EnableAutoConfiguration
-
@SpringBootConfiguration:该注解标注在 某个类上, 说明该类为 SpringBoot的 配置类,(注意 是配置类,没有主),这个注解里面还有一个@Configuration注解,这个注解是为了让springboot知道哪些是配置类,点进去后还有一个@Component注解,这是一个组件,所以配置类就是容器的一个组件
-
@EnableAutoConfiguration:springboot实现自动装配的核心组件,意思就是开启自动装配功能。而@EnableAutoConfiguration这个注解里面也有两个非常重要的注解@AutoConfigurationPackage注解和@Import
-
@AutoConfigurationPackage:自动装配包,点进去后还有一个@Import注解,这个注解是给容器导入组件,这个组件就是AutoConfigurationPackages.Registrar.class,点进Registrar里面,他会获取getPackageNames()包名,这个包名就是我们在springboot上面写的包名,就是扫描整个包里面所有的组件,都装入容器里面
-
@Import(AutoConfigurationImportSelector.class):这个注解就是将AutoConfigurationImportSelector.class导入到容器中,点进去后,有个process()方法,能够帮我们完成一些列的自动装配操作,他会将 META-INF/spring.factories 中获取EnableAutoConfiguration指定的值,并将这些值加载到自动配置类导入到容器中,自动配置类 就生效,帮助我们进行自动配置功能。 而这些自动配置类 全都在 spring-boot-autoconfigure-2.2.6.RELEASE.jar 该jar包之下 。
-
-
52.mybatis的一级缓存和二级缓存
- mybatis的缓存:缓存就是内存中的数据,常常来自对数据库查询结果的保存,使用缓存可以避免频繁与数据库进行交互,从而提高查询响应速度。
- mybatis的一级缓存:SqlSession级别的缓存,缓存的数据只在SqlSession内有效,默认是开启的
- mybatis的二级缓存:mapper级别的缓存,同一个namespace公用这一个缓存,所以对SqlSession是共享的,二级缓存需要我们手动开启。
53.线程池的7大参数
- corePoolSize:核心线程数,指的是线程池中长期存活的线程数量
- maximumPoolSize:最大线程数,线程池中允许创建最大的线程数量
- keepAliveTime:空闲线程存活时间,当线程池中没有任务时,会销毁一些线程,销毁的线程数=maximumPoolSize(最大线程数)-corePoolSize(核心线程数)。
- TimeUnit:时间单位,有天,小时,分钟,秒,毫秒,微妙,纳秒
- BlockingQueue:线程池任务队列,BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue、DelayQueue、LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等。
- ThreadFactory:创建线程的工厂,线程池创建线程时调用的工厂方法,通过此方法可以设置线程的优先级、线程命名规则以及线程类型(用户线程还是守护线程)等
- RejectedExecutionHandler:拒绝策略,当线程池的任务超出线程池队列可以存储的最大值之后,执行的策略。默认的拒绝策略有以下 4 种:
- AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- CallerRunsPolicy:使用当前调用的线程来执行此任务。
- DiscardOldestPolicy:丢弃队列头部(最旧)的一个任务,并重新执行当前任务(重复此过程)。
- DiscardPolicy:也是丢弃任务,但是不抛出异常。
54.java中有哪些引用类型
- 强引用:java默认的就是强引用,只要强引用存在,垃圾回收器是不会回收的,除非将它置为null。
- 软引用:内存充足的时候,不会去回收软引用,在内存不足的时候,就会回收软引用,如果软引用回收了之后还是内存不足,则会报出内存溢出。
- 弱引用:进行垃圾回收时,弱引用就会被回收
- 虚引用:这个引用也有人叫幻引用,也很明显,引用一个不存在,随时会被干掉,算是所有引用中最容易被干掉的。
55.在Java中为什么不允许从静态方法中访问非静态变量?
- 静态变量是属于类本身,在类加载的时候就会分配内存,可以通过类名直接点出来。
- 非静态变量属于类的对象,只有在类的对象产生的时候,才会被分配内存,通过类的实例去访问
- 静态方法也属于类本身,但是此时没有实例,内存中没有非静态变量,所以不能调用
56.什么是java的内存模型?
- Java 内存模型是 JVM 的一种规范
- 定义了共享内存在多线程程序中读写操作行为的规范,即每个线程只能把主内存的数据拷贝到自己的工作内存中,不同的线程不能访问其他线程的工作内存。
- 屏蔽了各种硬件和操作系统的访问差异,保证了 Java 程序在各种平台下对内存的访问效果一致
- 解决并发问题采用的方式:限制处理器优化和使用内存屏障
- 增强了三个同步原语(synchronized、volatile、final)的内存语义
- 定义了 happens-before 规则
57.在Java中,什么时候用重载,什么时候用重写?
- 重载是多态的几种表现,在类中,要用统一的类型来处理不同的数据时,可以使用重载。
- 重写是建立在继承关系上,子类继承父类的基础上,增加新的功能,可以重写。
- 总结
- 重载是多样性,重写是增强性。
- 目的是提高程序的多样性和健壮性,以适配不同场景使用时,使用重载进行扩展。
- 目的是在不修改原方法及源代码的基础上对方法进行扩展或增强时,使用重写;
58.实例化对象有几种方式。
- new
- clone()
- 通过反射机制
//用 Class.forName方法获取类,在调用类的newinstance()方法
Class<?> cls = Class.forName("com.dao.User");
User u = (User)cls.newInstance();
- 序列化和反序列化
//将一个对象实例化后,进行序列化,再反序列化,也可以获得一个对象(远程通信的场景下使用)
ObjectOutputStream out = new ObjectOutputStream (new FileOutputStream("D:/data.txt"));
//序列化对象
out.writeObject(user1);
out.close();
//反序列化对象
ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:/data.txt"));
User user2 = (User) in.readObject();
System.out.println("反序列化user:" + user2);
in.close();
59.Collection 和 Collections 有什么区别?
- Collection是最基本的集合接口,Collection派生了两个子接口,list和set,分别定义了两种不同的存储方式。
- Collections是集合的工具类,它包含各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等)。
60.list与Set区别
-
list:list实际上有两种,一种ArrayList和linkedList
- ArrayList:底层结构是数组,新增和删除会导致数组移动,插入和删除效率低,但是支持随机访问,查询效率快。
- linkedList:底层结构是链表,新增会记住上一个和下一个元素的地址,不需要移动数据,插入和删除效率高,但是查询需要遍历整个链表,查询效率慢。
-
set:存入Set的每个元素都必须是唯一的,因为Set不保存重复元素。加入Set的元素必须定义equals()方法以确保对象的唯一性。Set与Collection有完全一样的接口。Set接口不保证维护元素的次序。
- hashSet:为快速查找设计的Set。存入HashSet的对象必须定义hashCode()。
- TreeSet: 保存次序的Set, 底层为树结构。使用它可以从Set中提取有序的序列。
-
list与Set区别
- List,Set都是继承自Collection接口
- List特点:元素有放入顺序,元素可重复 ,Set特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(元素虽然无放入顺序,但是元素在set中的位置是有该元素的HashCode决定的,其位置其实是固定的,加入Set 的Object必须定义equals()方法 ,另外list支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。)
- Set和List对比:
- set: 检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
- list: 和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
61.HashMap 和 Hashtable 有什么区别?
- HashMap是线程不安全的,HashTable是线程安全的;
- HashMap中允许键和值为null,HashTable不允许;
- HashMap的默认容器是16,为2倍扩容,HashTable默认是11,为2倍+1扩容;
62.说一下 HashMap 的实现原理?
- 简介
hashMap是基于map接口的,元素以键值对的方式存储,允许有null值,HashMap是线程不安全的。 - 基本属性
- 初始化大小默认16,2倍扩容
- 负载因子0.75
- HashMap的存储结构
- JDK1.7中采用数组+链表的存储形式。HashMap采取Entry数组来存储key-value,每一个键值对组成了一个Entry实体,Entry类时机上是一个单向的链表结构,它具有next指针,指向下一个Entry实体,以此来解决Hash冲突的问题
- JDK1.8中采用数据+链表+红黑树的存储形式。当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升
63.在 Queue 中 poll()和 remove()有什么区别?
- offer()和add()区别:
增加新项时,如果队列满了,add()会抛出异常,offer()会返回false。 - poll()和remove()区别:
poll()和remove()都是从队列中删除第一个元素,remove抛出异常,poll返回null。 - peek()和element()区别:
peek()和element()用于查询队列头部元素,为空时element抛出异常,peek返回null。
64.哪些集合类是线程安全的?
- vector:就比Arraylist多了个同步化机制(线程安全)。
- set:栈,也是线程安全的,继承于Vector。
- Hashtable: 就比Hashmap多了个线程安全。
- ConcurrentHashMap:是一种高效但是线程安全的集合。
65.迭代器 Iterator 是什么?
为了方便的处理集合中的元素,Java中出现了一个对象,该对象提供了一些方法专门处理集合中的元素.例如删除和获取集合中的元素.该对象就叫做迭代器(Iterator)。
66.Iterator 和 ListIterator 有什么区别?
- ListIterator继承Iterator
- ListIterator 比 Iterator多方法
- add(E e) 将指定的元素插入列表,插入位置为迭代器当前位置之前
- set(E e) 迭代器返回的最后一个元素替换参数e
- hasPrevious() 迭代器当前位置,反向遍历集合是否含有元素
- previous() 迭代器当前位置,反向遍历集合,下一个元素
- previousIndex() 迭代器当前位置,反向遍历集合,返回下一个元素的下标
- nextIndex() 迭代器当前位置,返回下一个元素的下标
- 使用范围不同,Iterator可以迭代所有集合;ListIterator 只能用于List及其子类
- ListIterator 有 add 方法,可以向 List 中添加对象;Iterator 不能
- ListIterator 有 hasPrevious() 和 previous() 方法,可以实现逆向遍历;Iterator不可以
- ListIterator 有 nextIndex() 和previousIndex() 方法,可定位当前索引的位置;Iterator不可以
- ListIterator 有 set()方法,可以实现对 List 的修改;Iterator 仅能遍历,不能修改。
67.如何确保一个集合不会被修改
final关键字可以修饰类,方法,成员变量,final修饰的类不能被继承,final修饰的方法不能被重写,final修饰的成员变量必须初始化值,如果这个成员变量是基本数据类型,表示这个变量的值是不可改变的,如果说这个成员变量是引用类型,则表示这个引用的地址值是不能改变的,但是这个引用所指向的对象里面的内容还是可以改变的。
Collections提供了一些方法,可以让map,list和set不被修改
- Collections.unmodifiableMap(map)
- Collections.unmodifiableList(List)
- Collections.unmodifiableSet(Set)
68.队列和栈是什么?有什么区别?
(1)队列先进先出,栈先进后出。
(2)遍历数据速度不同。
栈只能从头部取数据 也就最先放入的需要遍历整个栈最后才能取出来,而且在遍历数据的时候还得为数据开辟临时空间,保持数据在遍历前的一致性;
队列则不同,他基于地址指针进行遍历,而且可以从头或尾部开始遍历,但不能同时遍历,无需开辟临时空间,因为在遍历的过程中不影像数据结构,速度要快的多。
69.JVM 的架构是怎样的?
jvm的架构主要分为三个部分
- 类加载器:主要负责将类加在到内存里面
- 运行时数据区:主要包括栈,堆,方法区等
- 执行引擎:执行引擎负责执行字节码
70. JVM 中的堆和栈有什么区别?
- 堆:堆用于存储对象实例,是线程共享的
- 栈:用于存储局部变量和方法调用信息
71.如何保证redis的数据和数据库一致
那么我们需要做的就是根据不同的场景来使用合理的方式来解决数据问题。
- 第一种:先删除缓存,再更新数据库
在出现失败时可能出现的问题:
1:线程A删除缓存成功,线程A更新数据库失败;
2 :线程B从缓存中读取数据;由于缓存被删,进程B无法从缓存中得到数据,进而从数据库读取数据;此时数据库中的数据更新失败,线程B从数据库成功获取旧的数据,然后将数据更新到了缓存。
最终,缓存和数据库的数据是一致的,但仍然是旧的数据。
- 第二种:先更新数据库,再删除缓存
假设这会有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
(1)缓存刚好失效
(2)请求A查询数据库,得一个旧值
(3)请求B将新值写入数据库
(4)请求B删除缓存
(5)请求A将查到的旧值写入缓存
如果发生上述情况,确实是会发生脏数据。
然而,发生这种情况的概率又有多少呢?
发生上述情况有一个先天性条件,就是步骤(3)的写数据库操作比步骤(2)的读数据库操作耗时更短,才有可能使得步骤(4)先于步骤(5)。
数据库的读操作的速度远快于写操作的(不然做读写分离干嘛,做读写分离的意义就是因为读操作比较快,耗资源少),因此步骤(3)耗时比步骤(2)更短,这一情形很难出现。
先更新数据库,再删缓存依然会有问题,不过,问题出现的可能性会因为上面说的原因,变得比较低。
- 第三种:给所有的缓存一个失效期
第三种方案可以说是一个大杀器,任何不一致,都可以靠失效期解决,失效期越短,数据一致性越高。但是失效期越短,查数据库就会越频繁。因此失效期应该根据业务来定。
1.并发不高的情况:
读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
写: 写mysql->成功,再写redis;
2.并发高的情况:
读: 读redis->没有,读mysql->把mysql数据写回redis,有的话直接从redis中取;
写:异步话,先写入redis的缓存,就直接返回;定期或特定动作将数据保存到mysql,可以做到多次更新,一次保存;
- 第四种:加锁,使线程顺序执行
如果一个服务部署到了多个机器,就变成了分布式锁,或者是分布式队列按顺序去操作数据库或者 Redis,带来的副作用就是:数据库本来是并发的,现在变成串行的了,加锁或者排队执行的方案降低了系统性能,所以这个方案看起来不太可行。
- 第五种:采用双删
先删除缓存,再更新数据库,当更新数据后休眠一段时间再删除一次缓存。
方案推荐两种:
1:项目整合quartz等定时任务框架,去实现延时3–5s再去执行最后一步任务 。(推荐使用)
2:创建线程池,线程池中拿一个线程,线程体中延时3-5s再去执行最后一步任务(不能忘了启动线程)
- 第六种:异步更新缓存(基于订阅binlog的同步机制)
MySQL binlog增量订阅消费+消息队列+增量数据更新到redis读Redis
热数据基本都在Redis写MySQL:增删改都是操作MySQL更新Redis数据:MySQ的数据操作binlog,来更新到Redis:
1)数据操作主要分为两大块:一个是全量(将全部数据一次写入到redis)一个是增量(实时更新)。
这里说的是增量,指的是mysql的update、insert、delate变更数据。
2)读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。
这样一旦MySQL中产生了新的写入、更新、删除等操作,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。
其实这种机制,很类似MySQL的主从备份机制,因为MySQL的主备也是通过binlog来实现的数据一致性。
这里可以结合使用canal(阿里的一款开源框架),通过该框架可以对MySQL的binlog进行订阅,而canal正是模仿了mysql的slave数据库的备份请求,使得Redis的数据更新达到了相同的效果。
当然,这里的消息推送工具你也可以采用别的第三方:kafka、rabbitMQ等来实现推送更新Redis。
72.什么是微服务?
微服务是一种架构风格, 可以把应用程序划分为小型的松散耦合的服务,并通过轻量级的通信机制进行通信,,每个服务都运行在自己的进程里面,每个服务都可以独立部署,独立扩展,独立更新,从而提高了应用程序的可伸缩性,可维护性和可测试性。
由于微服务是分布式架构下的一种,针对玉应用架构的一种设计风格,所以会面临一些分布式架构下的服务治理问题,因此spring官方提供了一套spring cloud解决方案,帮助我们快速对微服务方案的搭建。
73.Spring Bean 生命周期的执行流程。
spring bean生命周期大致分为五个阶段,创建前的准备、创建实例阶段,依赖注入阶段、容器缓存阶段和容器销毁实例阶段。
- 创建前准备:
这个阶段主要的作用是,bean在开始加载之前,需要工上下文和相关配置中解析并查找bean有关的扩展实现。 - 创建实例阶段
这个阶段主要是通过反射来创建bean的实例对象,并且扫描和解析bean声明的一些属性。 - 依赖注入阶段
如果被实例化的bean存在依赖其他bean对象的情况,则需要对这些依赖bean进行对象注入,比如常见的@Autowired、setter注入等依赖注入的配置形式。同 时 , 在 这 个 阶 段 会 触 发 一 些 扩 展 的 调 用 , 比 如 常 见 的 扩 展 类 :BeanPostProcessors(用来实现 bean 初始化前后的扩展回调)、InitializingBean(这个类有一个afterPropertiesSet(),这个在工作中也比较常见)、BeanFactoryAware 等等 - 容器缓存阶段
容器缓存阶段只要是把bean保存到容器以及spring的缓存中,到了这个阶段,bean就可以被使用了。 - 销毁实例阶段
当spring应用上下文关闭时,该上下文中所有的bean都会被注销。
74. spring是如何解决循环依赖的问题?
spring设计了三级缓存来解决循环依赖的问题,当我们去调用getbean()方法的时候,spring会先从以及缓存中去找到目标,如果发现一级缓存中没有便会去二级缓存中找,而如果一级二级缓存中都没有找到,意味着该目标bean还没有实例化,于是,spring容器会实例化目标bean,然后将目标bean放到二级缓存中,同时加上表示是否存在循环依赖,如果不存在循环依赖变回将目标bean存入到二级缓存,否则变回标记该bean存在循环依赖,然后将等待下一次轮询赋值,也就是解析@Autowired注解,等@Autowired注解赋值完成后,会将目标bean存入到以及缓存中。
spring一次缓存中存在所有的成熟得到bean,二级缓存中存放所有早期bean,先取一级缓存,再取二级缓存。
75.三级缓存的作用是什么?
三级缓存是用来存储代理bean,当调用getbean(),发现目标bean需要通过代理工厂来创建,此时会将创建好的实例保存到三级缓存,最终也会将赋值好的bean同步到一级缓存中
76.spring哪些情况下,不能解决循环依赖问题。
- 多例bean通过setter注入的情况
- 构造器注入的bean情况
- 单例的代理bean通过setter注入的情况
- 设置了@DependsOn 的 Bean 的情况
77.什么是死锁?
- 所谓死锁,是一组互相竞争资源的线程,因互相等待,导致永久的阻塞的现象
- 发生死锁的原因主要有4个
- 第一个是互斥条件,共享资源X和Y只能被一个线程占用
- 第二个是指占有等待,线程T1已经取得共享资源X,在等待共享资源Y的时候,不能释放共享资源X
- 第三个是不可抢占,其他线程不能抢占线程T1占有的资源
- 第四个是循环等待,线程T1等待线程T2占有的资源,线程T2等待线程T1占有的资源。
- 如何避免死锁,首先,死锁需要达到以上4个条件,只需要破坏其中的一条,就能避免死锁
- 线程的锁机制本身就是通过互斥来完成的,所以第一个条件我们避免不了
- 针对占用且等待,如果我们一次性获取所有资源,就可以不用等待对方释放资源
- 针对不可抢占,如果申请其他资源时,申请不到,可以主动释放自己占有的资源
- 对于循环等待,可以按照申请资源来进行预防,是指资源时有线性顺序的,申请的时候可以现申请资源号小的,再申请资源号大的,这样线性化后自然就不存在循环等待了。
78.如何知道一个线程池执行完毕了?
- 线程池可以使用submit()提交方式可以获取一个Future的返回值,我们通过future.get()方法来获取任务的执行结果,当线程中的任务没有执行完毕,future.get()方法会一直阻塞,直到方法执行完毕后。若能正常返回,则说明线程执行完毕了。
- 可以引入一个countDownLatch计数器,可以通过初始化指定一个计数器进行倒计时,其中有两个方法分别是await()阻塞线程,以及countDown()进行倒计时,一旦倒计时归零,所有被阻塞在await()方法的线程都会被释放。
79.HashMap是怎么解决哈希冲突的?
- 首先hashmap是通过hash算法来计算hash值的,由于哈希算法被计算的数据是无限的,但是计算后的结果是范围有限的,所以就会存在有冲突的值,这就是哈希冲突。
- 通常解决哈希冲突的4种方法
- 开发定址法,就是从冲突的位置开始,按照一定的次序从hash表中找到一个空闲的位置,然后把发生冲突的元素存入到这个空闲的地址中。
- 链式寻址法,就是把hash冲突的key以单向链表的方式来存储,HashMap 就是采用链式寻址法来实现的
- 再hash法,就是在计算key冲突后,再用另外一个hash函数对这个key做hash,一直计算到不在冲突为止,这种方式会增加计算时间
- 建立公共溢出区,就是把 hash 表分为基本表和溢出表两个部分,凡事存在冲突的元素,一律放入到溢出表中。
hashmap在JDK1.8后,通过链式寻址法+红黑树的方式来解决hash冲突,其中红黑树是为了优化hash链表过长导致时间复杂度增加,当链表长度大于8或者链表容量大于64的时候,再向链表中添加元素就会发生转化。
80.什么叫阻塞队列的有界和无界?
- 阻塞队列是一种特殊的队列,他在普通队列的基础上增加了两个功能
- 当队列为空的时候,获取队列中的元素的消费者线程会被阻塞,同时会唤醒生产者线程
- 当队列满了的时候,向队列中添加元素的生产者被阻塞,同时唤醒消费者线程。
- 这其中,阻塞队列中能够容纳的元素个数,通常是有界的,比如我们在实例化一个arrayblockinglist,可以在构造函数中传入一个整形数字,表示这是一个基于数组的阻塞队列中能够容纳的元素个数,这种就是有界队列。
- 而无界队列,就是没有设置固定大小的队列,不过他不是像我们理解的那种元素没有任何限制的,只是它的存储量很大,像linkedBlockingQueue,它的默认队列长度是Integer.Max_Value,所以我们感知不到它的长度限制,无界队列会存在比较大的风险,如果在大并发的情况下,可以无限添加任务,容易存在内存溢出的问题。
81.ConcurrentHashMap的实现原理是什么?
- ConcurrentHashMap是jdk1.8中的存储结构,他是由数组、链表+红黑树组成,当我们初始化一个ConcurrentHashMap实例时,默认会初始化一个长度为16的数组,由于ConcurrentHashMap的核心仍然是hash表,所以必然会存在哈希冲突,同样的,采用了链表寻址法来解决哈希冲突,当哈希冲突较多时,会自动转换成红黑树,
当数组长度大于 64 并且链表长度大于等于 8 的时候,单项链表就会转换为红黑树,同样链表长度小于8时,又会退化成单向链表。 - ConcurrentHashMap本质上是一个hashmap,它的功能和hashmap是一样的,但是ConcurrentHashMap在hashmap的基础上,提供了并发安全的实现,主要是通过对指定的node节点加锁,来保证数据更新的安全性。
- ConcurrentHashMap锁粒度更细,只锁每个节点,而hashtable是锁整个hash表,其次引入红黑树,降低了数据查询的时间复杂度。当数组长度不够时,ConcurrentHashMap需要对数据进行扩容,在扩容的实现上,ConcurrentHashMap引入了多线程并发扩容机制,简单来说就是多个线程对原始数组进行分片,每个线程负责一个分片的数据迁移,从而提升了扩容的效率。
82.spring的事务传播特性
- 使用当前事务,如果没有事务则抛出异常
- 新建事务,如果有事务,就把当前事务挂起
- 新建事务,如果有事务,则加入该事务,默认是这种方式
- 如果当前存在事务,嵌套到该事务了,如果没有事务则新建一个事务。
- 加入当前事务,如果没有,则已非事务的方式执行
- 以非事务的方式执行,如果有事务,则把这个事务挂起。
- 以非事务的方式执行,如果有事务,则抛出异常。
83.什么是聚集索引和非聚集索引
- 聚集索引是以主键创建的索引,除了主键索引以外的索引称为非聚集索引
- 由于在innodb引擎中,主键索引使用的是B+树的数据结构来存储数据,叶子节点存储的是行数据,而非聚集索引虽然也是使用B+树的数据结构来存储数据,但是他的叶子节点存储的不是行数据,是指向主键索引的指针。
- 聚集所以只会查询一次,非聚集索引要查询两次,先在非聚集索引中找到指向主键索引的指针,再在主键索引中找到行数据
84.spring中有哪些方式可以把bean注入到IOC容器中
- 使用xml中的bean标签来定义,spring容器在启动的时候会加载并解析这个xml,把bean装载到IOC容器中
- 使用@CompontScan注解来扫描声明了@Controller、@Service、@Repository、 @Component 注解的类
- 使用@Configuration 注解声明配置类,并使用@Bean 注解实现 Bean 的定义
- 使用@Import 注解,导入配置类或者普通的 Bean
- 使 用 FactoryBean 工 厂 bean , 动 态 构 建 一 个 Bean 实 例 , Spring Cloud OpenFeign 里面的动态代理实例就是使用FactoryBean 来实现的。
- 实现 ImportBeanDefinitionRegistrar 接口,可以动态注入 Bean 实例。这个在Spring Boot 里面的启动注解有用到。
- 实现 ImportSelector 接口,动态批量注入配置类或者 Bean 对象,这个在 SpringBoot 里面的自动装配机制里面有用到。
85.redis如何进行rdb持久化
- 手动
- save 在命令行执行save命令,将以同步的方式创建rdb文件保存快照,会阻塞服务器的主进程,生产环境中不要用
- bgsave: 在命令行执行bgsave命令,将通过fork一个子进程以异步的方式创建rdb文件保存快照,除了fork时有阻塞,子进程在创建rdb文件时,主进程可继续处理请求
- 自动
- 在redis.conf中配置 save m n 定时触发,如 save 900 1表示在900s内至少存在一次更新就触发
- 执行shutdown且没有开启AOF持久化
86.redis如何进行AOF持久化
- 在redis.conf文件中,去修改appendonly改为yes
- 修改aof文件名和路径
- 选择AOF持久化策略
* always:每一次redis的写入都去执行持久化
* everysec:每一秒去执行一次持久化
* no:不主动去执行持久化 - 重启redis
87.redis过期键的删除策略
- 定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。(创建定时器删除)
- 优点:对内存最友好,可以最快的清楚过期的键,释放内存
- 缺点:对cpu不是很好,在过期键比较多的时候,删除过期键这一行为,会大量占用CPU的时间,对服务器的响应和吞吐量影响较大。
- 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面过期的键。至于要删除多少过期键,以及要检查多少个数据库,则有算法决定。(定期扫描删除)
- 优点:定期删除会每隔一段时间执行一次,并通过限制删除操作时长和频率来减少删除操作对CPU的影响
- 缺点:难以确定删除操作执行的时长和频率
- 如果删除执行太频繁,就和定时删除类似,会大量占用CPU时间,影响性能。
- 如果删除太慢,就会和惰性删除类似,会造成大量过期键不能及时被删除,造成内存浪费
- 惰性删除:放任键的过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
- 优点:对CPU很友好,只有在被调用的时候才会被判断是否过期才会进行删除操作
- 缺点:对内存不是很友好,会造成大量的过期键不能及时删除,造成内存浪费
88、jvm的架构
jvm架构主要分为三个子系统:类加载器系统,运行时数据区域和执行引擎
- 类加载器:负责加载类文件
- 运行时数据区域:包含堆,方法区,栈等
- 执行引擎:负责执行字节码指令的
89、jvm的运行时数据区域
- 线程私有:程序计数器,虚拟机栈,本地方法栈
- 线程共享:元数据区,堆
- 线程私有:
- 程序计数器:
- 线程独立拥有计数器,是当前线程字节码执行行号指示器
- 改变计数器的值来选取当前线程需要执行的下一条字节码指令
- 线程独有,即一个线程拥有一个计数器
- 只能对Java方法进行计数,如果是native方法,则会直接undefined
- 不会发生内存泄漏
- Java虚拟机栈:
- 作用:Java方法执行的内存模型,包含多个栈帧,保存方法的局部变量,部分结果,并参与方法的调出和返回。
- jvm直接对栈的操作有两个,一个是调用方法的入栈,一个是方法结束后的出栈。
- 栈不会存在垃圾回收问题。
- 程序执行过程:对于当前线程,每个方法会产生一个栈帧,当方法执行结束后,这个栈帧就会取消,每个栈帧包含:局部变量表,操作栈,动态链接,放回地址等。
- 本地方法栈:与虚拟机栈类似,主要用于标注native方法
- 程序计数器:
- 线程共享:
- 元空间:
- 作用:存储class基本信息,包括Java对象的method和field等
- 元空间使用的是本地内存,而永久代使用的是jvm内存
- Java堆
- 堆内存分类:根据对象存活的周期不同,把堆划分为新生代,老年代,永久代 ,这就是jvm的内存分代策略
- 存放对象实例和数组
- 内存分代主要用于垃圾回收,否的,就需要频繁遍历所有对象实例,很耗费资源。
- 内存分代:新创建的对象会被分到新生代中,经过多次回收仍然存活的对象,会被分配到老年代中,静态属性和类信息等直接存在永久代中,这样新生代的对象存活时间比较短,只需要在新生代中频繁进行CG;老年代中对象生命周期比较长,内存回收频率较低;永久代回收效率差,一般不进行回收
- 元空间:
90、什么是JMM , 它解决的是什么问题?
- jmm就是Java内存模型,为了屏蔽掉各种硬件和系统访问内存存在的差异,让Java能够在各平台实现一样的并发效果,创建的内存模型
91、详解单例模式的优缺点、注意事项以及使用场景
- 单例模式介绍:
单例模式,是一种很常见的软件设计模式,单例对象的类必须保证只能有一个实例存在,许多时候,整个系统只需要一个全局对象,这样方便协调系统的整体行为。 - 单例模式实现思路:
- 一个类只能返回一个对象的引用和一个获取该实例的方法(必须是静态方法,通常使用getInstance这个名称)
- 调用这个方法时,如果引用不为空,就返回这个引用,如果类保持的引用为空,就去创建该类的实例,并将该实例的引用赋予该类保持的引用
- 将该类的构造函数设定为私有,这样其他处代码就不能通过调用该类的构造器来创建该类的实例,只能通过该类提供的静态方法来获取唯一的实例化。
- 需要注意的地方:
如果单例模式在多线程的情况下,在实例还未创建的时候,当有两个线程同时调用创建方法,那么他们会同时检测到没有唯一的实力存在,从而各自去创建了一个实例,这样就导致有两个实例被构造出来,从而违反了实例的唯一的原则。
解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁。 - 优点:
- 在单例模式下,活动的实例只有一个,对单例类的所有实例化得到的都是相同的一个实例,这样就防止了其他对象对自己的实例化,确保所有对象访问的都是同一个实例。
- 单例模式具有一定的伸缩性,类自己来控制实例化进程,类就在改变实例化进程上有相应的伸缩性
- 提供了对唯一实例的受控访问
- 由于在系统中只存在一个对象,因此可以节约资源,当需要频繁创建和销毁对象时,单例模式是最具有性价比的
- 允许可变数目的的实例
- 避免对共享资源的多重占用。
- 缺点
- 不适合变化的对象,如果同一个对象,总是要在不同场景,发生用例变化,单例就会引起数据的错误,不能保存彼此的状态。
- 由于单例模式中没有抽象层,所以单例的扩展就比较困难
- 单例类的职责过重,一定程度上违背了单一职责原则
- 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;如果实例化的对象长时间不被利用,系统会认为是垃圾而被回收,这将导致对象状态的丢失。
- 使用注意项:
- 使用单例模式时,不能通过反射机制来创建,否则会实例化一个新的对象
- 使用懒单例模式时,需要注意线程安全问题
- 饿汉单例和懒汉单例模式的构造是私有的,所以是不能被继承的,有的单例模式时可以被继承的(如登记式模式)
92、垃圾收集算法
- 标记-清除法
- 标记:标记出需要回收的对象
- 清除:回收所有被标记的对象
- 缺点:
- 随着需要被回收的对象越来越多,这是就必须要进行大量的标记和清除操作,就会导致执行这两个操作的效率随着对象的增长而降低。
- 标记和清除后,会产生大量的不连续的内存碎片,空间碎片太多的话,就会导致程序对较大内存进行分配的时候,找不到连续内存而不得以再次提前触发一次内存回收。
- 标记-复制法
- 标记-复制法会把可用内存划分成两个相等的部分,每次只用其中的一部分,当这部门用满后,会将还存活的对象复制到另外一边,再将使用满的那一部分一次性清除
- 优点:
- 不会产生不连续的内存碎片,清除效率比较高
- 缺点
- 因为需要将可用内存分为两个部分,另外一部分内存空间使用率低,会造成内存浪费
- 标记-整理法
- 为了降低内存的消耗,引入了一种针对性的算法,其中的标记过程任然和“标记-清除”一致,但是后续步骤不是直接回收被标记的对象,而是将存活的对象往内存空间的一端移动,然后直接清除掉边界意外的内存。
93、解释下双亲委派机制
- 当某个类加载器需要去加载某个.class文件时,他不会先自己去加载,而是会交给他的父类加载器,然后递归这个操作,一直到顶层父类加载器,如果这时候顶层父类加载器不能去加载,那么,会继续沉淀下去,再有自己的加载器去加载。
- 作用:
- 防止重复加载同一个.class文件,通过不断的向上级去询问是否加载过,如果加载过了,就不需要再加载,保证数据的安全性
- 保证核心.class不会被篡改,就算篡改了也不会再加载,即使加载了,也不会是同一个.class文件,不同加载器加载同一个.class文件,也不是同一个class对象,这样保证了class执行的安全性。
94、sychronized锁升级过程
- 偏向锁:在锁的对象头中记录下当前获取锁的线程ID,该线程下次再来获取锁的时候,就可以直接执行代码,不需要再次获取锁。
- 轻量级锁:当两个或以上的线程交替获取锁,但并没有在对象上并发的获取锁,偏向锁就会升级成轻量级锁,在此阶段,等待线程会通过CAS的自旋锁方式尝试获取锁,避免阻塞线程,造成CPU在用户和内核态转换的消耗。
- 重量级锁:当两个或两个以上的线程,并发的在同一个对象上进行同步时,为了避免无用的自旋消耗cpu,轻量级锁会升级成重量级锁。
- 自旋锁:等待线程在获取锁的过程中,不会阻塞线程,这样就不会存在唤醒线程,阻塞和唤醒都是系统去操作,是比较消耗时间的,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则会继续获取,如果获取到则表示获取到锁了,这个过程一直是线程在执行,相对而言不会有太多的系统操作。
95、如何判断一个对象是否被引用
- 引用计数器:
当一个对象被引用时,计数器就会+1,不再被引用就-1,当对象的引用计数器变成0 ,就是不再被引用了,那么就可以被回收了。但是这种方法有一个漏洞,就是当A对象引用了B对象,B对象引用了A对象,除此之外,再没有其他的引用,但是这时,引用计数器是不为0的,那么这两个对象就不会被回收。但实际,这两者是没有再被引用了,这样就会造成内存泄漏。 - 可达性分析算法:
首先确定一系列不会被程序回收的对象,将这些对象作为GCroots,然后从这个根开始往下搜索,去寻找他的间接和直接引用,没有被这些根间接或者直接引用,那么这个对象就可以被回收。这样就可以有效的解决循环引用的问题。
可以被作为GC Roots的有:- 程序中的系统类对象,如java.lang.String类。
- native方法引用的对象
- 活动中的线程引用的对象
- 正在被加锁的对象
96、如何设计一个秒杀系统
秒杀系统,一般在临近秒杀时间时,会产生瞬时的高并发,在达到秒杀时间点,并发量会达到峰值,这之后,并发量会急剧减少,一般的系统,是很难处理这种场景,所以,需要重新设计一套,可以从以下几点考虑
- 页面静态化
- CDN加速
- 缓存
- mq异步处理
- 限流
- 分布式锁
-
页面静态化
如果页面做成动态的,那么在临近秒杀时,会有大量的访问,这时就会造成服务端访问的压力,而这些访问基本上都是查询一些商品的信息,所以针对这些,可以将页面做成静态的,将商品的信息都通过静态来展示,这样,就减少了服务端的访问压力,只有当秒杀时间到了,再去点击秒杀接口,才能访问服务端。这样就可以过滤掉大部分无效的请求。 -
CDN加速
由于用户所在位置不同,对应的网速也可能不同,就导致在加载静态页面的时候,速度也有差异,所以,这时候可以使用CDN来加速静态页面,使用户就近获取所需内容,降低网络阻塞,提高用户访问响应速度和命中率。 -
秒杀按钮
当活动开始之前,秒杀按钮置灰,不让用户点击,等秒杀时间到了,才可以变亮点击,在点击完之后,为了防止用户多次点击,可以再次置灰,几秒后才能再次点击,降低对服务端的访问次数。 -
redis缓存
一般秒杀系统,都是读多写少的场景,在下单之前都会去判断一下商品的库存是否够,如果商品库存不足,那么就直接提示商品库存不足,不能下单等提示语。但是在高并发的场景下,在短时间,数据库接受了很多的访问,会对数据库造成很大压力,甚至会挂掉,所以这时候可以使用redis来代替数据库。- redis缓存商品库存信息:在秒杀活动前,将商品库存信息同步到redis中,这样就可以直接通过访问redis来查询库存信息,减少了数据库的压力,大致流程如下
- 缓存问题:
- 缓存击穿:商品A第一次参与秒杀,如果缓存中没有该商品,虽然可以从数据库查了商品放入缓存中,但是在高并发场景下,同一时刻,会有大量的访问这一件商品,就会导致这些请求会同时访问数据库,造成数据库压力,所以针对这种情况,可以有两个方案
- 分布式锁:在从数据库获取商品信息时,可以使用分布式锁,只有获取锁的才能去获取商品信息,同时将数据同步到缓存中
- 也可以提前将商品数据全部放到缓存中,当然,也可以同时在从数据库获取商品信息的时候,加上锁,以防止其他问题,导致数据库压力剧增。
- redis缓存商品库存信息:在秒杀活动前,将商品库存信息同步到redis中,这样就可以直接通过访问redis来查询库存信息,减少了数据库的压力,大致流程如下
-
库存问题
- 对于库存,不是简单扣减就完事了,如果用户在规定时间内未完成付款,是要取消订单和还回库存的,所以这里需要引入一个“预扣库存”和“回退库存”的概念。具体流程如下:
- 库存不足和超卖
-
数据库扣减库存:如果先通过查询库存数量,再去扣减库存的操作,那么在高并发的情况下,会造成库存不足或者超卖的情况。如果在先加锁,在去查询库存,扣减库存,虽然不会出现库存不足或者超卖,但是性能就会降低,所以可以使用乐观锁,直接使用如下sql:
update product set stock=stock-1 where id=product and stock > 0; -
redis 扣减库存: 可以使用redis的incr的原子操作,当然在高并发下,也是需要需要加锁的。其次可以使用redis的lua脚本来完成,lua脚本也是可以保证原子性的。
-
- 对于库存,不是简单扣减就完事了,如果用户在规定时间内未完成付款,是要取消订单和还回库存的,所以这里需要引入一个“预扣库存”和“回退库存”的概念。具体流程如下:
-
mq异步处理
我们知道,秒杀场景分为三个阶段:秒杀-下单-支付,真正并发量大的是秒杀阶段,下单和支付阶段,并发量不是很高,所以在秒杀完后,使用mq给下单模块推送消息,下单模块监听到后,就去做下单的业务操作,同理,支付模块也是如此 -
限流
- 秒杀按钮加置灰:可以在点击完秒杀按钮后,几秒后才能再次点击
- 对同一用户限流:有些人可以模拟页面请求越过页面,那么可以使用redis,存储同一用户的请求次数,如果超过了阈值,那么直接返回请求失败。
- 对同一IP限流:原理如上
- 对接口限流:可以对同一个接口的访问次数做限流,比如每秒只能访问1000次等
97、spring中的事务是如何实现的
- spring事务底层是基于数据库事务和aop机制
- 首先对于使用了@transactional注解的bean,spring会创建一个代理对象bean
- 当调用代理对象bean时,会先判断该方法上是否添加了@transactional注解
- 如果加了,那么则会利用事务管理器创建一个数据库连接
- 并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现spring事务重要的一步。
- 然后执行当前方法,方法中会执行sql
- 执行完后,如果方法没有出现异常,那么事务就会直接提交
- 如果出现异常,并且这个异常是需要回滚的,那么就会回滚异常,否则仍然提交事务
- spring事务的隔离级别,就是对应数据库的隔离级别
10.spring事务的传播机制,就是spring事务自己实现的
98、@Autowired和@Resource注解的区别和联系
- 联系
- @autowired和@resource注解都是作为bean对象注入的时候使用
- 两者都可以申明在字段和setter方法上
- 区别
- @autowired注解是spring提供的,@resource是javaee提供的
- @autowired注解默认通过bytype方式注入,而@resource注解注入是通过byname方式注入
- @autowired注解注入的对象必须在ioc容器中存在,否则需要加上属性required=false,表示忽略当前要注入的bean,如果有直接注入,没有跳过,则会报错