项目面试之自测知识点:2022-3-16

1、ArrayList和LinkedList(加Vector)的区别?

这三者都是实现集合框架中的 List 接口,也就是所谓的有序集合,因此具体功能也比较近似,比如都提供搜索、添加或者删除的操作,都提供迭代器以遍历其内容等功能。

  • 数据结构实现:ArrayList 和 Vector 是动态数组的数据结构实现,而 LinkedList 是双向循环链表的数据结构实现。
  • 随机访问效率:ArrayList 和 Vector 比 LinkedList 在根据索引随机访问的时候效率要高,因为 LinkedList 是链表数据结构,需要移动指针从前往后依次查找。
  • 增加和删除效率:在非尾部的增加和删除操作,LinkedList 要比 ArrayList 和 Vector 效率要高,因为 ArrayList 和 Vector 增删操作要影响数组内的其他数据的下标,需要进行数据搬移。因为 ArrayList 非线程安全,在增删元素时性能比 Vector 好。
  • 内存空间占用:一般情况下LinkedList 比 ArrayList 和 Vector 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,分别是前驱节点和后继节点。
  • 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;Vector 使用了 synchronized 来实现线程同步,是线程安全的。
  • 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍容量,而 ArrayList 只会增加 50%容量。
  • 使用场景:在需要频繁地随机访问集合中的元素时,推荐使用 ArrayList,希望线程安全的对元素进行增删改操作时,推荐使用Vector,而需要频繁插入和删除操作时,推荐使用 LinkedList。

2、ArrayList和LinkedList不是线程安全的,多线程怎么保证安全?

使用Collections.synchronizedList获取一个线程安全的集合;你必须在修改集合之前首先获取集合的锁进行同步控制;或者你可以使用java.util.concurrent包下的相关线程安全的集合类。

3、有哪些线程安全的集合?

  1. Vector:就比Arraylist多了个同步化机制(线程安全)。
  2. Hashtable:就比Hashmap多了个线程安全。
  3. ConcurrentHashMap:是一种高效但是线程安全的集合。
  4. Stack:栈,也是线程安全的,继承于Vector。

4、CopyOnWriteArrayList?

这和 ReentrantReadWriteLock 读写锁的思想非常类似,也就是 读读共享、写写互斥、读写互斥、写读互斥。JDK中提供了 CopyOnWriteArrayList 类,相比于在读写锁的思想又更进一步。为了将读取的性能发挥到极致,CopyOnWriteArrayList 读取是完全不用加锁的,并且更厉害的是:写入也不会阻塞读取操作,只有写入和写入之间需要进行同步等待,读操作的性能得到大幅度提升。

CopyOnWriteArrayList 类的所有可变操作(add,set等等)都是通过创建底层数组的新副本来实现的。当 List 需要被修改的时候,并不直接修改原有数组对象,而是对原有数据进行一次拷贝,将修改的内容写入副本中。写完之后,再将修改完的副本替换成原来的数据,这样就可以保证写操作不会影响读操作了。

从 CopyOnWriteArrayList 的名字可以看出,CopyOnWriteArrayList 是满足 CopyOnWrite 的 ArrayList,所谓 CopyOnWrite 的意思:、就是对一块内存进行修改时,不直接在原有内存块中进行写操作,而是将内存拷贝一份,在新的内存中进行写操作,写完之后,再将原来指向的内存指针指到新的内存,原来的内存就可以被回收。

5、synchronized 和 ReentrantLock 区别是什么?

相同点:两者都是可重入锁。
主要区别如下:

  • 本质:synchronized 是关键字,ReetrantLock是类,这是二者的本质区别。既然 ReentrantLock 是类,那么它就提供了比 synchronized 更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的变量
  • 加锁和释放锁:ReentrantLock 必须手动获取与释放锁,而 synchronized 不需要手动开启和释放锁;
  • 作用域:ReentrantLock 只能给代码块加锁,而 synchronized 可以给方法、代码块加锁。
  • 性能:早期版本 synchronized 在很多场景下性能较差,在后续版本进行了较多改进,在低竞争场景中表现可能优于 ReentrantLock。
  • 底层实现:二者的锁机制其实也是不一样的。ReentrantLock 底层调用的是 Unsafe 的 park 方法加锁,synchronized 操作的是对象头中 mark word

6、乐观锁和悲观锁,syn是悲观锁?

悲观锁:假定会发生并发冲突,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。再比如 jdk1.6 以前的同步原语 synchronized 关键字的实现也是悲观锁。

乐观锁:假设不会发生并发冲突,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于 write_condition 机制是乐观锁。在 Java中 java.util.concurrent.atomic 包下面的原子操作类就是使用了乐观锁的一种实现方式 CAS。

1、版本号机制:一般是在数据表中加上一个版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读到的version值与当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
2、CAS算法:java 中的 Compare and Swap 即 CAS ,当多个线程尝试使用 CAS 同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存地址里面的值 V 和 预期原值 A 的值是一样的,那么就将内存里面的值 V 更新成新值 B。CAS是通过无限循环来获取数据的,如果在第一轮循环中,a 线程获取地址里面的值被 b 线程修改了,那么 a 线程需要自旋,到下次循环才有可能机会执行。

7、乐观锁CAS?

CAS 是 compare and swap 的缩写,即我们所说的比较交换。CAS 是基于冲突检测的乐观锁(非阻塞)
cas 是一种基于锁的操作,而且是乐观锁。

java.util.concurrent.atomic 包下的原子操作类大多是使用 CAS 操作来实现的(AtomicInteger,AtomicBoolean,AtomicLong)。

8、HashMap,可以的话直接说1.8,或者少聊1.7?

在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难;链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。

JDK1.8之前

JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的的值加到链表中即可。

JDK1.8之后

相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8),但是数组长度小于64时会首先进行扩容,否则会将链表转化为红黑树,以减少搜索时间。

9、头插法是循环链表的问题,容量要是2的次幂,为了hash冲突算法?

我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制与操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。

10、事务,spring事务的事务传播机制?

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。真正的数据库层的事务提交和回滚是通过bin log或者redo log实现的。

spring事务的传播行为说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。

  • ① PROPAGATION_REQUIRED:如果当前没有事务,就创建一个新事务,如果当前存在事务,就加入该事务,该设置是最常用的设置。
  • ② PROPAGATION_SUPPORTS:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就以非事务执行。
  • ③ PROPAGATION_MANDATORY:支持当前事务,如果当前存在事务,就加入该事务,如果当前不存在事务,就抛出异常。
  • ④ PROPAGATION_REQUIRES_NEW:创建新事务,无论当前存不存在事务,都创建新事务。
  • ⑤ PROPAGATION_NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,就把当前事务挂起。
  • ⑥ PROPAGATION_NEVER:以非事务方式执行,如果当前存在事务,则抛出异常。
  • ⑦ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则按REQUIRED属性执行。

11、MySQL的事务原理?

关系性数据库需要遵循ACID规则,具体内容如下:

特性说明
原子性 Atomic事务是最小的执行单位,不允许分割。事务包含的所有操作要么全部成功,要么全部失败回滚。
一致性 Consistency事务执行之前和执行之后都必须处于一致性状态。举例:拿转账来说,假设用户A和用户B两者的钱加起来一共是5000,那么不管A和B之间如何转账,转几次账,事务结束后两个用户的钱相加起来应该还得是5000,这就是事务的一致性。
隔离性 Isolation隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间是相互隔离的。数据库规定了多种事务隔离级别,不同的隔离级别对应不同的干扰程度。隔离级别越高,数据一致性越好,但并发性越差。
持久性 Durability持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下,也不会丢失提交事务的操作。

12、MySQL的默认引擎?

存储引擎Storage engine:MySQL中的数据、索引以及其他对象是如何存储的,是一套文件系统的实现。

常用的存储引擎有以下:

  • Innodb引擎:Innodb引擎提供了对数据库ACID事务的支持。并且还提供了行级锁和外键的约束。它的设计的目标就是处理大数据容量的数据库系统。
  • MyISAM引擎(原本MySQL的默认引擎):不提供事务的支持,也不支持行级锁和外键。
  • MEMORY引擎:所有的数据都在内存中,数据的处理速度快,但是安全性不高。
  • MyISAM与InnoDB区别
InnodbMyISAM
存储结构每张表都保存在同一个数据文件中每张表被存放在三个文件:表定义文件、数据文件、索引文件
数据和索引存储方式数据和索引是集中存储的,查询时做到覆盖索引会非常高效数据和索引是分开存储的,索引的叶子节点存储的是行数据地址,需要再寻址一次才能得到数据
记录存储顺序按主键大小有序插入按记录插入顺序保存
索引聚簇索引非聚簇索引
索引的实现方式B+树索引,Innodb 是索引组织表B+树索引,myisam 是堆表
全文索引不支持支持
哈希索引支持不支持
外键支持不支持
事务支持不支持
锁粒度(锁是避免资源争用的一个机制,MySQL锁对用户几乎是透明的)行级锁定、表级锁定,锁定力度越小并发能力越高表级锁定
SELECTMyISAM更优
select count(*)myisam更快,因为myisam内部维护了一个计数器,可以直接调取。
INSERT、UPDATE、DELETEInnoDB更优
存储引擎选择

MyISAM:适用于管理非事务表,它提供高速存储和检索, 以及全文搜索能力的场景。比如博客系统、新闻门户网站。

InnoDB:适用于更新操作频繁,或者要保证数据的完整性,并发量高,支持事务和外键的场景。比如OA自动化办公系统。

如果没有特别的需求,使用默认的Innodb即可。

13、事务失效的几种情形?

  • 1、spring的事务注解@Transactional只能放在public修饰的方法上才起作用,如果放在其他非public(private,protected)方法上,虽然不报错,但是事务不起作用

  • 2、如果采用spring+spring mvc,则context:component-scan重复扫描问题可能会引起事务失败。
    如果spring和mvc的配置文件中都扫描了service层,那么事务就会失效。
    原因:因为按照spring配置文件的加载顺序来讲,先加载springmvc配置文件,再加载spring配置文件,我们的事物一般都在srping配置文件中进行配置,如果此时在加载srpingMVC配置文件的时候,把servlce也给注册了,但是此时事物还没加载,也就导致后面的事物无法成功注入到service中。所以把对service的扫描放在spring配置文件中或是其他配置文件中。

  • 3、如使用mysql且引擎是MyISAM,则事务会不起作用,原因是MyISAM不支持事务,可以改成InnoDB引擎

  • 4、 @Transactional 注解开启配置,必须放到listener里加载,如果放到DispatcherServlet的配置里,事务也是不起作用的。
    如果spring和mvc的配置文件中都扫描了service层,那么事务就会失效。
    原因:因为按照spring配置文件的加载顺序来讲,先加载springmvc配置文件,再加载spring配置文件,我们的事物一般都在srping配置文件中进行配置,如果此时在加载srpingMVC配置文件的时候,把servlce也给注册了,但是此时事物还没加载,也就导致后面的事物无法成功注入到service中。所以把对service的扫描放在spring配置文件中或是其他配置文件中。

  • 5、Spring团队建议在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上。在接口上使用 @Transactional 注解,只能当你设置了基于接口的代理时它才生效。因为注解是 不能继承 的,这就意味着如果正在使用基于类的代理时,那么事务的设置将不能被基于类的代理所识别,而且对象也将不会被事务代理所包装。

  • 6、在业务代码中如果抛出RuntimeException异常,事务回滚;但是抛出Exception,事务不回滚;

  • 7、如果在加有事务的方法内,使用了try…catch…语句块对异常进行了捕获,而catch语句块没有throw new RuntimeExecption异常,事务也不会回滚。

  • 8、在类A里面有方法a 和方法b, 然后方法b上面用 @Transactional加了方法级别的事务,在方法a里面 调用了方法b, 方法b里面的事务不会生效。原因是在同一个类之中,方法互相调用,切面无效 ,而不仅仅是事务。这里事务之所以无效,是因为spring的事务是通过aop实现的。

14、多线程,join()和yield()的区别?

  • yield(): yield() 方法和 sleep() 方法类似,也不会释放“锁标志”,区别在于,它没有参数,即 yield() 方法只是使当前线程重新回到可执行状态,所以执行 yield() 的线程有可能在进入到可执行状态后马上又被执行,另外 yield() 方法只能使同优先级或者高优先级的线程得到执行机会,这也和 sleep() 方法不同。

  • join(): join() 方法会使当前线程等待调用 join() 方法的线程结束后才能继续执行

15、多线程参数,工作原理,拒绝策略?

  • corePoolSize :核心线程数,定义了最小可以同时运行的线程数。
  • maximumPoolSize :线程池中允许存在的最大工作线程数。
  • workQueue:工作队列的长度。当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。

ThreadPoolExecutor 其他常见参数

  1. keepAliveTime:线程池中的线程数大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;
  2. unitkeepAliveTime 参数的时间单位。
  3. threadFactory:创建新线程的线程工厂
  4. handler :当工作队列已满并且同时运行的线程数达到最大工作线程数时,新加入的任务就会走拒绝策略

ThreadPoolExecutor 工作原理
任务被提交到线程池,会先判断当前线程数量是否小于corePoolSize,如果小于则创建线程来执行提交的任务,否则将任务放入workQueue队列,如果workQueue满了,则判断当前线程数量是否小于maximumPoolSize,如果小于则创建线程执行任务,否则就会调用handler,以表示线程池拒绝接收任务。

ThreadPoolExecutor 拒绝策略定义:

如果当工作队列已满并且同时运行的线程数达到最大工作线程数时,ThreadPoolTaskExecutor 定义一些策略:

  • ThreadPoolExecutor.AbortPolicy(默认):抛出 RejectedExecutionException来拒绝新任务的处理。
  • ThreadPoolExecutor.CallerRunsPolicy:用调用者所在的线程来执行任务。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。
  • ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃最早的未处理的任务。

举个例子:Spring 通过 ThreadPoolTaskExecutor 或者我们直接通过 ThreadPoolExecutor 的构造函数创建线程池的时候,当我们不指定 RejectedExecutionHandler 拒绝策略的话,配置线程池的时候默认使用的是 ThreadPoolExecutor.AbortPolicy。在默认情况下,ThreadPoolExecutor 将抛出 RejectedExecutionException 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。对于可伸缩的应用程序,建议使用 ThreadPoolExecutor.CallerRunsPolicy

16、机器或服务宕机,线程池中有任务进入阻塞队列,导致任务丢失?

1.线上机器宕机,导致内存中积压在阻塞队列的任务丢失怎么办?

方案一:数据备份:
在提交任务到线程池之前,先任务进行备份
如果发生宕机,内存中工作队列的任务会消失
机器重启后,从备份的数据中找回任务,重新提交重新执行

方案二:采用多阶段的方式,解决可用性与一致性的问题

2.采用多阶段的方式将任务划分为哪几个状态?

  • 未提交
  • 已提交
  • 已执行

提交任务之后,将任务的状态从未提交改变已提交

机器重启后,找到这些已提交未执行的任务,重新执行变成已完成。

3.如果一个任务在线程池内部执行完毕,还没来得及将任务改为已完成状态,这时候突然宕机,怎么办?

任务仍处于已提交状态,重启之后会被重新执行,造成任务的重复执行,解决方案:

方案一:所以不能只用已提交状态判断,还应该由具体业务,包括幂等性判断(即一次请求只能执行一次,多次请求执行出来的结果一致),辅助任务的状态判断是否应该重新提交此任务

方案二:如果任务执行的是数据库操作,可以采用事务,保证任务执行和任务状态更新同时完成,或者同时回滚## 线程之间的通信方式?

17、自己开发一个线程池?

线程池,顾名思义它首先是一个“池”,这个池里面放的是线程,线程是用来执行任务的。

首先,线程池中的线程应该是有类别的,有的是核心线程,有的是非核心线程,所以我们需要两个变量标识核心线程数量coreSize和最大线程数量maxSize。

为什么要区分是否为核心线程呢?这是为了控制系统中线程的数量。

当线程池中线程数未达到核心线程数coreSize时,来一个任务加一个线程是可以的,也可以提高任务执行的效率。

当线程池中线程数达到核心线程数后,得控制一下线程的数量,来任务了先进队列,如果任务执行足够快,这些核心线程很快就能把队列中的任务执行完毕,完全没有新增线程的必要。

当队列中任务也满了,这时候光靠核心线程就无法及时处理任务了,所以这时候就需要增加新的线程了,但是线程也不能无限制地增加,所以需要控制其最大线程数量maxSize。

其次,我们需要一个任务队列来存放任务,这个队列必须是线程安全的,我们一般使用BlockingQueue阻塞队列来充当,当然使用ConcurrentLinkedQueue也是可以的(注意ConcurrentLinkedQueue不是阻塞队列,不能运用在jdk的线程池中)。

最后,当任务越来越多而线程处理却不及时,迟早会达到一种状态,队列满了,线程数也达到最大线程数了,这时候怎么办呢?这时候就需要走拒绝策略了,也就是这些无法及时处理的任务怎么办的一种策略,常用的策略有丢弃当前任务、丢弃最老的任务、调用者自己处理、抛出异常等。

根据上面的描述,我们定义一个线程池一共需要这么四个变量:核心线程数coreSize、最大线程数maxSize、阻塞队列BlockingQueue、拒绝策略RejectPolicy。

另外,为了便于给线程池一个名称,我们再加一个变量:线程池的名称name。

18、spring,bean的生命周期?

初始化->设置属性->放入Spring容器->销毁

bean在Spring容器中从创建到销毁经历了若干阶段,每一阶段都可以针对Spring如何管理bean进行个性化定制。

正如你所见,在bean准备就绪之前,bean工厂执行了若干启动步骤。

我们对上图进行详细描述:

Spring对bean进行实例化;

Spring将值和bean的引用注入到bean对应的属性中;

如果bean实现了BeanNameAware接口,Spring将bean的ID传递给setBean-Name()方法;

如果bean实现了BeanFactoryAware接口,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入;

如果bean实现了ApplicationContextAware接口,Spring将调用setApplicationContext()方法,将bean所在的应用上下文的引用传入进来;

如果bean实现了BeanPostProcessor接口,Spring将调用它们的post-ProcessBeforeInitialization()方法;

如果bean实现了InitializingBean接口,Spring将调用它们的after-PropertiesSet()方法。类似地,如果bean使用initmethod声明了初始化方法,该方法也会被调用;

此时,bean已经准备就绪,可以被应用程序使用了,它们将一直驻留在应用上下文中,直到该应用上下文被销毁;

如果bean实现了DisposableBean接口,Spring将调用它的destroy()接口方法。同样,如果bean使用destroy-method声明了销毁方法,该方法也会被调用。

19、bean的初始化?

  • BeanNameAware.setBeanName() 在创建此bean的bean工厂中设置bean的名称,在普通属性设置之后调用,在InitializinngBean.afterPropertiesSet()方法之前调用

  • BeanClassLoaderAware.setBeanClassLoader(): 在普通属性设置之后,InitializingBean.afterPropertiesSet()之前调用

  • BeanFactoryAware.setBeanFactory() : 回调提供了自己的bean实例工厂,在普通属性设置之后,在InitializingBean.afterPropertiesSet()或者自定义初始化方法之前调用

  • EnvironmentAware.setEnvironment(): 设置environment在组件使用时调用

  • EmbeddedValueResolverAware.setEmbeddedValueResolver(): 设置StringValueResolver 用来解决嵌入式的值域问题

  • ResourceLoaderAware.setResourceLoader(): 在普通bean对象之后调用,在afterPropertiesSet 或者自定义的init-method 之前调用,在 ApplicationContextAware 之前调用。

  • ApplicationEventPublisherAware.setApplicationEventPublisher(): 在普通bean属性之后调用,在初始化调用afterPropertiesSet 或者自定义初始化方法之前调用。在 ApplicationContextAware 之前调用。

  • MessageSourceAware.setMessageSource(): 在普通bean属性之后调用,在初始化调用afterPropertiesSet 或者自定义初始化方法之前调用,在 ApplicationContextAware 之前调用。

  • ApplicationContextAware.setApplicationContext(): 在普通Bean对象生成之后调用,在InitializingBean.afterPropertiesSet之前调用或者用户自定义初始化方法之前。在ResourceLoaderAware.setResourceLoader,ApplicationEventPublisherAware.setApplicationEventPublisher,MessageSourceAware之后调用。

  • ServletContextAware.setServletContext(): 运行时设置ServletContext,在普通bean初始化后调用,在InitializingBean.afterPropertiesSet之前调用,在 ApplicationContextAware 之后调用注:是在WebApplicationContext 运行时

  • BeanPostProcessor.postProcessBeforeInitialization() : 将此BeanPostProcessor 应用于给定的新bean实例 在任何bean初始化回调方法(像是InitializingBean.afterPropertiesSet或者自定义的初始化方法)之前调用。这个bean将要准备填充属性的值。返回的bean示例可能被普通对象包装,默认实现返回是一个bean。

  • BeanPostProcessor.postProcessAfterInitialization() : 将此BeanPostProcessor 应用于给定的新bean实例 在任何bean初始化回调方法(像是InitializingBean.afterPropertiesSet或者自定义的初始化方法)之后调用。这个bean将要准备填充属性的值。返回的bean示例可能被普通对象包装

  • InitializingBean.afterPropertiesSet(): 被BeanFactory在设置所有bean属性之后调用(并且满足BeanFactory 和 ApplicationContextAware)。

20、Spring三级缓存解决循环依赖?

spring三级缓存解决循环依赖
Spring实现循环依赖原理
spring解决循环依赖也是如此,首先暴露一个未初始化的实例TestA放到缓存中,创建TestB的实例时,获取的是TestA的未初始化对象,TestB创建完成以后,将TestA进行初始化,由于TestB中TestA的引用和TestA是一样的,TestB中的属性也是完全的初始化的。

Spring的三级缓存
DefaultSingletonBeanRegistry中存在三个Map,用作三级缓存。
一级缓存:singletonObjects ,用于保存BeanName与创建bean实例。
二级缓存:earlySingletonObjects ,用于BeanName与创建bean实例,与singletonObjects 不同的是earlySingletonObjects 中存放的bean是一个未初始化的bean。
三级缓存:singletonFactories ,用于存放BeanName与bean工厂。

public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {

	//一级缓存(单例池,经过完成生命周期的对象会放入其中)
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

    //二级缓存(刚实例化还未初始化的原始对象会放入其中)
    private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
	
    //三级缓存(存放创建某个对象的工厂)
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

Spring Bean对象从创建到初始化大致会经过四个流程

getSingleton()、doCreateBean()、populateBean()、addSingleton()
循环依赖的对象在三级缓存中的迁移过程

A 创建过程中需要 B, 于是 A 将自己放到三级缓存里面,去实例化 B
B 实例化的时候发现需要 A,于是 B 先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存
找到了A,然后把三级缓存中的 A 放到二级缓存,并删除三级缓存中的 A
B 顺利初始化完毕,将自己放到一级缓存中(此时 B 中的 A 还是创建中状态,并没有完全初始化),删除三级缓存中的 B
然后接着回来创建 A,此时 B 已经完成创建,直接从一级缓存中拿到 B,完成 A 的创建,并将 A 添加到单例池,删除二级缓存中的 A

在这里插入图片描述

21、springboot的自动装配原理?

从源码声明可以看出,@SpringBootApplication相当于 @SpringBootConfiguration + @ComponentScan + @EnableAutoConfiguration ,因此我们直接拆开来分析。

上面三个注解都在做一件事:注册bean到spring容器。他们通过不同的条件不同的方式来完成:

  • @SpringBootConfiguration 通过与 @Bean 结合完成Bean的 JavaConfig 配置;
  • @ComponentScan 通过范围扫描的方式,扫描特定注解注释的类,将其注册到Spring容器;
  • @EnableAutoConfiguration 通过 spring.factories 的配置,并结合 @Condition 条件,完成bean的注册;

除了上面的三个注解,还可以使用@Import注解将bean注册到Spring容器

  • @Import 通过导入的方式,将指定的class注册解析到Spring容器;

22、springcloud的组件,服务降级?

  • Eureka:服务治理组件,包括服务端的注册中心和客户端的服务发现机制;
  • Ribbon:负载均衡的服务调用组件,具有多种负载均衡调用策略;
  • Hystrix:服务容错组件,实现了断路器模式,为依赖服务的出错和延迟提供了容错能力;
  • Feign:基于Ribbon和Hystrix的声明式服务调用组件;
  • Zuul:API网关组件,对请求提供路由及过滤功能。

23、事务失败回滚?

24、mq保证消息被消费有序?重复消费?消息丢失

想想为什么要使用MQ?

1.解耦,系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!

2.异步,将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度

3.削峰,并发量大的时候,所有的请求直接怼到数据库,造成数据库连接异常

使用了消息队列会有什么缺点?

1.系统可用性降低:你想啊,本来其他系统只要运行好好的,那你的系统就是正常的。现在你非要加个消息队列进去,那消息队列挂了,你的系统不是呵呵了。因此,系统可用性降低

2.系统复杂性增加:要多考虑很多方面的问题,比如一致性问题、如何保证消息不被重复消费,如何保证保证消息可靠传输。因此,需要考虑的东西更多,系统复杂性增大。

如何保证消息队列是高可用的?

使用集群的方式维持MQ的可高用性。

如何保证消息不被重复消费?

保证消息不被重复消费的关键是保证消息队列的幂等性,这个问题针对业务场景来答分以下几点:

1.比如,你拿到这个消息做数据库的insert操作。那就容易了,给这个消息做一个唯一主键,那么就算出现重复消费的情况,就会导致主键冲突,避免数据库出现脏数据。

2.再比如,你拿到这个消息做redis的set的操作,那就容易了,不用解决,因为你无论set几次结果都是一样的,set操作本来就算幂等操作。

3.如果上面两种情况还不行,上大招。准备一个第三方介质,来做消费记录。以redis为例,给消息分配一个全局id,只要消费过该消息,将<id,message>以K-V形式写入redis。那消费者开始消费前,先去redis中查询有没消费记录即可。

如何解决丢数据的问题?

1.生产者丢数据
生产者的消息没有投递到MQ中怎么办?从生产者弄丢数据这个角度来看,RabbitMQ提供transaction和confirm模式来确保生产者不丢消息。

transaction机制就是说,发送消息前,开启事物(channel.txSelect()),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()),如果发送成功则提交事物(channel.txCommit())。

然而缺点就是吞吐量下降了。因此,按照博主的经验,生产上用confirm模式的居多。一旦channel进入confirm模式,所有在该信道上面发布的消息都将会被指派一个唯一的ID(从1开始),一旦消息被投递到所有匹配的队列之后,rabbitMQ就会发送一个Ack给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了.如果rabiitMQ没能处理该消息,则会发送一个Nack消息给你,你可以进行重试操作。

2.消息队列丢数据
处理消息队列丢数据的情况,一般是开启持久化磁盘的配置。这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。这样,如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

那么如何持久化呢,这里顺便说一下吧,其实也很容易,就下面两步

①、将queue的持久化标识durable设置为true,则代表是一个持久的队列

②、发送消息的时候将deliveryMode=2

这样设置以后,rabbitMQ就算挂了,重启后也能恢复数据。在消息还没有持久化到硬盘时,可能服务已经死掉,这种情况可以通过引入mirrored-queue即镜像队列,但也不能保证消息百分百不丢失(整个集群都挂掉)

3.消费者丢数据
启用手动确认模式可以解决这个问题

①自动确认模式,消费者挂掉,待ack的消息回归到队列中。消费者抛出异常,消息会不断的被重发,直到处理成功。不会丢失消息,即便服务挂掉,没有处理完成的消息会重回队列,但是异常会让消息不断重试。

②手动确认模式,如果消费者来不及处理就死掉时,没有响应ack时会重复发送一条信息给其他消费者;如果监听程序处理异常了,且未对异常进行捕获,会一直重复接收消息,然后一直抛异常;如果对异常进行了捕获,但是没有在finally里ack,也会一直重复发送消息(重试机制)。

③不确认模式,acknowledge=“none” 不使用确认机制,只要消息发送完成会立即在队列移除,无论客户端异常还是断开,只要发送完就移除,不会重发。

如何保证消息的顺序性?

针对这个问题,通过某种算法,将需要保持先后顺序的消息放到同一个消息队列中。然后只用一个消费者去消费该队列。同一个queue里的消息一定是顺序消息的。我的观点是保证入队有序就行,出队以后的顺序交给消费者自己去保证,没有固定套路。例如B消息的业务应该保证在A消息后业务后执行,那么我们保证A消息先进queueA,B消息后进queueB就可以了。

25、数据库中如何存储树形结构?

第一种:id parentId
第二种:文件路径结构
第三种:建立中间关系表:preId 、currentId、nextId

26、JVM,类加载机制?

Java中的所有类,都需要由类加载器装载到JVM中才能运行,同时对数据进行验证,准备,解析和初始化,最终形成可以被虚拟机直接使用的类型。

类加载器本身也是一个类,而它的工作就是把class文件从硬盘读取到内存中。在写程序的时候,我们几乎不需要关心类的加载,因为这些都是隐式装载的,除非我们有特殊的需求,像是反射,就需要显式的加载所需要的类。

类装载方式,有两种 :

  • 隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中

  • 显式装载, 通过class.forname()等方法,显式加载需要的类

Java中类的加载是动态的,它并不会一次性将所有类全部加载后再运行,而是保证程序运行的基础类(像是基类)完全加载到jvm中,至于其他类,则在需要的时候才加载,这是为了节省内存开销。

27、年轻代到老年代的几种方式?

大对象直接进入老年代

所谓大对象是指需要大量连续内存空间的对象,频繁出现大对象是致命的,会导致在内存还有不少空间的情况下提前触发 GC,以获取足够的连续空间来安置新对象。

新生代使用的是复制算法,如果大对象直接在新生代分配,就会导致 Eden 区和两个 Survivor 区之间发生大量的内存复制,因此对于大对象都会直接在老年代进行分配。

长期存活对象将进入老年代

虚拟机采用分代收集的思想来管理内存,会给每个对象定义了一个对象年龄的计数器,对象在 Eden 区出生,经过一次 Minor GC 对象年龄就会加 1,当年龄达到一定程度(默认 15) 就会被晋升到老年代,也就是长期存活对象将进入老年代。

28、了解JVM对开发有什么帮助?

  • 内存泄漏问题
  • 大对象问题

29、索引失效的原因?

1.隐式转换导致索引失效.这一点应当引起重视.也是开发中经常会犯的错误.

由于表的字段tu_mdn定义为varchar2(20),但在查询时把该字段作为number类型以where条件传给Oracle,这样会导致索引失效.
错误的例子:select * from test where tu_mdn=13333333333;
正确的例子:select * from test where tu_mdn=‘13333333333’;

2. 对索引列进行运算导致索引失效,我所指的对索引列进行运算包括(+,-,*,/,! 等)

错误的例子:select * from test where id-1=9;
正确的例子:select * from test where id=10;

3. 使用Oracle内部函数导致索引失效.对于这样情况应当创建基于函数的索引.

错误的例子:select * from test where round(id)=10; 说明,此时id的索引已经不起作用了
正确的例子:首先建立函数索引,create index test_id_fbi_idx on test(round(id));然后 select * from test where round(id)=10; 这时函数索引起作用了

4. 以下使用会使索引失效,应避免使用;

a. 使用 <> 、not in 、not exist、!=
b. like “%_” 百分号在前(可采用在建立索引时用reverse(columnName)这种方法处理)
c. 单独引用复合索引里非第一位置的索引列.应总是使用索引的第一个列,如果索引是建立在多个列上, 只有在它的第一个列被where子句引用时,优化器才会选择使用该索引。
d. 字符型字段为数字时在where条件里不添加引号.
e. 当变量采用的是times变量,而表的字段采用的是date变量时.或相反情况。

5. 不要将空的变量值直接与比较运算符(符号)比较。

如果变量可能为空,应使用 IS NULL 或 IS NOT NULL 进行比较,或者使用 ISNULL 函数。

6. 不要在 SQL 代码中使用双引号。

因为字符常量使用单引号。如果没有必要限定对象名称,可以使用(非 ANSI SQL 标准)括号将名称括起来。

7. 将索引所在表空间和数据所在表空间分别设于不同的磁盘chunk上,有助于提高索引查询的效率。
8. Oracle默认使用的基于代价的SQL优化器(CBO)非常依赖于统计信息,一旦统计信息不正常,会导致数据库查询时不使用索引或使用错误的索引。

一般来说,Oracle的自动任务里面会包含更新统计信息的语句,但如果表数据发生了比较大的变化(超过20%),可以考虑立即手动更新统计信息,例如:analyze table abc compute statistics,但注意,更新 统计信息比较耗费系统资源,建议在系统空闲时执行。

9. Oracle在进行一次查询时,一般对一个表只会使用一个索引.

因此,有时候过多的索引可能导致Oracle使用错误的索引,降低查询效率。例如某表有索引1(Policyno)和索引2(classcode),如果查询条件为policyno = ‘xx’ and classcode = ‘xx’,则系统有可能会使用索 引2,相较于使用索引1,查询效率明显降低。

10. 优先且尽可能使用分区索引。

30、强制走索引?

如果在sql中使用的索引不生效,可以使用FORCE INDEX(索引),来强制使用索引:

例:SELECT COUNT(DEAL_STAT) FROM c_pac FORCE INDEX (IDX_PACKET_DEAL_STAT)

31、索引是怎么提高查询效率的?

索引结构B+tree,在业务场景中查询数据时,往往是查询多条数据,比如查询最近修改过的10条数据,B+树在B树的基础上进行了优化,B+树的所有数据都在叶子结点,同时有链表结构,只需要找到首尾,就可以把所有的数据找出来了。综上述所述,MySQL中存储索引用B+树的好处主要是降低树高度提高查询效率、多路设计保证硬盘到内存的加载、叶子节点存储数据并且加了指针形成链表在范围查找时只需定位首尾就可以取出所需数据。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

唯荒

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值