每日八股文之Java

1、请你说说ConcurrentHashMap

数组+链表+红黑树、锁的粒度

ConcurrentHashMap的底层数据结构与HashMap一样,也是采用“数组+链表+红黑树”的形式

采用锁定头节点的方式降低了锁粒度,以较低的性能代价实现了线程安全。它的线程安全的实现机制:

  1. 初始化数组或头节点时,ConcurrentHashMap并没有加锁,而是CAS的方式进行原子替换(原子操作,基于Unsafe类的原子操作API)。

  2. 插入数据时会进行加锁处理,但锁定的不是整个数组,而是槽中的头节点。所以,ConcurrentHashMap中锁的粒度是槽,而不是整个数组,并发的性能很好。

  3. 扩容时会进行加锁处理,锁定的仍然是头节点。并且,支持多个线程同时对数组扩容,提高并发能力。每个线程需先以CAS操作抢任务,争抢一段连续槽位的数据转移权。抢到任务后,该线程会锁定槽内的头节点,然后将链表或树中的数据迁移到新的数组里。

  4. 查找数据时并不会加锁,所以性能很好。另外,在扩容的过程中,依然可以支持查找操作。如果某个槽还未进行迁移,则直接可以从旧数组里找到数据。如果某个槽已经迁移完毕,但是整个扩容还没结束,则扩容线程会创建一个转发节点存入旧数组,届时查找线程根据转发节点的提示,从新数组中找到目标数据。

加分回答 :ConcurrentHashMap实现线程安全的难点在于多线程并发扩容,即当一个线程在插入数据时,若发现数组正在扩容,那么它就会立即参与扩容操作,完成扩容后再插入数据到新数组。在扩容的时候,多个线程共同分担数据迁移任务,每个线程负责的迁移数量是 (数组长度 >>> 3) / CPU核心数。 也就是说,为线程分配的迁移任务,是充分考虑了硬件的处理能力的。多个线程依据硬件的处理能力,平均分摊一部分槽的迁移工作。另外,如果计算出来的迁移数量小于16,则强制将其改为16,这是考虑到目前服务器领域主流的CPU运行速度,每次处理的任务过少,对于CPU的算力也是一种浪费。

2、说说Spring Boot常用的注解

得分点: Spring Boot常用注解的作用

@SpringBootApplication注解: 在Spring Boot入口类中,唯一的一个注解就是@SpringBootApplication。它是Spring Boot项目的核心注解,用于开启自动配置,准确说是通过该注解内组合的@EnableAutoConfiguration开启了自动配置。

@EnableAutoConfiguration注解: @EnableAutoConfiguration的主要功能是启动Spring应用程序上下文时进行自动配置,它会尝试猜测并配置项目可能需要的Bean。自动配置通常是基于项目classpath中引入的类和已定义的Bean来实现的。在此过程中,被自动配置的组件来自项目自身和项目依赖的jar包中。

@Import注解: @EnableAutoConfiguration的关键功能是通过@Import注解导入的ImportSelector来完成的。从源代码得知@Import(AutoConfigurationImportSelector.class)是@EnableAutoConfiguration注解的组成部分,也是自动配置功能的核心实现者。

@Conditional注解: @Conditional注解是由Spring 4.0版本引入的新特性,可根据是否满足指定的条件来决定是否进行Bean的实例化及装配,比如,设定当类路径下包含某个jar包的时候才会对注解的类进行实例化操作。总之,就是根据一些特定条件来控制Bean实例化的行为。

3、说说Bean的生命周期

Spring Bean生命周期的四大部分以及详细步骤

Bean 生命周期大致分为 Bean 定义、Bean 的初始化、Bean的生存期和 Bean 的销毁4个部分。具体步骤如下:

  1. Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化

  2. Bean实例化后对将Bean的引入和值注入到Bean的属性中

  3. 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法

  4. 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入

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

  6. 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。

  7. 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用

  8. 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。

  9. 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。

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

创建,初始化,调用,销毁; bean的创建方式有四种,构造器,静态工厂,实例工厂,setter注入的方式。 spring在调用bean的时候因为作用域的不同,不同的bean初始化和创建的时间也不相同。 在作用域为singleton的时候,bean是随着容器一起被创建好并且实例化的, 在作用域为pritotype的时候,bean是随着它被调用的时候才创建和实例化完成。 然后程序就可以使用bean了,当程序完成销毁的时候,bean也被销毁。

4、synchronized和Lock有什么区别

synchronized是同步锁,可以修饰静态方法、普通方法和代码块。修饰静态方法时锁住的是类对象,修饰普通方法时锁住的是实例对象。当一个线程获取锁时,其他线程想要访问当前资源只能等当前线程释放锁。 synchronized是java的关键字,Lock是一个接口。 synchronized可以作用在代码块和方法上,Lock只能用在代码里。 synchronized在代码执行完或出现异常时会自动释放锁,Locl不会自动释放,需要在finally中释放。 synchronized会导致线程拿不到锁一直等待,Lock可以设置获取锁失败的超时时间。 synchronized无法获知是否获取锁成功,Lock则可以通过tryLock判断是否加锁成功。

5、String、StringBuffer、Stringbuilder有什么区别

得分点: 字符串是否可变,StringBuffer、StringBuilder线程安全问题

Java中提供了String,StringBuffer两个类来封装字符串,并且提供了一系列方法来操作字符串对象。

String是一个不可变类,也就是说,一个String对象创建之后,直到这个对象销毁为止,对象中的字符序列都不能被改变。

StringBuffer对象则代表一个字符序列可变的字符串,当一个StringBuffer对象被创建之后,我们可以通过StringBuffer提供的append()、insert()、reverse()、setCharAt()、setLength()、等方法来改变这个字符串对象的字符序列。当通过StringBuffer得到期待中字符序列的字符串时,就可以通过toString()方法将其转换为String对象。

StringBuilder类是JDK1.5中新增的类,他也代表了字符串对象。和StringBuffer类相比,它们有共同的父类AbstractStringBuilder,二者无论是构造器还是方法都基本相同,不同的一点是,StringBuilder没有考虑线程安全问题,也正因如此,StringBuilder比StringBuffer性能略高。因此,如果是在单线程下操作大量数据,应优先使用StringBuilder类;如果是在多线程下操作大量数据,应优先使用StringBuffer类。

1. 可变性

String 不可变

StringBuffer 和 StringBuilder 可变

2. 线程安全

String 不可变,因此是线程安全的

StringBuilder 不是线程安全的

StringBuffer 是线程安全的,内部使用 synchronized 进行同步

6、Redis如何与数据库保持双写一致性

得分点: 四种同步策略及其可能出现的问题,重试机制

保证缓存和数据库的双写一致性,共有四种同步策略:

即先更新缓存再更新数据库、先更新数据库再更新缓存、先删除缓存再更新数据库、先更新数据库再删除缓存。

先更新缓存的优点是:

每次数据变化时都能及时地更新缓存,这样不容易出现查询未命中的情况,但这种操作的消耗很大,如果数据需要经过复杂的计算再写入缓存的话,频繁的更新缓存会影响到服务器的性能。如果是写入数据比较频繁的场景,可能会导致频繁的更新缓存却没有业务来读取该数据。

删除缓存的优点是:

操作简单,无论更新的操作复杂与否,都是直接删除缓存中的数据。这种做法的缺点则是,当删除了缓存之后,下一次查询容易出现未命中的情况,那么这时就需要再次读取数据库。 那么对比而言,删除缓存无疑是更好的选择。

先操作数据库和后操作数据库的区别:

先删除缓存再操作数据库的话,如果第二步骤失败可能导致缓存和数据库得到相同的旧数据。先操作数据库但删除缓存失败的话则会导致缓存和数据库得到的结果不一致。

出现上述问题的时候,我们一般采用重试机制解决,而为了避免重试机制影响主要业务的执行,一般建议重试机制采用异步的方式执行。当我们采用重试机制之后由于存在并发,先删除缓存依然可能存在缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致的情况。 所以我们得到结论:先更新数据库、再删除缓存是影响更小的方案。如果第二步出现失败的情况,则可以采用重试机制解决问题。

7、请你说说==与equals()的区别

得分点: ==和equals()比较基本变量用法,==和equals()对比引用变量的用法

== 比较基本数据类型时,比较的是两个数值是否相等; 比较引用类型是,比较的是对象的内存地址是否相等。 equals() 没有重写时,Object默认以==来实现,即比较两个对象的内存地址是否相等; 重写以后,按照对象的内容进行比较

8、说说Redis的单线程架构

得分点: 单线程的前提,单线程的优劣,简单的io模型

  1. Redis的网络IO和键值对读写是由一个线程来完成的,但Redis的其他功能,例如持久化、异步删除、集群数据同步等操作依赖于其他线程来执行。

  2. 单线程可以简化数据结构和算法的实现,并且可以避免线程切换和竞争造成的消耗。但要注意如果某个命令执行时间过长,会造成其他命令的阻塞。

  3. Redis采用了io多路复用机制,这带给了Redis并发处理大量客户端请求的能力。

Redis单线程实现为什么这么快呢?

因为对服务端程序来说,线程切换和锁通常是性能杀手,而单线程避免了线程切换和竞争所产生的消耗。另外Redis的大部分操作是在内存上完成的,这是它实现高性能的一个重要原因;Redis还采用了IO多路复用机制,使其在网络IO操作中能并发处理大量的客户端请求,实现高吞吐率。

加分回答: Redis的单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的。而Redis的其他功能,如持久化、异步删除、集群数据同步等,则是依赖其他线程来执行的。所以,说Redis是单线程的只是一种习惯的说法,事实上它的底层不是单线程的。

9、如何实现Redis高可用

得分点: 哨兵模式、集群模式

主要有哨兵和集群两种方式可以实现Redis高可用。

哨兵: 哨兵模式是Redis的高可用的解决方案,它由一个或多个Sentinel实例组成Sentinel系统,可以监视任意多个主服务器以及这些主服务器属下的所有从服务器。当哨兵节点发现有节点不可达时,会对该节点做下线标识。如果是主节点下线,它还会和其他Sentinel节点进行“协商”,当大多数Sentinel节点都认为主节点不可达时,它们会选举出一个Sentinel节点来完成自动故障转移的工作,同时会将这个变化实时通知给Redis应用方。 哨兵节点包含如下的特征:

  1. 哨兵节点会定期监控数据节点,其他哨兵节点是否可达;

  2. 哨兵节点会将故障转移的结果通知给应用方;

  3. 哨兵节点可以将从节点晋升为主节点,并维护后续正确的主从关系;

  4. 哨兵模式下,客户端连接的是哨兵节点集合,从中获取主节点信息;

  5. 节点的故障判断是由多个哨兵节点共同完成的,可有效地防止误判;

  6. 哨兵节点集合是由多个哨兵节点组成的,即使个别哨兵节点不可用,整个集合依然是健壮的;

  7. 哨兵节点也是独立的Redis节点,是特殊的Redis节点,它们不存储数据,只支持部分命令。

集群: Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。虚拟槽分区具有如下特点:。

  1. 解耦数据和节点之间的关系,简化了节点扩容和收缩的难度;

  2. 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据;

  3. 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景

10、请你说说重载和重写的区别,构造方法能不能重写

得分点: 重载定义、重写定义

重载要求发生在同一个类中,多个方法之间方法名相同且参数列表不同。注意重载与方法的返回值以及访问修饰符无关。

重写发生在父类子类中,若子类方法想要和父类方法构成重写关系,则它的方法名、参数列表必须与父类方法相同。另外,返回值要小于等于父类方法,抛出的异常要小于等于父类方法,访问修饰符则要大于等于父类方法。若父类方法的访问修饰符为private,则子类不能对其重写。

其实除了二者都发生在方法之间,要求方法名相同之外并没有太大相同点。

加分回答: 同一个类中有多个构造器,多个构造器的形参列表不同就被称为构造器重载,构造器重载让Java类包含了多个初始化逻辑,从而允许使用不同的构造器来初始化对象。 构造方法不能重写。因为构造方法需要和类保持同名,而重写的要求是子类方法要和父类方法保持同名。如果允许重写构造方法的话,那么子类中将会存在与类名不同的构造方法,这与构造方法的要求是矛盾的。 父类方法和子类方法之间也有可能发生重载,因为子类会获得父类的方法,如果子类中定义了一个与父类方法名字相同但参数列表不同的方法,就会形成子类方法和父类方法的重载。

posted @ 2023-04-25 23:09  小謝同學  阅读(25)  评论(0编辑  收藏  举报