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对指令重排序,优化排序等。
volatile能保证有序性和可见性不能保证原子性
原理:在线程操作变量时,每次都会先从主存中获取改变量的值,操作完之后会马上把值刷新到主存中去,保证主存的值是最新的。也就成就了可见性。
不能替代锁。

十三、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修改结束后,,发现还有一条没有修改,就好像发生了幻读一样。

脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务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(终止状态)

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

答案

之前写过详细的实现

并发编程下的锁机制,乐观锁、悲观锁、共享锁、排他锁、分布式锁、锁降级原理篇_大道至简,悟在天成。-CSDN博客

二十六、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如何保证消息不丢失?如果mq宕机了如何处理?

答案

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

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

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

// 提交事务
channel.txCommit

2.MQ服务器端

消息持久化到硬盘

3.生产者

消息ack确认机制

4.消费者

必须确认消息消费成功
rabbitmq中才会将该消息删除。
rocketmq或者kafka中:才会提交offset

如果mq宕机了如何处理?

1.生产者投递消息会将msg消息内容记录下来,后期如果发生生产者投递消息失败;
2.可以根据该日志记录实现补偿机制;
3.补偿机制(获取到该msg日志消息内容实现重试)

mq宕机了可以先把数据记录Redis/Mysql,后期通过定时任务读取Redis/Mysql数据记录日志,再去投递mq服务端。

原理图:

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

答案

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分布式锁实现原理 

答案

Redis分布式锁实现及原理_大道至简,悟在天成。-CSDN博客_redis分布锁原理

六十三、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自动注入。

六十六、线程池中的一个线程异常了会被怎么处理

答案

  1. execute方法,可以看异常输出在控制台,而submit在控制台没有直接输出,必须调用Future.get()方法时,可以捕获到异常。
  2. 一个线程出现异常不会影响线程池里面其他线程的正常执行。
  3. 线程不是被回收而是线程池把这个线程移除掉,同时创建一个新的线程放到线程池中。

六十七、Redis六种淘汰策略

答案

  • noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。 大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL )。
  • allkeys-lru: 所有key通用; 优先删除最近最少使用(less recently used ,LRU) 的 key。
  • volatile-lru: 只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key。
  • allkeys-random: 所有key通用; 随机删除一部分 key。
  • volatile-random: 只限于设置了 expire 的部分; 随机删除一部分 key。
  • volatile-ttl: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key。

六十八、HashMap链表升级成红黑树的条件

答案

  1. 链表长度大于8
  2. 隐含条件是 Node 数组不为 null 且 Node 数组长度大于等于64 (不满足则会发生扩容代替升级)

六十九、为什么ArrayLIst查询快?LinkedList插入删除快?

答案

  1. ArrayLIst查询效率高:ArrayLIst是连续存放元素的,找到第一个元素的首地址,再加上每个元素的占据的字节大小就能定位到对应的元素。
  2. LinkedList插入删除效率高。因为执行插入删除操作时,只需要操作引用即可,元素不需要移动元素,他们分布在内存的不同地方,通过引用来互相关联起来。而ArrayLIst需要移动元素,故效率低。

七十、Redis分布式锁过期了,但是业务还没处理完怎么办?

答案

需要对锁进行续期,在项目中开启定时任务,每个一段时间比如10秒为当前分布式锁续期,续期那时就是每隔10秒重新设置当前key的过期时间,如果key存在说明业务还未处理完,就进行续期,否则说明业务处理完了,为了避免续期的key是其他客户端写入的,所以在value里面可以存一个服务器的ip来判断是否是当前客户端的,如果是则续期,否则不做处理。

七十一、为什么数组能支持随机访问呢?

答案

  1. 数组占用的内存空间是连续的
  2. 数组中都为同一类型的元素

例子

我们可以拿一个长度为5的int数组为例,当我们执行了一段int[] arr = new int[5]后,计算机会给这个数组分配如下图所示的一段内存空间:

首地址为1000

arr[0]1000-1003
arr[1]1003-1006
arr[2]1006-1009
arr[3]1009-1012
arr[4]1012-1015

当我们访问角标为2的位置上的元素时我们可以直接通过计算得出该元素对于的内存位置:

1000+2∗4

其中1000是我们的基地址,2代表了我们的偏移量,4代表了每个元素所占内存的大小(int占4个字节)这样通过一次计算,我们就能直接找到数组中对应角标的位置了。

七十二、什么是内存溢出内存泄露?如何解决ThreadLocal 内存泄露

答案

  • 内存溢出: Memory overflow 没有足够的内存提供申请者使用.
  • 内存泄漏: Memory Leak 程序中已经动态分配的堆内存由于某种原因, 程序未释放或者无法释放, 造成系统内部的浪费, 导致程序运行速度减缓甚至系统崩溃等严重结果. 内存泄漏的堆积终将导致内存溢出

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法

  • 每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
  • 在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

七十三、Spring通知类型有那五种?

答案

  • Around 环绕通知    org.aopalliance.intercept.MethodInterceptor    拦截对目标方法调用
  • Before 前置通知    org.springframework.aop.MethodBeforeAdvice     在目标方法调用前调用
  • After  后置通知    org.springframework.aop.AfterReturningAdvice    在目标方法调用后调用
  • Throws 异常通知    org.springframework.aop.ThrowsAdvice    当目标方法抛出异常时调用
  • 最终通知   无论切入点方法是否正常执行,它都会在其后面执行

七十四、Spring bean加载的有那三种方式?

答案

        //第一种方式:ClassPathXmlApplicationContext
        ApplicationContext context=new ClassPathXmlApplicationContext("bean.xml");
        UserService userService=(UserService) context.getBean("userService");
        userService.add();
        System.out.println(userService);
        System.out.println(".................................");
        //第二种方式:通过文件系统路径获得配置文件   【绝对路径】
        ApplicationContext context1=new FileSystemXmlApplicationContext("C:\\bean.xml");
        UserService userService1=(UserService) context.getBean("userService");
        userService1.add();
        System.out.println(userService1);
        System.out.println(".................................");
        //第三种方式:使用BeanFactory
        String path="C:\\bean.xml";
        BeanFactory factory=new XmlBeanFactory(new FileSystemResource(path));
        UserService userService2=(UserService) factory.getBean("userService");
        userService2.add();
        System.out.println(userService2);
  1. 第一种方式:ClassPathXmlApplicationContext   配置文件xml
  2. 第二种方式:通过文件系统路径获得配置文件   
  3. 第三种方式:使用BeanFactory

七十五、BeanFactory 和ApplicationContext 的区别?

答案

  1. BeanFactory 采取延迟加载,第一次getBean时才会初始化Bean
  2. ApplicationContext 即时加载

ApplicationContext是对BeanFactory扩展,提供了更多功能:

  • 国际化处理
  • 事件传递
  • Bean自动装配
  • 各种不同应用层的Context实现

七十六、如何保持mysql和redis中数据的一致性?

答案

先删缓存,再更新数据库该方案会导致不一致的原因是。同时有一个请求A进行更新操作,另一个请求B进行查询操作。那么会出现如下情形:

  1. 请求A进行写操作,删除缓存
  2. 请求B查询发现缓存不存在
  3. 请求B去数据库查询得到旧值
  4. 请求B将旧值写入缓存
  5. 请求A将新值写入数据库

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。那么,如何解决呢?采用延时双删策略。

/**
*解决方法的伪代码
*/
public void write(String key,Object data){
	//1、先删除缓存
	redis.delKey(key);
	//2、更新数据库,写入数据
	db.updateData(data);
	//3、休眠1秒
	Thread.sleep(1000);
	//4、再次删除缓存
	redis.delKey(key);
}

休眠时间需要评估自己的项目的读数据业务逻辑的耗时。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

七十七、千万级数据分页查询很慢如何优化?

答案

通过覆盖索引优化查询速度

select  * from test limit 10000000, 10  速度很慢

优化查询语句

select test.* from test  (select id from test limit 10000000, 10)  as  t1

where test.id=t1.id

通过子查询把这个id的范围查出来,这个id实际上是一个覆盖索引(只需扫描索引而无须回表),然后在做个关联查询,通过索引所以查询就很快了。

七十八、MYSQL为什么使用B+树不是B树?

答案

B类树的特点,B类树保证尽量多的在结点上存储相关的信息,同时保证层数尽量的少,查找更快,磁盘的IO操作也少一些。

  1. B+树的IO更少:B+树的非叶子节点没有指向关键字具体信息的指针,只用作索引,因此B+树的非叶子节点比B树占用更少磁盘空间。当数据量大时,一次不能把整个索引全部加载到内存,只能逐个加载每一个磁盘块,而关键字所占空间更小可以使得一次性读入内存的索引也就越多,IO次数也就越少。
  2. B+树更擅长范围查找:B+树的叶子节点是按顺序放置的双向链表,更适合MySQL的区间查询。B树因为其分支结点同样存储着数据,需要进行一次中序遍历按序。
  3. B+树更擅长遍历:B+树只需要去遍历叶子节点就可以实现整棵树的遍历,更适合MySQL的扫表操作。B树则需要进行深度优先遍历或层序遍历。(类比2)
  4. B+树的查询效率更加稳定:B+树叶子节点(数据)都在同一层,查找操作从根节点到叶子节点经历的路径长度相同,所以查询效率稳定。

七十九、什么是覆盖索引?

答案

1.我们要查询的字段刚好就是索引,直接从索引获取数据就可以了。

八十、MYSQL数据库写一个死锁sql,如何监测到死锁sql?

答案

//左边
begin
update orders set status=1 where id=1;
update orders set status=1 where id=2;
commit;
//右边
begin
update orders set status=1 where id=2;
update orders set status=1 where id=1;
commit;

第一次操作左边更改id是1的数据,右边更新id是2的数据,这个时候是不冲突的。
第二次左边更新id是2的数据,这个时候事务2就会冲突持有id是2的锁了,进行等待。
第三次右边更新id是1的数据就会造成死锁。

//项目运行过程中造成死锁监测方法
show engine innodb status 通过日志排查,检查innodb状态,通过分析日志找到死锁。

八十一、高并发场景分布式锁的优化方案?

答案

比如1000个商品的库存扣减,可以拆分成20个库存字段数据库缓存都可以,然后每个库存50个商品(20库存*50商品=1000总商品数量),做一个随机算法工具,通过分段加锁的方式并发最多20个库存访问扣减商品,如果当前库存为0则切换另一个库存扣减。

八十二、什么时候需要考虑分库分表?

答案

在进行分库分表之前我们可以对硬件进行升级,对网络进行升级,对数据库进行读写分离,对数据表的关系进行合理设计,对索引进行优化,如果我们这些都做了,数据量日增长上百万数据这个的话会导致单表数据量过大,会更运维造成影像,比如做数据备份会花费很长的时间,对数据的一个修改可能会造成锁的等待,那这个时候我们就要考虑分库分表了,业界指标则为单表数据超过500w或者单表数据达到2GB的时候就需要考虑分库分表。

八十三、什么是MYSQL的回表?

答案

回表查询 ,先定位主键值,再定位行记录,它的性能较扫一遍索引树更低,需要扫描两遍索引树。

举个栗子,

InnoDB有两大类索引:

  • 聚集索引(clustered index)
  • 普通索引(secondary index)

不妨设有表:

table(id PK, name KEY, age);

画外音:id是聚集索引,name是普通索引。

InnoDB 聚集索引 的叶子节点存储行记录,因此, InnoDB必须要有,且只有一个聚集索引:

(1)如果表定义了PK,则PK就是聚集索引;

(2)如果表没有定义PK,则第一个not NULL unique列是聚集索引;

(3)否则,InnoDB会创建一个隐藏的row-id作为聚集索引;

画外音:所以PK查询非常快,直接定位行记录。

八十四、select.....for update 锁表还是锁行?

答案

如果在select查询条件中使用了索引那么就会锁行,如果没有使用索引就会锁表。

八十五、线程为什么会出现不可见原因?

答案

在Java中每一个的线程都有一块独立的内存空间,俗称线程工作区,当它们去执行临界区的资源(共享资源)时,都会先去主内存将共享资源复制一份到自己的工作内存中再进行操作,最后再把操作完的副本写入到主内存中,看似表面上是直接操作了共享资源,其实实际上是操作了共享资源的副本。

八十六、怎么解决线程之间的不可见?

答案

通过锁机制,每次只让一个线程处理资源,另一个是volatile关键字,它能够保证线程之间的可见性和能够防止指令重排。

八十七、volatile为什么不能保证原子性?

答案

简单的说,修改volatile变量分为四步:
1)读取volatile变量到local
2)修改变量值
3)local值写回
4)插入内存屏障,即lock指令,让其他线程可见这样就很容易看出来,
前三步都是不安全的,取值和写回之间,不能保证没有其他线程修改。
原子性需要锁来保证。这也就是为什么,volatile只用来保证变量可见性,但不保证原子性。

八十八、volatile底层原理实现是什么?

答案

volatile的底层是通过:store,load等内存屏障命令,解决JMM的可见性和重排序问题的。
写操作时,使用store指令会强制线程刷新数据到主内存,读操作使用load指令会强制从主内存读取变量值。
但是它无法解决竞争问题,要解决竞争问题需要加锁,或使用cas等无锁技术。  

八十九、为什么需要使用线程池?

答案

因为频繁的开启线程或者停止线程,线程需要从新被cpu_从就绪到运行状态调度,需要发生cpu的上下文切换,效率非常低。
线程池是复用机制
提前创建好一些固定的线程数一直在运行状态实现复用﹐从而可以减少就绪到运行状态的切换。

九十、你们哪些地方会使用到线程池?

答案
实际开发项目中 禁止自己new 线程,必须使用线程池来维护和创建线程。
如果项目比较小可以通过线程池异步去发短信,发邮件,开通权益等,如果项目比较大则为整个
项目做异步就应该使用一些mq。

九十一、线程池有哪些好处?

答案

核心点:复用机制提前创建好固定的线程一直在运行状态实现复用限制线程创建数量。

  1. 降低资源消耗:通过池化技术重复利用已创建的线程,降低线程创建和销毁造成的损耗。
  2. 提高响应速度:任务到达时,无需等待线程创建即可立即执行。
  3. 提高线程的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会因为线程的不合理分布导致资源调度失衡,降低系统的稳定性。使用线程池可以进行统一的分配、调优和监控。
  4. 提供更多更强大的功能:线程池具备可拓展性,允许开发人员向其中增加更多的功能。比如延时定时线程池ScheduledThreadPoolExecutor,就允许任务延期执行或定期执行。

九十二、线程池底层复用机制的原理?

答案

 线程池核心点:复用机制------

1.提前创建好固定的线程一直在运行状态----死循环实现

2.提交的线程任务缓存到一个并发队列集合中,交给我们正在运行的线程执行

3.正在运行的线程就从队列中获取该任务执行

九十三、线程池创建的线程会一直存活吗?

答案

不会

例如:配置核心线程数corePoolSize为2、最大线程数maximumPoolSize为5我们可以通过配置超出 corePoolSize核心线程数后创建的线程的存活时间例如为60s,在60s内没有核心线程一直没有任务执行,则会停止该线程。

eg:
corePoolSize---核心线程正在运行线程2
maximumPoolSize----最大线程数4
keepAliveTime----超出核心线程数后创建的线程存活时间 60m
在额外创建2个线程==最大线程数4-核心线程正在运行线程2=2

java核心线程池的回收由allowCoreThreadTimeOut参数控制,默认为false,若开启为true,则此时线程池中不论核心线程还是非核心线程,只要其空闲时间达到keepAliveTime都会被回收。
但如果这样就违背了线程池的初衷(减少线程创建和开销),所以默认该参数为false

九十四、MQ如何避免消息堆积问题

答案

需要注意的是:
rabbitmq消费者我们的消息消费成功的话,消息会被立即删除。
kafka或者rocketmq消息消费如果成功的话,消息是不会立即被删除的。
消息堆积产生过程:生产者投递消息的速率与我们消费者消费的速率完全不匹配。

解决办法
1.提供消费者消费的速率
2.消费者应该批量形式获取消息 减少网络传输的次数

线程池优化eg:

	    
	    public void startSynUserChatRoomMsg() {
            //构建consumer对象
	        ConsumerConnector consumerConnector = kafkaLiveRoomConfig.getConsumerConnector(groupName,zookeeperAddr);
            //构建一个map对象,代表topic-------String:topic名称,Integer:从这个topic中获取多少条记录
	        Map<String, Integer> topicCountmap = new HashMap<>();
	        topicCountmap.put(topic, partition);
            //构造一个messageStreams:输入流      --String:topic名称,List获取的数据
	        Map<String, List<KafkaStream<byte[], byte[]>>> messageStreamsMap = consumerConnector.createMessageStreams(topicCountmap);
	        List<KafkaStream<byte[], byte[]>> kafkaSteamList = messageStreamsMap.get(topic);
	        ExecutorService executor = Executors.newFixedThreadPool(partition);
	        for (final KafkaStream<byte[], byte[]> kafkaStream : kafkaSteamList) {
	            executor.submit(new Runnable() {
	                @Override
	                public void run() {
	                    for (MessageAndMetadata<byte[], byte[]> messageAndMetadata : kafkaStream) {
	                        String message = new String(messageAndMetadata.message());
	                        System.out.println(message);
	                        log.info("***********startSynUserChatRoomMsg is : {}", message);
	                        //插入
	                        liveChatroomUserActionService.insertUserChatRoomMsg(message);
	                    }
	                }
	            });
	        }
	    }

九十五、MQ如何保证消息顺序一致性

答案

产生背景:
mq服务器集群或者mq采用分区模型架构存放消息,每个分区对于一个消费者消费消息。
解决消息顺序一致性问题核心办法:
消息一定要投递到同一个mq、同一个分区模型、最终被同一个消费者消费。
根据消息 key计算%分区模型总数=得到同一个mq被同一个mq消费。

  1. 大多数的项目是不需要保证mq消息顺序一致性的问题,只有在一些特定的场景可能会需要,比如MySQL与Redis实现异步同步数据;
  2. 所有消息需要投递到同一个mq服务器,同一个分区模型中存放,最终被同一个消费者消费,核心原理:设定相同的消息 key,根据相同的消息 key计算 hash 存放在同一个分区中。
  3. 如果保证了消息顺序一致性有可能降低我们消费者消费的速率。

九十六、MQ如何保证消息的幂等性

答案

  1. 消费者获取消息,如果消费消息失败,mq服务器则会间隔的形式实现重试策略;
  2. 重试过程中,需要保证业务幂等性问题,保证业务不能够重复执行。
  3. 我们可以通过全局的消息 id,提前查询如果该业务逻辑已经执行过,则不会重复执行。
  4. 我们也需要在数据库的db层面需要保证幂等性问题,唯一主键约束、乐观锁等。

九十七、MQ与Redis如何保证数据一致性问题

答案

  1. 直接删除 Redis 缓存;----延迟双删策略方案
  2. 基于MQ异步同步更新
  3. 基于canal订阅binlog同步

九十八、Spring如何解决循环依赖问题?

答案

Spring是通过三级缓存解决的,就是三个Map,一级缓存存储完整的Bean并加入三级缓存,当出现ABA互相持有对方,最终形成闭环的情况,在getBean(A)才会调用三级缓存(如果实现了aop则创建动态代理,如果没有创建依然返回Bean的实例),并放入二级缓存,二级缓存的作用则是避免多重循环依赖的情况,比如AB,AC,BA,CA这种情况A被依赖了俩次就有可能会创建俩次动态代理,所以避免这种情况则需要用一个二级缓存来存储。

总结如下

  • 一级缓存:存储完整的Bean
  • 二级缓存:避免多重循环依赖重复创建
  • 三级缓存:把Bean的名字和实例传进去(aop创建),不会立刻调用,会在ABA才回去调用三级缓存,放入二级缓存

九十九、如何终止正在运行的线程?

答案

如果想要停止一个正在运行的线程,就要提供某种方式来让线程自动结束,比如设置一个while(flag=true)循环的flag标志,当while循环flag改变为false则停止运行,来让线程离开run()方法,从而终止线程。这样的方法虽然能够终止线程,但是也存在着问题,当线程处在非运行状态,sleep()或wait()方法被调用或当被IO阻塞时,上面的方法就不可用了,此时可以使用interrupt()方法,设置中断状态为true来打破阻塞,抛出InterruptedException异常,通过catch捕获异常来让线程安全退出,如果线程正在执行中没有可以中断的方法,则可以通过isInterrupted()方法判断中断状态来决定是否继续执行,如果程序因为IO而停滞,进入非运行状态,无法使用interrupt()来让程序离开run()方法,基本思路也是触发一个异常,比如close方法来关闭流,则会引发IOException异常,通过捕获异常来安全结束线程。

一百、什么是垃圾,GC如何定位垃圾,垃圾回收算法是什么?

答案

一、GC什么是垃圾?

没有任何引用指向一个对象或者多个对象(循环引用)

二、如何定位垃圾?

  1. 引用计数(该算法给每个对象分配一个计数器,当有引用指向这个对象时, 计数器加1,当指向该对象的引用失效时,计数器减一。 最后如果该对象的计数器为0时,java垃圾回收器会认为该对象是可回收的。
  2. 根可达算法(除了GC root包含的线程栈变量、静态变量、常量池、JNI指针其他根找不到的都是垃圾

三、垃圾回收算法是什么?

  1. 标记清除(位置不连续产生碎片)
  2. 复制算法(没有碎片浪费空间)
  3. 标记压缩(没有碎片效率偏低)

一百一、GC常见的垃圾收集器区别?

答案

  1. CMS收集器是一种以获取最短回收停顿时间为目标的收集器
  2. G1收集器可以设置垃圾回收的时间,还可以建立可预测停顿模型
  3. 串行收集器指使用单线程进行垃圾回收,垃圾回收时,只有一个线程在工作
  4. 并行收集器将单线程改为了多线程进行垃圾回收,这样可以缩短垃圾回收的时间

一百二、System.gc()的作用是什么?如何立即执行?

答案

调用Systemgc()方法时,执行的是Runtime.getRuntime.gc()方法。调用这两种方法,只是强制启动垃圾回收器,但是系统是否立即进行,是不确定的。

  • System.gc() 告诉垃圾收集器打算进行垃圾收集,而垃圾收集器进不进行收集是不确定的 

  • System.runFinalization() 强制调用已经失去引用的对象的finalize方法 

  • finalize() 当垃圾收集器认为没有指向对象实例的引用时,会在销毁该对象之前调用该方法,是Object的protected方法,子类可以覆盖该方法以实现资源清理工作,GC在回收对象之前调用该方法

若要是所有对象执行finalize()方法,需先执行System.gc() ,在执行System.runFinalization()

eg:重写finalize方法

 调用System.runFinalization()执行后

一百三、运行时异常和非运行时异常 (编译异常)区别?

答案

  • 运行时异常:都是RuntimeException类及其子类异常,如NullPointerException(空指针异常)、IndexOutOfBoundsException(下标越界异常)等,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。运行时异常的特点是Java编译器不会检查它,也就是说,当程序中可能出现这类异常,即使没有用try-catch语句捕获它,也没有用throws子句声明抛出它,也会编译通过。
  • 非运行时异常 (编译异常):是RuntimeException以外的异常,类型上都属于Exception类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如IOException、SQLException等以及用户自定义的Exception异常,一般情况下不自定义检查异常。

一百四、JVM新生代、老年代和永久代作用?

答案

  • 新生代:主要用来存放新生的对象。一般占据堆空间的1/3。在新生代中,保存着大量的刚刚创建的对象,但是大部分的对象都是朝生夕死,所以在新生代中会频繁的进行MinorGC,进行垃圾回收
  • 老年代:用于存放新生代中经过多次垃圾回收仍然存活的对象。

  • 永久代:指内存的永久保存区域,主要存放Class和Meta(元数据)的信息,Class在被加载的时候被放入永久区域。它和和存放实例的区域不同,GC不会在主程序运行期对永久区域进行清理。所以这也导致了永久代的区域会随着加载的Class的增多而胀满,最终抛出OOM异常。永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。

一百五、类加载过程,类加载器有哪些?

答案

类加载的过程分为:加载、验证、准备、解析和初始化五个阶段。

类加载器有四种分别是:

  1. 启动类加载器:用于加载 java 核心类库(在 JRE 的 lib 目录下),无法被 java 程序直接引用;
  2. 扩展类加载器:同于加载 java 的扩展库(在 JRE 的 lib 目录下的 ext 扩展目录中的 JAR 类包)。java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载java类;
  3. 系统类加载器(应用类加载器):它根据 java 应用的类路径(classpath)来加载 java 类,主要就是加载自己写的那些类。可以通过 ClassLoader.getSystemClassLoader() 来获取它;
  4. 用户自定义类加载器:负责加载用户自定义路径下的类包。通过继承 java.lang.ClassLoader 类的方式来实现。

一百六、Nacos配置中心动态刷新原理?

答案

配置的动态刷新的俩种模式

  1. Pull模式:主动去拉取客户端,会定时主动的去服务端查看配置是否有改变。
  2. Push模式:服务端主动去推送,服务端在检测到配置发生变化后,主动通知给客户端。

第一种问题:客户端要频繁的对服务端进行请求,请求间隔不太好设置。

第二种问题:服务端需要维持与客户端之间的心跳连接,需要消耗大量资源来维持这种心跳。

nacos的刷新原理:结合了上面两种模式,客户端每十毫秒向服务端发送一次请求,求头上携带长轮询的超时时间默认是三十秒,当服务端接收到客户端的请求时会挂起一段时间。而在这期间,如果配置发生变化,就会立即响给客户端。如果没有变化,客户端再重新发送请求就可以了。那这样一来,不需要客户端频繁的去发送这种请求,而服务端也不需要去维持心跳,由服务端来控制响应客户端的请求响应时间,从而减少客户端无效请求。

一百七、Kafak的零拷贝原理?

答案

所谓的零拷贝是指将数据直接从磁盘文件复制到网卡设备中,而不需要经由应用程序之手,

大大提高了应用程序的性能,减少了内核和用户模式之间的上下文切换。

传统IO

传统的文件读写或者网络传输,通常需要将数据从内核态转换为用户态。应用程序读取用户态内存数据,写入文件 / Socket之前,需要从用户态转换为内核态之后才可以写入文件或者网卡当中。我们可以称之为read/write模式,此模式的步骤为:

  1. 首先,调用read时,磁盘文件拷贝到了内核态;
  2. 之后,CPU控制将内核态数据copy到用户态下;
  3. 调用write时,先将用户态下的内容copy到内核态下的socket的buffer中;
  4. 最后将内核态下的socket buffer的数据copy到网卡设备中传送;

零拷贝

Kafka只是把文件存放到磁盘之后通过网络发出去,中间并不需要修改什么数据,那read和write的两次CPU copy的操作完全是多余的。

一百八、SpringBoot自动装配原理?

答案

  1. Springboot的自动配置注解是@EanbleAutoConfiguration
  2. 在springboot启动时,在启动类上使用的是@SpringbootApplication注解,他是一个复合注解,包含了@EanbleAutoConfiguration
  3. 自动配置注解的关键是@Import注解,他导入了一个类AutoConfigurationImportSelector
  4. 类中使用了一个方法SelectorImports方法
  5. 这个方***扫描导入所有jar下的spring.factory文件,将配置文件中自动配置类key=value,将列表中的类进行对象创建并放到spring容器中

一百九、MYSQL数据库如何实现可重复读?

答案

可重复读实现是一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,mysql默认的事务的隔离级别是3,即可以实现可重复读,mysql主要使用MVCC(多版本并发控制)。InnoDB为每行记录添加了一个版本号(系统版本号),每当修改数据时,版本号加一。在读取事务开始时,系统会给事务一个当前版本号,事务会读取版本号<=当前版本号的数据,这时就算另一个事务插入一个数据,并立马提交,新插入这条数据的版本号会比读取事务的版本号高,因此读取事务读的数据还是不会变。

二百、Redis哨兵模式和集群模式的区别?

答案

Redis 哨兵模式是一种高可用的解决方案,它可以监控主节点的状态,如果主节点出现故障,哨兵会自动将从节点升级为主节点,以确保服务的可用性。而Redis集群模式是一种分布式解决方案,它可以将数据分布到多个节点上,以提高服务的可用性和性能

二百一、雪花算法原理是什么?

答案

雪花算法实现的原理是基于Twitter的开源项目Snowflake来实现的,它主要使用了一个64位的整数来生成唯一的ID,其中包含41位的时间戳,10位的机器码,12位的序列号。它的优点是性能高,分布式系统内不会产生ID冲突,并且可以根据时间戳进行排序。

Java大厂面试必考真题算法篇(持续更新)     Java大厂面试必考真题算法篇(持续更新)_大道至简,悟在天成。-CSDN博客

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

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

  • 16
    点赞
  • 107
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
作为Java技术专家面试的一部分,常见的面试题主要集中在算法和数据结构方面。这些问题旨在评估面试者对Java编程的理解和应用能力。 一种常见的面试问题是要求面试者编写一个算法来解决某个具体的问题。这个问题可能涉及到数组操作、字符串处理、链表操作、树结构等等。面试者需要根据题目要求,使用Java语言编写出一个高效且正确的算法解决方案。这类问题可以通过独立思考、分析问题、设计算法和编写代码来回答。 另一个常见的问题是关于Java中的数据结构和算法的理解。面试官可能会问面试者关于数组、链表、栈、队列、堆、树等常见数据结构的特点和应用场景。面试者需要清楚地解释这些数据结构的定义、操作和复杂度,并能够根据具体的问题选择合适的数据结构来解决问题。 此外,面试官还可能问到Java中的一些核心概念和特性,例如多线程编程、异常处理、IO操作、集合框架等。面试者需要展示自己对这些概念和特性的理解,并能够灵活运用它们解决实际问题。 对于准备参加Java技术专家面试的同学,建议多刷一些常见的Java算法题和数据结构题,加深对Java编程的理解和应用能力。同时,也可以阅读一些相关的面试指南和面试经验,提前准备好对常见问题的回答。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [Java开发专家阿里P6-P7面试题大全答案汇总(持续更新)](https://blog.csdn.net/qq_17025903/article/details/113927157)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [Java面试题(全)](https://blog.csdn.net/m0_46991147/article/details/125897857)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

南归北隐

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

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

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

打赏作者

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

抵扣说明:

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

余额充值