Java开发专家阿里P6-P7面试题大全及答案汇总(持续更新)

一、CPU100%问题如何快速定位

答案

1.执行top -c ,显示进程运行信息列表
  键入P (大写p),进程按照CPU使用率排序

2.找到最耗CPU的线程
  top -Hp 10765 ,显示一个进程的线程运行信息列表
  键入P (大写p),线程按照CPU使用率排序

3.查看堆栈,定位线程在干嘛,定位对应代码
 首先,将线程PID转化为16进制。
 工具:printf
 方法:printf "%x\n" 10768
 打印进程堆栈通过线程id
4.查看堆栈,找到线程在干嘛

工具:pstack/jstack/grep

方法:jstack 10765 | grep ‘0x2a34’ -C5 --color

二、TCP三次握手四次挥手过程

答案

TCP三次握手

所谓三次握手,是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。

  三次握手的目的是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号并交换 TCP 窗口大小信息.在socket编程中,客户端执行connect()时。将触发三次握手。

   (1) 第一次握手:建立连接时,客户端A发送SYN包(SYN=j)到服务器B,并进入SYN_SEND状态,等待服务器B确认。

   (2) 第二次握手:服务器B收到SYN包,必须确认客户A的SYN(ACK=j+1),同时自己也发送一个SYN包(SYN=k),即SYN+ACK包,此时服务器B进入SYN_RECV状态。

   (3) 第三次握手:客户端A收到服务器B的SYN+ACK包,向服务器B发送确认包ACK(ACK=k+1),此包发送完毕,客户端A和服务器B进入ESTABLISHED状态,完成三次握手。

完成三次握手,客户端与服务器开始传送数据。

TCP 四次挥手
  TCP的连接的拆除需要发送四个包,因此称为四次挥手。客户端或服务器均可主动发起挥手动作,在socket编程中,任何一方执行close()操作即可产生挥手操作。

  TCP连接是全双工的,因此每个方向都必须单独进行关闭。这原则是当一方完成它的数据发送任务后就能发送一个FIN来终止这个方向的连接。收到一个 FIN只意味着这一方向上没有数据流动,一个TCP连接在收到一个FIN后仍能发送数据。首先进行关闭的一方将执行主动关闭,而另一方执行被动关闭。

(1) TCP客户端发送一个FIN,用来关闭客户到服务器的数据传送。

(2) 服务器收到这个FIN,它发回一个ACK,确认序号为收到的序号加1。和SYN一样,一个FIN将占用一个序号。

(3) 服务器关闭客户端的连接,发送一个FIN给客户端。

(4) 客户端发回ACK报文确认,并将确认序号设置为收到序号加1

三、HTTP与HTTPS有什么区别?

答案

HTTP协议传输的数据都是未加密的,也就是明文的,因此使用HTTP协议传输隐私信息非常不安全,为了保证这些隐私数据能加密传输,于是网景公司设计了SSL(Secure Sockets Layer)协议用于对HTTP协议传输的数据进行加密,从而就诞生了HTTPS。

简单来说,HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全。

HTTPS和HTTP的区别主要如下:

1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。

2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。

3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。

4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

四、用过哪些Map类,都有什么区别,HashMap是线程安全的吗,并发下使用的Map是什么,他们 内部原理分别是什么,比如存储方式,hashcode,扩容,默认容量等。

答案

直接实现了map接口的主要有HashMap和AbstractMap以及Hashtable。而TreeMap和ConcurrentHashMap都继承与AbstractMap。、
区别:
1.HashMap的底层数据结构是数组加链表。每一个entry都有一个key,value,当不同的key經过hash函数运算之后得到了一个相同的值,这时候便发生了hash冲突。便会把value值存在一个数组里面。而多个entry便构成一个数组。允许key,value位null。采用链表可以有效的解决hash冲突。
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; //这是hashMap的默认容量。2^4为16.默认的加载因子为0.75,当数组元素的实际个数超过16 * 0.75时,便会进行扩容操作。大小为2*16,扩大一倍。
2.Hashtable方法是同步的,也就是线程安全的,key和value都不能为空。
3.CurrentHashMap是线程安全的,采用了分段锁,在java1.8之后放弃的分段锁的使用。
4.TreeMap底层的数据结构是有顺序访问的红黑树,

五、有没有有顺序的Map实现类,如果有,他们是怎么保证有序的。

答案

LinkedHashMap,TreeMap便是有顺序的map实现类。LinkedHashMap继承于HashMap。
LinkedHashMap保证有序的结构是双向链表,TreeMap保证有序的结构是红黑树。

六、说一说你对java.lang.Object对象中hashCode和equals方法的理解。在什么场景下需要重新实现这两个方法。

答案

原因:在object中equals方法内容是this==obj。而hashcode也就是通过一个值,可能是一个对象或者一个内存地址。经过hash运算过后得到一个值。这个值叫hash码值。
hash码值有一个特征。同一个对象经过无论多少次运算得到的结果都是一样的。而不同的对象经过运算,有可能一样,有可能不一样。
而在map集合中,不能存在相同的key值。
我们重写了equals方法。而不重写hashcode方法后果是怎样。我们创建该类的两个对象,两个对象赋予相同的值。然后依次put进hashmap中。hashmap中会通过对象得到hashcode。由于是两个不同的对象,hashcode并没有重写,此时有可能得到的是不同的hash值。然后存粗在了hashmap集合中不同的下标上。而在我们看来,他们两个对象的值相同,就是同一个对象,为什么还能在集合中存在两个呢。所以错误
在重写了equals方法后,并重写hashcode方法。还是上面的两个对象。由于重写了hashcode。两个对象的值相同,所以得到了同一个hash值。这时候通过equlas判断是否是同一个对象。由于重写了equals方法,所以得到的还是true。hashmap便会认为这两个对象是同一个对象。
其实就是一点。当我们重写了equals方法,即在我们眼中只要这个对象的值相同,即我们把他看作了同一个对象。而你要把两个不同的对象看成是同一个对象,就必须重写他的hashcode方法。
所以在我们没有重写equals方法时,哪怕两个对象的值一样,我们也看作是两个对象
在java中。String,Integer等类都重写了equals方法和hashcode方法。

七、有没有可能2个不相等的对象有相同的hashcode。

答案

有可能。这也是为什么,会产生hash冲突。当两个不同对象,得到了同一个hashcode。在hashmap中,即表示这两个对象的下标是相同的。解决hash冲突的方法有几种,在hash化,寻地址法。链地址法。hashmap中便采用了链地址法

八、java8的新特性。

答案

1.接口可以通过default关键字修饰方法,实现方法的具体内容,子类实现后,可直接调用该方法。
2.Lambda表达式。new Thread( () -> System.out.println(“In Java8, Lambda expression rocks !!”) ).start();
features.forEach(System.out::println);

九、什么情况下会发生栈内存溢出。

答案

什么是栈溢出,栈是存放方法的局部变量,参数,操作数栈,动态链接等。栈是每个线程私有的,一个方法便会创建一个栈帧。当方法执行创建的栈帧超过了栈的深度,这时候便会发生栈溢出。
常见的几种案列。
大量递归调用或无限递归
循环过多或死循环
局部变量过多
数组,List,map数据是否过大。
这些都有可能造成栈内存溢出

十、当出现了内存溢出,你怎么排错。

答案

可以在jvm中设置参数:
-XX:+HeapDumpOnOutOfMemoryError
JVM 就会在发生内存泄露时抓拍下当时的内存状态,也就是我们想要的堆转储文件。这种方式适合于生产环境。本文采用的这种方式
此时会获取到一个.hprof的文件,这个文件可以通过jmp工具,或者mat工具等查看,是一个二进制文件,
在这个文件我们可以清楚的看见,线程的个数,对象的个数,堆内存的使用。可分析出造成内存溢出是哪些对象,具体原因,根据原因来解决问题。

十一、你们线上应用的JVM参数有哪些。

答案

  • -Xms:520M 初始堆内存
  • -Xmx:1024M 最大堆内存
  • -Xmn:256M 新生代大小
  • -XX:NewSize=256M 设置新生代初始大小
  • -XX:MaxNewSize=256M 设置新生代最大值内存
  • -XX:PermSize=256M 设置永久代初始值大小
  • -XX:MaxPermSize=256M 设置永久最大值大小
  • -XX:NewRatio=4 设置老年代和新生代的比值。表示老年代比新生代为4:1
  • -XX:SurvivorRatio=8 设置新生代中Survivor区和eden区的比值,该比值为Eden区比Survivor区为8:2
  • -XX:MaxTenuringThreshold=7 表示一个对象在Survivor区移动了7次还没有被回收,则进入老年代。该值可减少full GC代频率

线程

  • -Xss:1M 设置每个线程栈大小

十二、volatile的原理,作用,能代替锁么。

答案

原理作用:原子性,可见性,有序性
原子性:要么不执行,要么执行完毕。和事物提交和回滚相似。
可见性:保证变量的值是真实的,最新的
有序性:防止jvm对指令重排序,优化排序等。
原理:在线程操作变量时,每次都会先从主存中获取改变量的值,操作完之后会马上把值刷新到主存中去,保证主存的值是最新的。也就成就了可见性。
不能替代锁。

十三、ThreadLocal用过么,用途是什么,原理是什么,用的时候要注意什么。

答案

当使用ThreadLocal存值时,首先是获取到当前线程对象,然后获取到当前线程本地变量Map,最后将当前使用的ThreadLocal和传入的值放到Map中,也就是说ThreadLocalMap中存的值是[ThreadLocal对象, 存放的值],这样做的好处是,每个线程都对应一个本地变量的Map,所以一个线程可以存在多个线程本地变量(即不同的ThreadLocal,就如1中所说,可以重写initialValue,返回不同类型的子类)。

十四、CAS机制是什么,如何解决ABA问题。

答案

内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做
解决ABA问题
JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。如果当前引用 == 预期引用,并且当前标志等于预期标志,则以原子方式将该引用和该标志的值设置为给定的更新值。

十五、TCP/IP如何保证可靠性,说说TCP头的结构。

答案

可靠性
1.是一个长连接
2.面向流,数据按顺序投递
3.tcp发出一个报文后,会启动一个定时器,等待响应报文,如果接收不到响应报文,将重发次消息

十六、数据库隔离级别有哪些,各自的含义是什么,MYSQL默认的隔离级别是是什么。

答案

数据库的隔离级别有4类
1.read-uncommitted (读未提交) 脏读,不可重复读,幻读
2.read-committed (不可重复读) 不可重复读,幻读
3.repeatable-read(可重复读)幻读
4.serializable (串行化)
mysql默认的隔离级别,repeatable-read(可重复读)

十七、什么是幻读。

答案

幻读:系统管理员a将数据库中所有的学生成绩从具体分数改为ABCDE等级,但是系统管理员b这时插入了一条具体分数的数据,当系统管理员a修改结束后,,发现还有一条没有修改,就好像发生了幻读一样。

十八、MYSQL有哪些存储引擎,各自优缺点。

答案

介绍5类常见的mysql存储引擎
1.MySAM
不支持事务,不支持外键,访问速度快,
2.InnoDB
健壮的事务型存储引擎,外键约束,支持自动增加AUTO_INCREMENT属性。
3.MEMORY
响应速度快,数据不可恢复
4.MERGE
meger表是几个相同的MyISAM表的聚合器
5.ARCHIVE
拥有很好的压缩机制,在记录被请求时会实时压缩,经常被用来当做仓库使用。

十九、如何解决幻读

答案

  • 行锁
  • 间隙锁

原理:将当前数据行与上一条数据和下一条数据之间的间隙锁定,保证此范围内读取的数据是一致的。

select * from T where number = 1 for update;

select * from T where number = 1 lock in share mode;

insert

update

delete

二十、Redis如何解决缓存穿透、缓存雪崩、缓存击穿

1.缓存穿透

  1. 当业务系统发起某一个查询请求时,首先判断缓存中是否有该数据;
  2. 如果缓存中存在,则直接返回数据;
  3. 如果缓存中不存在,则再查询数据库,然后返回数据。

业务系统要查询的数据根本就存在!当业务系统发起查询时,按照上述流程,首先会前往缓存中查询,由于缓存中不存在,然后再前往数据库中查询。由于该数据压根就不存在,因此数据库也返回空。这就是缓存穿透。

综上所述:业务系统访问压根就不存在的数据,就称为缓存穿透。

答案

1.之所以发生缓存穿透,是因为缓存中没有存储这些空数据的key,导致这些请求全都打到数据库上。

那么,我们可以稍微修改一下业务系统的代码,将数据库查询结果为空的key也存储在缓存中。当后续又出现该key的查询请求时,缓存直接返回null,而无需查询数据库。

但是这样有个弊端就是缓存太多空值占用了更多的空间,可以通过给缓存层空值设立一个较短的过期时间来解决,例如60s。

2.将数据库中所有的查询条件,放入布隆过滤器中,当一个查询请求过来时,先经过布隆过滤器进行查,如果判断请求查询值存在,则继续查;如果判断请求查询不存在,直接丢弃。

2.缓存雪崩

缓存其实扮演了一个保护数据库的角色。它帮数据库抵挡大量的查询请求,从而避免脆弱的数据库受到伤害。

如果缓存因某种原因发生了宕机,那么原本被缓存抵挡的海量查询请求就会像疯狗一样涌向数据库。此时数据库如果抵挡不了这巨大的压力,它就会崩溃。

这就是缓存雪崩。

答案

  • 使用缓存集群,保证缓存高可用,即使个别节点、个别机器、甚至是机房宕掉,依然可以提供服务。
  • Hystrix是一款开源的“防雪崩工具”,它通过 熔断、降级、限流三个手段来降低雪崩发生后的损失

3.缓存击穿

我们一般都会给缓存设定一个失效时间,过了失效时间后,该数据库会被缓存直接删除,从而一定程度上保证数据的实时性。

但是,对于一些请求量极高的热点数据而言,一旦过了有效时间,此刻将会有大量请求落在数据库上,从而可能会导致数据库崩溃

如果某一个热点数据失效,那么当再次有该数据的查询请求时就会前往数据库查询。但是,从请求发往数据库,到该数据更新到缓存中的这段时间中,

由于缓存中仍然没有该数据,因此这段时间内到达的查询请求都会落到数据库上,这将会对数据库造成巨大的压力。此外,当这些请求查询完成后,都会重复更新缓存。

答案

互斥锁此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。

二十一、Redis数据类型有哪几种

Redis主要有5种数据类型,包括String,List,Set,Zset,Hash。

STRING 字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作
LIST 列表 从两端压入或者弹出元素
对单个或者多个元素进行修剪,
只保留一个范围内的元素
SET 无序集合 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素
HASH 包含键值对的无序散列表 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在
ZSET 有序集合 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名

二十二、Redis扩展数据类型有哪几种

答案

Redis GEO 主要用于存储地理位置信息,并对存储的信息进行操作,该功能在 Redis 3.2 版本新增。

​BitMap 原本的含义是用一个比特位来映射某个元素的状态。由于一个比特位只能表示 0 和 1 两种状态,所以 BitMap 能映射的状态有限,但是使用比特位的优势是能大量的节省内存空间。

Redis 2.8.9 版本就更新了 Hyperloglog 数据结构!Redis Hyperloglog 基数统计的算法!优点:占用的内存是固定,2^64 不同的元素的技术,只需要废 12KB内存!如果要从内存角度来比较的话 Hyperloglog 首选!网页的 UV (一个人访问一个网站多次,但是还是算作一个人!)

二十三、什么是双亲委托模式

答案

即一个类在加载过程中,会向上传递,最终到达启动类加载,启动类加载器,查看该类是否是核心库里面的类同名,如果是,只会加载核心库的类,不会加载该类。如果改类与核心库不同名,启动类加载器也不会加载该类,而会交给下一级加载器进行处理。
双亲委托模式便是至下向上传递。至上往下加载。

二十四、简述线程生命周期状态

答案

  • New(初始化状态)

  • Runnable(就绪状态)

  • Running(运行状态)

  • Blocked(阻塞状态)

  • Terminated(终止状态)

二十五、数据库乐观锁和悲观锁如何实现

答案

之前写过详细的实现

https://blog.csdn.net/qq_17025903/article/details/105839840

二十六、Synchronized 的锁升级过程

答案

synchronized 的锁升级包括四种状态。
无锁态 —> 偏向锁 —> 轻量级锁 —> 重量级锁
锁只能升级,不能降级,目的就是为了提高获得锁和释放锁的效率。

锁的状态变化

一、无锁态
在程序没有执行的时候,或者说代码块没有执行的时候,synchronized 并不会给代码加锁,这个阶段就是无锁的状态。
二、偏向锁
经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程获得,所以为了降低获得锁的代价,引用了偏向锁。偏向锁是在对象的对象头中将线程的ID添加进去,为了让线程在下次进入和退出同步快时不需要CAS操作加锁和解锁。偏向锁在获取锁时,只是简单测试一下对象头的Mark Word 里面是否存储着当前线程的ID,如果成功了,表示当前线程获得了锁。如果失败了,就通过CAS操作来获取锁。
偏向锁的撤销
偏向锁的撤销采用的竞争出现才释放锁的机制。也就是说,当其他线程竞争同一个锁的时候,持有偏向锁的线程才会释放锁。

可以使用 -XX:BiasedLockingStartupDelay=0 关闭偏向锁的延迟
可以使用 -XX:-UserBiasedLocking=false 关闭偏向锁

三、轻量级锁
1. 轻量级锁加锁
在发生偏向锁的基础上,如果其他线程想要获取锁,通过CAS操作来将Mark Word 中的线程ID改为当前线程,如果失败,则当前线程则会尝试使用自旋操作来获取锁。在发生一定次数的自旋后仍不能获得锁,那么此时就会升级为重量级锁。

锁的优缺点对比

二十七、Ribbon和Feign的区别

答案

Ribbon和Feign都是用于调用其他服务的,不过方式不同。

1.启动类使用的注解不同,Ribbon用的是@RibbonClient,Feign用的是@EnableFeignClients。

2.服务的指定位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用@FeignClient声明。

3.调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTemplate发送给其他服务,步骤相当繁琐。

  Feign则是在Ribbon的基础上进行了一次改进,采用接口的方式,将需要调用的其他服务的方法定义成抽象方法即可,

  不需要自己构建http请求。不过要注意的是抽象方法的注解、方法签名要和提供服务的方法完全一致。

二十八、zookeeper和Eureka区别

答案

Zookeeper保证CP
当向注册中心查询服务列表时,我们可以容忍注册中心返回的是几分钟以前的注册信息,但不能接受服务直接down掉不可用。也就是说,服务注册功能对可用性的要求要高于一致性。但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点会重新进行leader选举。问题在于,选举leader的时间太长,30 ~ 120s, 且选举期间整个zk集群都是不可用的,这就导致在选举期间注册服务瘫痪。在云部署的环境下,因网络问题使得zk集群失去master节点是较大概率会发生的事,虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用是不能容忍的。
Eureka保证AP(很多时间为了保证服务高可用,我们的保证AP)
Eureka看明白了这一点,因此在设计时就优先保证可用性。Eureka各个节点都是平等的,几个节点挂掉不会影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。而Eureka的客户端在向某个Eureka注册或时如果发现连接失败,则会自动切换至其它节点,只要有一台Eureka还在,就能保证注册服务可用(保证可用性),只不过查到的信息可能不是最新的(不保证强一致性)。除此之外,Eureka还有一种自我保护机制,如果在15分钟内超过85%的节点都没有正常的心跳,那么Eureka就认为客户端与注册中心出现了网络故障,此时会出现以下几种情况: 
1. Eureka不再从注册列表中移除因为长时间没收到心跳而应该过期的服务 
2. Eureka仍然能够接受新服务的注册和查询请求,但是不会被同步到其它节点上(即保证当前节点依然可用) 
3. 当网络稳定时,当前实例新的注册信息会被同步到其它节点中
因此, Eureka可以很好的应对因网络故障导致部分节点失去联系的情况,而不会像zookeeper那样使整个注册服务瘫痪。
5. 总结
Eureka作为单纯的服务注册中心来说要比zookeeper更加“专业”,因为注册服务更重要的是可用性,我们可以接受短期内达不到一致性的状况。不过Eureka目前1.X版本的实现是基于servlet的java web应用,它的极限性能肯定会受到影响。期待正在开发之中的2.X版本能够从servlet中独立出来成为单独可部署执行的服务。

二十九、Mybatis中的一级缓存和二级缓存

答案

【一级缓存】

它指的是Mybatis中SqlSession对象的缓存

  • 当我们执行查询之后,查询的结果会同时存入到SqlSession为我们提供一块区域中。该区域的结构是一个Map。

  • 当我们再次查询同样的数据,Mybatis会先去SqlSession中查询是否有,有的话直接拿出来用。

  • 当SqlSession对象消失时,Mybatis的一级缓存也就消失了。

【二级缓存】

它指的是Mybatis中SqlSessionFactory对象的缓存。

由同一个SqlSessionFactory对象创建的SqlSession共享其缓存

二级缓存的使用步骤:

  1. 让Mybatis框架支持二级缓存(在SqlMapConfig.xml中配置)
  2. 让当前的映射文件支持二级缓存(在IUserDao.xml中配置)
  3. 让当前的操作支持二级缓存(在select标签中配置)

三十、MQ如何保证消息不丢失

答案

可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。

// 开启事务
channel.txSelect
try {
    // 这里发送消息
} catch (Exception e) {
    channel.txRollback

    // 这里再次重发这条消息
}

// 提交事务
channel.txCommit

三十一、堆溢出,栈溢出的出现场景以及解决方案

答案

1.堆溢出的情况及解决方案

  • OutofMemoryError:Java heap space 堆内存中的空间不足以存放新创建的对象
  • OutOfMemoryError: GC overhead limit exceeded 超过98%的时间用来做GC并且回收了不到2%的堆内存
  • OutOfMemoryError: Direct buffer memory 堆外内存

OutOfMemoryError: Metaspace 元数据区(Metaspace) 已被用满

解决方案:-XX:MaxMetaspaceSize=512m

2.栈溢出几种情况及解决方案

  • 局部数组过大。当函数内部的数组过大时,有可能导致堆栈溢出。
  • 递归调用层次太多。递归函数在运行时会执行压栈操作,当压栈次数太多时,也会导致堆栈溢出。
  • 指针或数组越界。这种情况最常见,例如进行字符串拷贝,或处理用户输入等等。

解决这类问题的办法有两个

  • 增大栈空间
  • 改用动态分配,使用堆(heap)而不是栈(stack)
  • 直接查询生产环境服务器内存占用情况,通过命令定位到具体的那行代码

三十二、sleep()、join()、yield()有什么区别?

答案

1.sleep()

方法会让当前线程休眠(x)毫秒,线程由运行中的状态进入不可运行状态,睡眠时间过后线程会再进入可运行状态。

sleep() 方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入阻塞状态,该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。但是 sleep() 方法不会释放“锁标志”,也就是说如果有 synchronized 同步块,其他线程仍然不能访问共享数据。

2.join() 

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

3.yield() 

方法可暂停当前线程与其他等待的线程竞争CPU资源执行高优先级的线程,若无其他相同或更高优先级的线程,则继续执行。

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

三十三、Runnable和Thread区别和比较

  1. java不允许多继承,因此实现了Runnable接口的类可以再继承其他类,方便资源共享。
  2. Runnable是实现其接口即可,Thread 实现方式是继承其类

三十四、Java线程中run和start方法的区别

答案

1.start

用start方法来启动线程,真正实现了多线程运行,这时无需等待run方法体代码执行完毕而直接继续执行下面的代码。通过调用Thread类的start()方法来启动一个线程,这时此线程处于就绪(可运行)状态,并没有运行, 然后通过此Thread类调用方法run()来完成其运行操作的, 这里方法run()称为线程体,它包含了要执行的这个线程的内容, Run方法运行结束, 此线程终止。然后CPU再调度其它线程。 一旦得到cpu时间片,就开始执行run()方法,这里方法 run()称为线程体,它包含了要执行的这个线程的内容,Run方法运行结束,此线程随即终止。

2.run

run()方法只是类的一个普通方法而已,如果直接调用Run方法,程序中依然只有主线程这一个线程,其程序执行路径还是只有一条,还是要顺序执行,还是要等待run方法体执行完毕后才可继续执行下面的代码,这样就没有达到写线程的目的。总结:调用start方法方可启动线程,而run方法只是thread的一个普通方法调用,还是在主线程里执行。这两个方法应该都比较熟悉,把需要并行处理的代码放在run()方法中,start()方法启动线程将自动调用 run()方法,这是由jvm的内存机制规定的。并且run()方法必须是public访问权限,返回值类型为void。

三十五、JDK8新特性forEach循环写法

答案

public static void test5(List<String> list) {
  //list.forEach(System.out::println);和下面的写法等价
  list.forEach(str->{
    System.out.println(str);
  });
}

三十六、Set排序方法

答案

使用 Comparator比较器

1.平常写法

public class App {
 
    public static void main( String[] args ) {
        Set<String> set = new HashSet<>();
        set.add("1");
        set.add("2");
        set.add("5");
        set.add("4");
        set.add("3");
        System.out.println(set.toString());
        Set<String> sortSet = new TreeSet<String>(new Comparator<String>() {
            @Override
            public int compare(String o1, String o2) {
                return o2.compareTo(o1);//降序排列
            }
        });
        sortSet.addAll(set);
        System.out.println(sortSet.toString());
 
    }
}

2.lambda

public class App
{
 
    public static void main( String[] args ) {
        Set<String> set = new HashSet<>();
        set.add("2");
        set.add("1");
        set.add("5");
        set.add("3");
        set.add("4");
        System.out.println(set.toString());
        Set<String> sortSet = new TreeSet<String>((o1, o2) -> o2.compareTo(o1));
        sortSet.addAll(set);
        System.out.println(sortSet.toString());
 
    }
}

三十七、接口和抽象类的区别

答案

  1. 抽象类只能继承一次,但是可以实现多个接口
  2. 接口和抽象类必须实现其中所有的方法,抽象类中如果有未实现的抽象方法,那么子类也需要定义为抽象类。抽象类中可以有非抽象的方法
  3. 接口中的变量必须用 public static final 修饰,并且需要给出初始值。所以实现类不能重新定义,也不能改变其值。
  4. 接口中的方法默认是 public abstract,也只能是这个类型。不能是 static,接口中的方法也不允许子类覆写,抽象类中允许有static 的方法

三十八、SpringBoot启动原理简述

答案

主配置类入口

@SpringBootApplication
public class DevServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(DevServiceApplication.class,args);
    }
}

1.SpringApplication这个类主要做了4件事情:

  1. 推断应用的类型是普通项目还是Web项目
  2. 查找并加载所有的可用初始化器,设置到initializers属性中
  3. 找出所有的应用程序监听器,用于处理上下文获取Bean,设置到listeners属性中
  4. 推断并设置main方法的定义类,并找到运行的方法

2.进入run方法

  1. 创建了应用的监听器SpringApplicationRunListeners并开始监听
  2. 加载SpringBoot配置环境(ConfigurableEnvironment),如果是通过web容器发布,会加载StandardEnvironment,其最终也是继承了ConfigurableEnvironment
  3. 配置环境(Environment)加入到监听器对象中(SpringApplicationRunListeners)
  4. 创建run()返回对象:ConfigurableApplicationContext(应用配置上下文),根据环境决定创建Web的ioc还是普通的ioc
  5. prepareContext方法将listeners、environment、applicationArguments、banner等重要组件与上下文对象关联
  6. 接下来的refreshContext(context)方法(初始化方法如下)将是实现spring-boot-starter-*(mybatis、redis等)自动化配置的关键,包括spring.factories的加载,bean的实例化等核心工作
  7. 配置结束后,Springboot做了一些基本的收尾工作,返回了应用环境上下文。回顾整体流程,Springboot的启动,主要创建了配置环境(environment)、事件监听(listeners)、应用上下文(applicationContext),并基于以上条件,在容器中开始实例化我们需要的Bean,至此,通过SpringBoot启动的程序已经构造完成。

三十九、Https三次握手过程

答案

1. 客户端发起HTTPS请求

2. 服务端的配置

采用HTTPS协议的服务器必须要有一套数字证书,可以是自己制作或者CA证书。区别就是自己颁发的证书需要客户端验证通过,才可以继续访问,而使用CA证书则不会弹出提示页面。这套证书其实就是一对公钥和私钥。公钥给别人加密使用,私钥给自己解密使用。

3. 传送证书

这个证书其实就是公钥,只是包含了很多信息,如证书的颁发机构,过期时间等。

4. 客户端解析证书

这部分工作是有客户端的TLS来完成的,首先会验证公钥是否有效,比如颁发机构,过期时间等,如果发现异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个随即值,然后用证书对该随机值进行加密。

5. 传送加密信息

这部分传送的是用证书加密后的随机值,目的就是让服务端得到这个随机值,以后客户端和服务端的通信就可以通过这个随机值来进行加密解密了。

6. 服务段解密信息

服务端用私钥解密后,得到了客户端传过来的随机值(私钥),然后把内容通过该值进行对称加密。所谓对称加密就是,将信息和私钥通过某种算法混合在一起,这样除非知道私钥,不然无法获取内容,而正好客户端和服务端都知道这个私钥,所以只要加密算法够彪悍,私钥够复杂,数据就够安全。

7. 传输加密后的信息

这部分信息是服务段用私钥加密后的信息,可以在客户端被还原。

8. 客户端解密信息

客户端用之前生成的私钥解密服务段传过来的信息,于是获取了解密后的内容。

PS: 整个握手过程第三方即使监听到了数据,也束手无策。

四十、SSL与TLS的区别

答案

这两者是同一码事,SSL协议是TLS协议的前身
TLS 是传输层安全性协议(英语:Transport Layer Security,缩写作 TLS)。
SSL 是安全套接字层协议(Secure Sockets Layer,缩写作 SSL)。
一般情况下将二者写在一起TLS/SSL,我们可以将二者看做同一类协议,只不过TLS是SSL的升级版。

四十一、HashMap扩容机制

答案

首先resize()方法进行扩容,会拿到当前容量的大小,如果容量等于0的话,就会给他一个初始容量大小16,然后设置临界值为初始容量16 * 负载因子 0.75,也就是12了,然后将扩容好的tab返回

如果容量大于0的话,就会去判断当前容量是否大于最大限制容量 2^30 次幂,如果会大于的话,就设置临界值为 2^31 - 1,返回oldTab

如果当前容量的两倍小于最大限制容量,并且大于等于初始容量16的话,就设置新临界值为当前临界值的两倍,然后新建一个tab,将oldTab的数据放到newTab中,这个时候会rehash,然后将newTab返回,这就是HashMap的扩容机制了。

四十二、ArrayList扩容机制

答案

arraylist的底层是用数组来实现的。我们初始化一个arraylist集合还没有添加元素时,其实它是个空数组,只有当我们添加第一个元素时,内部会调用calculateCapacity方法并返回最小容量10,也就是说arraylist初始化容量为10。

当最小容量大于当前数组的长度时,便开始可以扩容了,arraylist扩容的真正计算是在一个grow()里面,新数组大小是旧数组的1.5倍,如果扩容后的新数组大小还是小于最小容量,那新数组的大小就是最小容量的大小,后面会调用一个Arrays.copyof方法

这个方法是真正实现扩容的步骤。

四十三、JAVA8的ConcurrentHashMap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。

答案

jdk1.8之后ConcurrentHashMap取消了segment分段锁,而采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

四十四、Redis 持久化存储方案和区别

答案

RDB

持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

(既然我的数据都在内存中存放着,最简单的就是遍历一遍把它们全都写入文件中。为了节约空间,我定义了一个二进制的格式,把数据一条一条码在一起,生成了一个RDB文件,

不过数据量有点大,要是全部备份一次得花不少时间所以复制出一个子进程去做这件事)

AOF

持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

(把我执行的所有写入命令都记录下来,专门写入了一个文件)

区别

  • AOF优先级高于RDB
    • 如果Redis服务器同时开启了RDB和AOF, 那么宕机重启之后会优先从AOF中恢复数据
  • RDB体积小于AOF
    • 由于RDB在备份的时候会对数据进行压缩, 而AOF是逐条保存命令, 所以RDB体积比AOF小
  • RDB恢复速度比AOF恢复速度快
    • 由于AOF是通过逐条执行命令的方式恢复数据, 而RDB是通过加载预先保存的数据恢复数据
    • 所以RDB的恢复速度比AOF快
  • AOF数据安全性高于RDB
    • 由于AOF可以逐条写入命令, 而RDB只能定期备份数据, 所以AOF数据安全性高于RDB
  • 所以综上所述, 两者各有所长, 两者不是替代关系而是互补关系

四十五、mysql为什么要做主从复制?主从复制原理是什么?主备切换实现原理?

答案

1.为什么要做主从复制

  1. 在业务复杂的系统中,有这么一个情景,有一句sql语句需要锁表,导致暂时不能使用读的服务,那么就很影响运行中的业务,使用主从复制,让主库负责写,从库负责读,这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运行。
  2. 做数据的热备,主库宕机后能够及时替换主库,保证业务可用性。
  3. 架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。

2.主从复制原理

  1. 主库db的更新事件(update、insert、delete)被写到binlog
  2. 主库创建一个binlog dump thread,把binlog的内容发送到从库
  3. 从库启动并发起连接,连接到主库
  4. 从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log(中继日志)
  5. 从库启动之后,创建一个SQL线程,从relay log里面读取内容,从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db

3.主备切换原理

一、概述

Keepalived看名字就知道,保持存活,在网络里面就是保持在线了,也就是所谓的高可用或热备,用来防止单点故障(单点故障是指一旦某一点出现故障就会导致整个系统架构的不可用)的发生,那说到keepalived不得不说的一个协议不是VRRP协议,可以说这个协议就是keepalived实现的基础。

二、实现原理

然后由keepalived配置文件可以知道,mysql关闭的话,将会执行keepalived_check_mysql.sh这一脚本。这个脚本在执行的时候,会判断mysql的状态,如果mysql关闭了,将会关闭主服务器上的keepalived。主服务器上的keepalived一旦关闭,那么从服务器马上变为主服务器,为用户提供服务。

四十六、线程池种类都有哪几种。

答案

1.带有缓冲区的线程池(newCachedThreadPool)

创建一个带有缓冲区的线程池,可根据需要创建新线程,在执行线程任务时先检测线程池是否存在可用线程,如果存在则直接使用,如果不存在则创建新线程然后使用。如果线程池中的线程空闲时间到达60秒后自动回收该线程。适于执行短期线程任务

        //获得一个带有缓冲区的线程池
        ExecutorService executorService  = Executors.newCachedThreadPool();
        //创建一个基于Callable的线程池
        Pool2 pool2 = new Pool2();
        for (int i=0;i<10;i++) {
          Future<Object> future =  executorService.submit(pool2); //future表示异步计算的结果
            //String str = (String) future.get();
           // System.out.println(str);
        }
        executorService.shutdown();//关闭线程池

2.固定数量的缓冲池(newFixedThreadPool)

创建一个固定数量的线程池,线程池中的线程数量固定,如果没有空闲线程则新任务处于等待状态,空闲线程不会被回收,适用于执行长期线程任务

 ExecutorService executorService = Executors.newFixedThreadPool(3);//固定数量为3,核心线程数
        Pool2 pool2 =new Pool2();
        for(int i=0;i<10;i++){
            Future future =executorService.submit(pool2);
        }
        executorService.shutdown();//一共有10个线程数,核心的有三个也是最大容量,那么剩下的7个被

3. 调度线程池(newScheduledThreadPool)

创建一个调度型线程池,可以执行定时任务

 ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        /***
         *  启动定时任务
         *  参数1:线程任务
         *  参数2:延迟时间 程序多少时间后执行
         *  参数3:间隔时间
         *  参数4:时间单位
         */
        scheduledExecutorService.scheduleAtFixedRate(new Pool1(),0,1000, TimeUnit.MILLISECONDS);
//定时器:Timer,TimerTask
        scheduledExecutorService.shutdown();

4.自定义线程池(ThreadPoolExecutor)

创建一个自定义线程池,可以自定义参数。

 ArrayBlockingQueue queue = new ArrayBlockingQueue(10);//创建线程等待队列
    ThreadPoolExecutor executor = new ThreadPoolExecutor(5,10,100, TimeUnit.MILLISECONDS,queue);
    Pool2 pool2 = new Pool2();
    for(int i=0;i<10;i++){
       executor.submit(pool2);  
    }

四十七、线程池任务队列都有哪几种。

答案

任务队列(BlockingQueue)指存放被提交但尚未被执行的任务的队列。包括以下几种类型:直接提交的、有界的、无界的、优先任务队列。

1.1 直接提交的任务队列(SynchronousQueue)

  1.  SynchronousQueue没有容量。
  2.  提交的任务不会被真实的保存在队列中,而总是将新任务提交给线程执行。如果没有空闲的线程,则尝试创建新的线程。如果线程数大于最大值maximumPoolSize,则执行拒绝策略。

1.2 有界的任务队列(ArrayBlockingQueue)

  1. 创建队列时,指定队列的最大容量。
  2. 若有新的任务要执行,如果线程池中的线程数小于corePoolSize,则会优先创建新的线程。若大于corePoolSize,则会将新任务加入到等待队列中。
  3. 若等待队列已满,无法加入。如果总线程数不大于线程数最大值maximumPoolSize,则创建新的线程执行任务。若大于maximumPoolSize,则执行拒绝策略。

1.3 无界的任务队列(LinkedBlockingQueue)

  1. 与有界队列相比,除非系统资源耗尽,否则不存在任务入队失败的情况。
  2. 若有新的任务要执行,如果线程池中的线程数小于corePoolSize,线程池会创建新的线程。若大于corePoolSize,此时又没有空闲的线程资源,则任务直接进入等待队列。
  3. 当线程池中的线程数达到corePoolSize后,线程池不会创建新的线程。
  4. 若任务创建和处理的速度差异很大,无界队列将保持快速增长,直到耗尽系统内存。
  5. 使用无界队列将导致在所有 corePoolSize 线程都忙时,新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize(因此,maximumPoolSize 的值也就无效了)。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。

1.4 优先任务队列(PriorityBlockingQueue)

  1.   带有执行优先级的队列。是一个特殊的无界队列。
  2.  ArrayBlockingQueue和LinkedBlockingQueue都是按照先进先出算法来处理任务。而PriorityBlockingQueue可根据任务自身的优先级顺序先后执行(总是确保高优先级的任务先执行)。

四十八、TCP/IP四层模型分别是哪四层?

答案

  1. 应用层:应用程序间沟通的层,如简单电子邮件传输(SMTP)、文件传输协议(FTP)、网络远程访问协议(Telnet)等。
  2. 传输层:在此层中,它提供了节点间的数据传送服务,如传输控制协议(TCP)、用户数据报协议(UDP)等,TCP和UDP给数据包加入传输数据并把它传输到下一层中,这一层负责传送数据,并且确定数据已被送达并接收。
  3. 网络层:负责提供基本的数据封包传送功能,让每一块数据包都能够到达目的主机(但不检查是否被正确接收),如网际协议(IP)。
  4. 网络接口层(物理层+数据链路层):对实际的网络媒体的管理,定义如何使用实际网络(如Ethernet、Serial Line等)来传送数据。

物理层

物理层规定:为传输数据所需要的物理链路创建、维持、拆除,而提供具有机械的,电子的,功能的和规范的特性,确保原始的数据可在各种物理媒体上传输,为设备之间的数据通信提供传输媒体及互连设备,为数据传输提供可靠的环境。 

数据链路层

主要提供链路控制(同步,异步,二进制,HDLC),差错控制(重发机制),流量控制(窗口机制)

四十九、拦截器和过滤器区别

答案

  1. 拦截器是基于java的反射机制的,而过滤器是基于函数的回调。
  2. 拦截器不依赖于servlet容器,而过滤器依赖于servlet容器。
  3. 拦截器只对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
  4. 拦截器可以访问action上下文、值、栈里面的对象,而过滤器不可以。
  5. 在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
  6. 拦截器可以获取IOC容器中的各个bean,而过滤器不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。

五十、wait和sleep区别

答案

sleep()

  1. 属于Thread类,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态
  2. sleep方法没有释放锁
  3. sleep必须捕获异常
  4. sleep可以在 任何地方使用

wait()

  1. 属于Object,一旦一个对象调用了wait方法,必须要采用notify()和notifyAll()方法唤醒该进程
  2. wait方法释放了锁
  3. wait不需要捕获异常
  4. wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用

五十一、线程池4种拒绝策略

答案

  1. ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 
  2. ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 
  3. ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,执行后面的任务,不抛出异常。 
  4. ThreadPoolExecutor.CallerRunsPolicy:由调用线程池的线程(比如main线程)处理该任务 ,不抛出异常,没有丢弃任务。

五十二、线程池中的7个参数

答案

  1. corePollSize:核心线程数。在创建了线程池后,线程中没有任何线程,等到有任务到来时才创建线程去执行任务。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
  2. maximumPoolSize:最大线程数。表明线程中最多能够创建的线程数量。
  3. keepAliveTime:空闲的线程保留的时间。
  4. TimeUnit:空闲线程的保留时间单位。
  5. BlockingQueue<Runnable>:阻塞队列,存储等待执行的任务。参数有ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue可选。
  6. ThreadFactory:线程工厂,用来创建线程
  7. RejectedExecutionHandler:队列已满,而且任务量大于最大线程的异常处理策略。有以下取值 

五十二、数据库中truncate、delete、drop三种删除的区别

答案

1.drop:drop table 表名

      删除内容和定义,并释放空间。执行drop语句,将使此表的结构一起删除。

2.truncate (清空表中的数据):truncate table 表名

      删除内容、释放空间但不删除定义(也就是保留表的数据结构)。与drop不同的是,只是清空表数据而已。

      truncate不能删除行数据,虽然只删除数据,但是比delete彻底,它只删除表数据。

3.delete:delete from 表名 (where 列名 = 值)

       与truncate类似,delete也只删除内容、释放空间但不删除定义;但是delete即可以对行数据进行删除,也可以对整表数据进行删除。

更多细节

1、delete每次删除一行时,都会将该行的删除操作作为事务记录在日志中。以便进行回滚操作。

2、执行速度:drop > truncate > delete:因为delete每执行一次,都要在事务日志中记录一次。所以最慢

3、delete语句是数据库操作语言(dml),这个操作会放到rollback segement(回滚分段)中,事务提交以后才会生效,若有相应的trigger(触发),执行的时候将被触发。

4、truncate、drop是数据库定义语言(ddl),操作立即生效,原数据不放到rollback segment中,不能回滚,操作不触发trigger

5、truncate语句执行以后,id标识还是按照顺序排列,保持连续;而delete语句执行后,id标识不连续。

注意事项

1、由于truncate、drop立即生效,不能回滚,所以谨慎使用。

2、采用delete删除数据时,和where连用最保险,且要有足够大的回滚段,防止删除错误数据好及时恢复。

3、对于外键约束引用的表,不能使用truncate table,而应该使用带where的delete语句,由于truncate table不记录在日志中,所以它不能激活触发器。
 

五十三、如何合理的设置线程池大小

答案

任务性质可分为:CPU密集型任务,IO密集型任务,混合型任务。

  • CPU密集型任务:  主要是执行计算任务,响应时间很快,cpu一直在运行,这种任务cpu的利用率很高
  • IO密集型任务:主要是进行IO操作,执行IO操作的时间较长,这是cpu出于空闲状态,导致cpu的利用率不高

CPU密集型任务

尽量使用较小的线程池,一般为CPU核心数+1。

IO密集型任务

可以使用稍大的线程池,一般为2*CPU核心数+1。

因为IO操作不占用CPU,不要让CPU闲下来,应加大线程数量,因此可以让CPU在等待IO的时候去处理别的任务,充分利用CPU时间。

混合型任务

可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。

只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。

因为如果划分之后两个任务执行时间相差甚远,那么先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失

依赖其他资源

对线程池大小的估算公式:

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为8,那么根据上面这个公式估算得到:((0.5+1.5)/0.5)*8=32。
 

五十四、CAS原理简述和解决ABA问题

答案

一、概念

CAS(Compare and Swap), 翻译成比较并交换。

CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。

二、原理图

CAS具体执行时,当且仅当预期值A符合内存地址V中存储的值时,就用新值U替换掉旧值,并写入到内存地址V中。否则不做更新。

三、CAS存在的ABA问题及解放方案

CAS在修改主内存值之前,需要检查主内存的值有没有被改变,如果没有改变才进行更新。但是仍然会存在一种情况如下:

  1. 线程1读取主内存值A,然后修改, 但是还没有写回主内存
  2. 线程2 将主内存的A 改为 B
  3. 线程3 将主内存的B 改为 A
  4. 线程1 再次读取主内存值A, 判断值没有改变,执行写入

解决方案
    使用AtomicStampedReference的版本号机制,在每次写入操作时,同时修改版本号,然后每次比较时,不但比较值是否改变,还比较版本号是否一致。如果都一致,才进行修改。

五十五、Synchronized 关键字原理

答案

一、实现原理:

JVM 是通过进入、退出 对象监视器(Monitor) 来实现对方法、同步块的同步的,而对象监视器的本质依赖于底层操作系统的 互斥锁(Mutex Lock) 实现。

具体实现是在编译之后在同步方法调用前加入一个monitor.enter指令,在退出方法和异常处插入monitor.exit的指令。

对于没有获取到锁的线程将会阻塞到方法入口处,直到获取锁的线程monitor.exit之后才能尝试继续获取锁。

二、流程图

五十六、ReentrantLock公平锁和非公平锁的区别

答案

一、概念

1.公平锁:顾名思义–公平,大家老老实实排队   (线程严格按照先进先出(FIFO)的顺序, 获取锁资源)。

2.非公平锁:只要有机会就尝试抢占资源   (拥有锁的线程在释放锁资源的时候, 当前尝试获取锁资源的线程可以和等待队列中的第一个线程竞争锁资源,但是已经进入等待队列的线程, 依然是按照先进先出的顺序获取锁资源)

3.非公平锁的弊端:可能导致后面排队等待的线程等不到相应的CPU资源,从而引起线程饥饿(线程因无法访问所需资源而无法执行下去的情况)。

二、示例图

五十七、什么情况下索引不会被命中?索引最左原则原理是什么?

答案

一、什么情况下索引不会被命中

1、如果条件中有 or ,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)

注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引

如果出现OR的一个条件没有索引时,建议使用 union ,拼接多个查询语句

2.、like查询是以%开头,索引不会命中

只有一种情况下,只查询索引列,才会用到索引,但是这种情况下跟是否使用%没有关系的,因为查询索引列的时候本身就用到了索引

3. 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引

4. 没有查询条件,或者查询条件没有建立索引

5. 查询条件中,在索引列上使用函数(+, - ,*,/), 这种情况下需建立函数索引

6. 采用 not in, not exist

7. B-tree 索引 is null 不会走, is not null 会走

二、索引最左原则原理是什么

索引本质是一棵B+Tree,联合索引(col1, col2,col3)也是。

其非叶子节点存储的是第一个关键字的索引,而叶节点存储的则是三个关键字col1、col2、col3三个关键字的数据,且按照col1、col2、col3的顺序进行排序。

联合索引(年龄, 姓氏,名字),叶节点上data域存储的是三个关键字的数据。且是按照年龄、姓氏、名字的顺序排列的。

而最左原则的原理就是,因为联合索引的B+Tree是按照第一个关键字进行索引排列的。

三、例子

联合索引(年龄, 姓氏,名字),叶节点上data域存储的是三个关键字的数据。且是按照年龄、姓氏、名字的顺序排列的。

因此,如果执行的是:

select * from STUDENT where 姓氏='李' and 名字='安';

或者

select * from STUDENT where 名字='安';

那么当执行查询的时候,是无法使用这个联合索引的。因为联合索引中是先根据年龄进行排序的。如果年龄没有先确定,直接对姓氏和名字进行查询的话,就相当于乱序查询一样,因此索引无法生效。因此查询是全表查询。

如果执行的是:

select * from STUDENT where 年龄=1 and 姓氏='李';

那么当执行查询的时候,索引是能生效的,从图中很直观的看出,age=1的是第一个叶子节点的前6条记录,在age=1的前提下,姓氏=’李’的是前3条。因此最终查询出来的是这三条,从而能获取到对应记录的地址。

如果执行的是:

select * from STUDENT where 年龄=1 and 姓氏='黄' and 名字='安';

那么索引也是生效的。

而如果执行的是:

select * from STUDENT where 年龄=1 and 名字='安';

那么,索引年龄部分能生效,名字部分不能生效。也就是说索引部分生效。

因此我对联合索引结构的理解就是B+Tree是按照第一个关键字进行索引,然后在叶子节点上按照第一个关键字、第二个关键字、第三个关键字…进行排序。

最左原则

而之所以会有最左原则,是因为联合索引的B+Tree是按照第一个关键字进行索引排列的。
 

五十八、Spring中bean的作用域

答案

  1. singleton:默认作用域,单例bean,每个容器中只有一个bean的实例。
  2. prototype:为每一个bean请求创建一个实例。
  3. request:为每一个request请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收。
  4. session:与request范围类似,同一个session会话共享一个实例,不同会话使用不同的实例。
  5. global-session:全局作用域,所有会话共享一个实例。如果想要声明让所有会话共享的存储变量的话,那么这全局变量需要存储在global-session中。

五十九、Spring框架中的Bean是线程安全的么?如果线程不安全,那么如何处理?

答案

Spring容器本身并没有提供Bean的线程安全策略,因此可以说Spring容器中的Bean本身不具备线程安全的特性,但是具体情况还是要结合Bean的作用域来讨论。

(1)对于prototype作用域的Bean,每次都创建一个新对象,也就是线程之间不存在Bean共享,因此不会有线程安全问题。

(2)对于singleton作用域的Bean,所有的线程都共享一个单例实例的Bean,因此是存在线程安全问题的。但是如果单例Bean是一个无状态Bean,也就是线程中的操作不会对Bean的成员执行查询以外的操作,那么这个单例Bean是线程安全的。比如Controller类、Service类和Dao等,这些Bean大多是无状态的,只关注于方法本身。

  • 有状态Bean(Stateful Bean) :就是有实例变量的对象,可以保存数据,是非线程安全的。
  • 无状态Bean(Stateless Bean):就是没有实例变量的对象,不能保存数据,是不变类,是线程安全的。

对于有状态的bean(比如Model和View),就需要自行保证线程安全,最浅显的解决办法就是将有状态的bean的作用域由“singleton”改为“prototype”。

也可以采用ThreadLocal解决线程安全问题,为每个线程提供一个独立的变量副本,不同线程只操作自己线程的副本变量。

六十、Spring Bean的生命周期?

答案

简单来说,Spring Bean的生命周期只有四个阶段:实例化 Instantiation --> 属性赋值 Populate  --> 初始化 Initialization  --> 销毁 Destruction

但具体来说,Spring Bean的生命周期包含下图的流程:

(1)实例化Bean:

对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。

对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。

(2)设置对象属性(依赖注入):实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成属性设置与依赖注入。

(3)处理Aware接口:Spring会检测该对象是否实现了xxxAware接口,通过Aware类型的接口,可以让我们拿到Spring容器的一些资源:

①如果这个Bean实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,传入Bean的名字;
②如果这个Bean实现了BeanClassLoaderAware接口,调用setBeanClassLoader()方法,传入ClassLoader对象的实例。
②如果这个Bean实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
③如果这个Bean实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;
(4)BeanPostProcessor前置处理:如果想对Bean进行一些自定义的前置处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。

(5)InitializingBean:如果Bean实现了InitializingBean接口,执行afeterPropertiesSet()方法。

(6)init-method:如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。

(7)BeanPostProcessor后置处理:如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;

以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。

(8)DisposableBean:当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;

(9)destroy-method:最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。

六十一、Spring容器的启动流

答案

(1)初始化Spring容器,注册内置的BeanPostProcessor的BeanDefinition到容器中:

  • ① 实例化BeanFactory【DefaultListableBeanFactory】工厂,用于生成Bean对象
  • ② 实例化BeanDefinitionReader注解配置读取器,用于对特定注解(如@Service、@Repository)的类进行读取转化成  BeanDefinition 对象,(BeanDefinition 是 Spring 中极其重要的一个概念,它存储了 bean 对象的所有特征信息,如是否单例,是否懒加载,factoryBeanName 等)
  • ③ 实例化ClassPathBeanDefinitionScanner路径扫描器,用于对指定的包目录进行扫描查找 bean 对象

(2)将配置类的BeanDefinition注册到容器中:

(3)调用refresh()方法刷新容器:

  • ① prepareRefresh()刷新前的预处理:
  • ② obtainFreshBeanFactory():获取在容器初始化时创建的BeanFactory:
  • ③ prepareBeanFactory(beanFactory):BeanFactory的预处理工作,向容器中添加一些组件:
  • ④ postProcessBeanFactory(beanFactory):子类重写该方法,可以实现在BeanFactory创建并预处理完成以后做进一步的设置
  • ⑤ invokeBeanFactoryPostProcessors(beanFactory):在BeanFactory标准初始化之后执行BeanFactoryPostProcessor的方法,即BeanFactory的后置处理器:
  • ⑥ registerBeanPostProcessors(beanFactory):向容器中注册Bean的后置处理器BeanPostProcessor,它的主要作用是干预Spring初始化bean的流程,从而完成代理、自动注入、循环依赖等功能
  • ⑦ initMessageSource():初始化MessageSource组件,主要用于做国际化功能,消息绑定与消息解析:
  • ⑧ initApplicationEventMulticaster():初始化事件派发器,在注册监听器时会用到:
  • ⑨ onRefresh():留给子容器、子类重写这个方法,在容器刷新的时候可以自定义逻辑
  • ⑩ registerListeners():注册监听器:将容器中所有的ApplicationListener注册到事件派发器中,并派发之前步骤产生的事件:
  • ⑪ finishBeanFactoryInitialization(beanFactory):初始化所有剩下的单实例bean,核心方法是preInstantiateSingletons(),会调用getBean()方法创建对象;
  • ⑫ finishRefresh():发布BeanFactory容器刷新完成事件:

8、BeanFactory和ApplicationContext有什么区别?

        BeanFactory和ApplicationContext是Spring的两大核心接口,都可以当做Spring的容器。

(1)BeanFactory是Spring里面最底层的接口,是IoC的核心,定义了IoC的基本功能,包含了各种Bean的定义、加载、实例化,依赖注入和生命周期管理。ApplicationContext接口作为BeanFactory的子类,除了提供BeanFactory所具有的功能外,还提供了更完整的框架功能:

继承MessageSource,因此支持国际化。
资源文件访问,如URL和文件(ResourceLoader)。
载入多个(有继承关系)上下文(即同时加载多个配置文件) ,使得每一个上下文都专注于一个特定的层次,比如应用的web层。
提供在监听器中注册bean的事件。
(2)①BeanFactroy采用的是延迟加载形式来注入Bean的,只有在使用到某个Bean时(调用getBean()),才对该Bean进行加载实例化。这样,我们就不能提前发现一些存在的Spring的配置问题。如果Bean的某一个属性没有注入,BeanFacotry加载后,直至第一次使用调用getBean方法才会抛出异常。

        ②ApplicationContext,它是在容器启动时,一次性创建了所有的Bean。这样,在容器启动时,我们就可以发现Spring中存在的配置错误,这样有利于检查所依赖属性是否注入。 

        ③ApplicationContext启动后预载入所有的单实例Bean,所以在运行的时候速度比较快,因为它们已经创建好了。相对于BeanFactory,ApplicationContext 唯一的不足是占用内存空间,当应用程序配置Bean较多时,程序启动较慢。

(3)BeanFactory和ApplicationContext都支持BeanPostProcessor、BeanFactoryPostProcessor的使用,但两者之间的区别是:BeanFactory需要手动注册,而ApplicationContext则是自动注册。

(4)BeanFactory通常以编程的方式被创建,ApplicationContext还能以声明的方式创建,如使用ContextLoader。
 

六十二、Redis分布式锁实现原理 

答案

https://blog.csdn.net/qq_17025903/article/details/96485351

六十三、hashmap的put操作

答案

一、原理图

二、put原理

1.判断键值对数组tab是否为空或为null,如果为空则执行resize()进行扩容;
2.根据键值key计算hash值得到索引i,如果tab[i]==null,则直接新建节点添加,进入第6步,如果tab[i]不为空,进入第3步;
3.判断tab[i]的首个元素的key是否和传入key一样并且hashCode相同,如果相同直接覆盖value,否则转进入第4步;
4.判断tab[i] 是否为treeNode(红黑树),如果是红黑树,则直接在树中插入新节点,否则进入第5步;
5.遍历tab[i]判断是否遍历至链表尾部,如果到了尾部,则在尾部链入一个新节点,然后判断链表长度是否大于8,如果大于8的话把链表转换为红黑树,否则进入6;遍历过程中若发现key已经存在,直接覆盖value,进入第6步;
6.插入成功后,判断size是否超过了阈值(当前容量*负载因子),如果超过,进行扩容

六十四、为什么hashmap用红黑树

答案

红黑树牺牲了一些查找性能 但其本身并不是完全平衡的二叉树。因此插入删除操作效率略高于AVL树
AVL树用于自平衡的计算牺牲了插入删除性能,但是因为最多只有一层的高度差,查询效率会高一些。

六十五、@Autowired和@Resource的区别

答案

  1. @AutoWried按by type自动注入,
  2. @Resource默认按byName自动注入。

Java大厂面试必考真题算法篇(持续更新)     https://blog.csdn.net/qq_17025903/article/details/114680318

常见面试题会持续更新。。。当然不光面试题有的时候还会手写算法题。。本人这方面是不太擅长推荐一本(Java程序员面试笔试宝典)里面的数据结构和算法有兴趣的同学可以刷一刷。。我反正是看吐了。。

这本书这块我也没怎么看,但是确实会考到。。。= = 下面是目录可以参考。。

猜你喜欢

转载自blog.csdn.net/qq_17025903/article/details/113927157
今日推荐