引言
昨日接了一个阿里外包的电话面试,问了一些技术问题感觉到自己是真的菜,接触Java开发已经也有一段时间,技术方面说来惭愧,一直以来只是局限于框架工具的用法,也没有进行了解其实现的原理,更重要的是一直没有归纳和总结,这次把这些问题记录下来,相关的知识点也找了一些资料学习下。
问题
1. CountDownLanch的工作原理
实现原理:计数器的值由构造函数传入,并用它初始化AQS的state值。当线程调用await方法时会检查state的值是否为0,如果是就直接返回(即不会阻塞);如果不是,将表示该节点的线程入列,然后将自身阻塞。当其它线程调用countDown方法会将计数器减1,然后判断计数器的值是否为0,当它为0时,会唤醒队列中的第一个节点,由于CountDownLatch使用了AQS的共享模式,所以第一个节点被唤醒后又会唤醒第二个节点,以此类推,使得所有因await方法阻塞的线程都能被唤醒而继续执行。
引用别人的博客里的一段话,详细请点击:Java并发包中CountDownLatch的工作原理、使用示例
题外话:
什么是 AQS(抽象的队列同步器)
AbstractQueuedSynchronizer类如其名,抽象的队列式的同步器,AQS定义了一套多线程访问 共享资源的同步器框架,许多同步类实现都依赖于它,如常用的 ReentrantLock/Semaphore/CountDownLatch。
它维护了一个 volatile int state(代表共享资源)和一个 FIFO 线程等待队列(多线程争用资源被 阻塞时会进入此队列)。这里 volatile 是核心关键词,具体 volatile 的语义,在此不述。state 的 访问方式有三种: getState() setState() compareAndSetState()
AQS只是一个框架,具体资源的获取/释放方式交由自定义同步器去实现,AQS这里只定义了一个 接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS)之所以没有定义成 abstract,是因为独占模式下只用实现 tryAcquire-tryRelease,而共享模式下只用实现 tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模 式下的接口。不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实 现共享资源 state 的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/ 唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
1. isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。2. tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
3. tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
4. tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余 可用资源;正数表示成功,且有剩余资源。
5. tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回 true,否则返回false。
AQS定义两种资源共享方式
1.Exclusive独占资源 -ReentrantLock Exclusive(独占,只有一个线程能执行,如ReentrantLock)
2.Share共享资源 -Semaphore/CountDownLatch Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。
3.实现独占和共享两种 方式,一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现 tryAcquiretryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也支持自定义同步器 同时实现独占和共享两种方式,如ReentrantReadWriteLock
同步器的实现是ABS核心,以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。此后,其他线程再 tryAcquire()时就会失 败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放 锁之前,A 线程自己是可以重复获取此锁的(state 会累加),这就是可重入的概念。但要注意, 获取多少次就要释放多么次,这样才能保证state是能回到零态的。
以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与 线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程 就会从await()函数返回,继续后余动作。
2. 说一下自旋锁的原理
如果持有锁的线程能在很短的时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换,它们只需要等一等(自旋),等待锁释放之后即可立即获取锁,这样避免了用户线程和内核切换的消耗
线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程 也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁 的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。自旋锁的优缺点
自旋锁尽可能的减少线程的阻塞,这对于锁的竞争不激烈,且占用锁时间非常短的代码块来 说性能能大幅度的提升,因为自旋的消耗会小于线程阻塞挂起再唤醒的操作的消耗,这些操作会 导致线程发生两次上下文切换!
但是如果锁的竞争激烈,或者持有锁的线程需要长时间占用锁执行同步块,这时候就不适合 使用自旋锁了,因为自旋锁在获取锁前一直都是占用 cpu 做无用功,占着 XX 不 XX,同时有大量 线程在竞争一个锁,会导致获取锁的时间很长,线程自旋的消耗大于线程阻塞挂起操作的消耗, 其它需要cup的线程又不能获取到cpu,造成cpu的浪费。所以这种情况下我们要关闭自旋锁;
3. synchronized中类锁和对象锁的区别
这么基础的一个问题我竟然遗忘了。。因为之前记的是锁static方法和非static方法的区别,其实锁static方法就相当于锁类class,因为static方法是所有的类共享的,类锁是锁当前类的所有实例,对象锁是锁当前实例对象,详细的引入别人的博客:Java锁Synchronized对象锁和类锁的区别
4. volatile关键字的作用
问:说下项目中用到的设计模式
答:单例模式、责任链模式、工程模式、模板模式等等
问:那你说下你们平常单例模式怎么实现的
答:我们用的是双向检查
问:你们双向检查有用到volatile吧,那你说下这个关键字有什么作用
答:它可以用来保证线程每次获取的对象都是最新状态
问:除此之外还有什么作用
答: 卒
真的是😓,充分说明了平时的学习和运用是多么的浅显,看一下volatile,它主要有两个作用,1.保证所有线程否能看到共享内存中的最新状态,2.可以禁止多线程时创建对象的指令重排,
详细的看:传统单例模式双重检查锁存在的问题 ,volatile关键字的作用、原理 ,volatile关键字的作用 总结的很详细,真是多谢大佬
5. finalize()方法的作用
具体的原问题我记不清了,好像没这么简单。finalize()是Object提供的一个方法,当gc要回收一个对象的时候,会主动调用这个对象的finalize()方法
6. redis分布式锁的实现原理
项目中我用到的是redisson实现的分布式锁,它的原理主要是在redis中存储了一个类似于Map<lockName,Map<threadName,number>>的HASH结构, redis的key为lockName即所用锁的名称,hash中的key为uuid+线程id,value为当前线程获取锁的次数,主要用于支持可重录锁,判断流程如下,首先判断key是否存在,不存在则直接新建hash并设置过期时间,key存在则判断hash key是否为当前线程,为当前线程则value+1,否则为获取锁失败查询剩余当前key ttl,释放锁时会判断Hash key 中的value是否为0,大于0则减一,为0 则删除当前key
详细见:拜托,面试请不要在问我redis分布式锁的实现原理 Redisson实现分布式锁(1)--原理
7.redis的主从数据同步是怎么做到的
本来是问我项目中redis是怎么部署的,生产上我们使用的是阿里云提供的redis服务,阿里云给我们提供一个ip,对我们来说就相当于一个单机服务,我猜测阿里云其实也是使用了主从架构,然后又问对主从架构有什么了解,之间的主从数据同步是怎么做的,卒。
Redis的复制功能可以分为同步和命令传播两个操作:
同步操作用于将从服务器的数据库状态更新至主服务器当前所处的状态。
命令传播操作则用于在主服务器的数据库状态被修改,导致主从服务器的数据库状态不一致时,让主从服务器的数据库重新回到一致状态。
同步操作的图解:
简单的来说,主从之间是通过rdb文件来进行数据同步的,同时呢在复制期间,主服务器会将此时接收到的写命令记录到缓存区中,待从服务器载入rdb文件后,向其发送缓存区中的命令,最终完成同步操作
详细的请看博客:Redis之主从复制的原理 深入详解redis主从复制的原理
8. redis的持久化方式,他们之间有什么不同
当时学的时候就没注意这一点,苦涩。
redis的持久化方式有俩种,持久化策略有4种:
- RDB(数据快照模式),原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化,保存的是数据本身,存储文件是紧凑的
- AOF((append only file 追加模式),原理是每次修改数据时将Reids的操作日志以追加的方式写入文件,同步到硬盘(写操作日志),保存的是数据的变更记录
- 如果只希望数据保存在内存中的话,俩种策略都可以关闭
- 也可以同时开启俩种策略,当Redis重启时,AOF文件会用于重建原始数据
默认情况下,是快照RDB的持久化方式,将内存中的数据以快照的方式写入二进制文件中,默认的文件名是dump.rdb,
rdb的方式是效率高,文件紧凑,适合备份,方便恢复不同版本,适合容灾恢复,缺点是如果redis不是正常关闭,那么到上个恢复节点的数据将丢失
aof方式的优点是基本可以全量的保证数据,可以轻易的修复数据,明文操作记录的更加详细,适合追踪,缺点是aof文件通常比较大,而且效率相对较低
9. redis为什么设计成单线程的
当时原问题好像是redis是怎么控制并发的,我答曰redis是单线程的,然后继续问那redis为什么设计成单线程的,多线程的话不是效率更高吗? 其实之前看过redis为什么设计成单线程,但是忘记了。卒
主要的原因恰恰是性能原因。。
(1)纯内存操作,效率十分的高;
(2)多线程仍然会有上下文切换的损耗,虽然比进程切换损耗小;此外多线程操作会涉及到锁,单线程不存在加锁动作
(3)采用了非阻塞I/O多路复用机制 ,,这个非阻塞IO多路复用可以单独掏出博文研究了,这里先不研究
采用单线程的缺点
(1) 耗时命令会导致并发的下降,读写均有影响
(2)无法发挥多核cpu的性能,不过可以通过单机开多个redis实例来解决,,不过我感觉一般没人这么干
redis不存在线程安全问题?
Redis采用了线程封闭的方式,把任务封闭在一个线程,自然避免了线程安全问题,不过对于需要依赖多个redis操作(即:多个Redis操作命令)的复合操作来说,依然需要锁,而且有可能是分布式锁。
问了好多关于redis的问题,但是redis相关的问题并不局限于此,还有redis的过期策略等好多知识,贴一个博文链接:分布式之redis复习精讲
10 .主键索引和普通索引的区别(innodb中)
非主键索引的叶子节点存放的是主键的值,而主键索引的叶子节点存放的是整行数据,其中非主键索引也被称为二级索引,而主键索引也被称为聚簇索引 (聚簇索引既存储了索引,也储存了值)
1、如果查询语句是 select * from table where ID = 100,即主键查询的方式,则只需要搜索 ID 这棵 B+树。
2、如果查询语句是 select * from table where k = 1,即非主键的查询方式,则先搜索k索引树,得到ID=100,再到ID索引树搜索一次,这个过程也被称为回表。
之前sql优化中接触到覆盖查询,主要目的是为了防止回表,没想到这里也有减少回表的原因
答案来自于:面试小知识:MySQL索引相关 另外推荐阅读:一条sql语句执行的很慢的原因
11. b+树和b树的区别,为什么使用B+当作索引
关于底层结构一直没研究过,卒
1. b+树的中间节点并不保存数据,所有的数据都保存在叶子节点(这里又有聚簇索引和非聚簇索引,这里相对于聚簇索引来说),因此在相同的数据集的情况下,B+相对于B树更加的矮胖,能够相对的减少io次数
2. b+树的叶子节点使用了链表结构,对于范围查询更加的简便
3. b+树所有的查询都要查找到叶子节点,相当于B树更加稳定(B树不一定需要查找到叶子节点,因为它的每一个节点都包含数据,当是根节点查询时,B树更快,但也因此看出B树的查询不稳定)
答案来源于别人的一个漫画,用我自己的话总结了一下,贴上转载的地址:B+与B树的区别
12. 事务的隔离级别,mysql的默认隔离级别,隔离级别是怎么实现的
老生常谈的问题,,这里中文名我记错了,还是记英文名好了。read uncommit,read commited , Repeatable read,serializeable
大多数数据库默认的事务级别隔离级别是Read committed、比如Sql Server,Oracle。Mysql的默认级别是Repeatable read
悲剧 ,当时问我mysql的默认隔离级别,我想不起来了,说了个read commited 悲剧
隔离级别怎么实现的,,这个后面在研究吧,应该主要是加锁。。
13. sql优化你是怎么看有没有走索引的,简单介绍下Explain的各个参数的意思,以及列举下Extra中你常见的场景
一般可以使用explain来查看sql语句的执行计划,主要字段如下图
比较常用的字段有type、key、ref、rows、extra,其中extra中会包含许多额外信息,比如Using filesort 、Using temporary,这两个是比较消耗性能的,是必须要优化的地方,Using index 则说明使用了覆盖索引,效率是比较高的
具体每个字段以及含义详细见sql中explain详解
14. rocketmq 使用的是拉还是推的模式,拉的话是单条拉取还是批量拉取
😓,一直都在使用rocketmq,但是代码中用的啥却是忘了,对于mq的理解也只是在理论上
rocketmq两种模式都支持,我们使用的是推(PUSH)模式,但实际上它基于长链接实现的模式,也能算是一种特俗的“拉模式”,推模式的好处是消息的实时性比较高,缺点也比较明显,服务端需要记录每次发送消息的状态,增大了服务端压力,另外服务端并不知道消费端的消费能力,可能会造成消费者消费能力不足的情况下仍在不停的发送消息,拉模式可以避免这种情况,但拉模式的缺点也比较明显,主动拉取的时间频率不好控制,实时性较差,另外如果有很多的消费者采用拉模式会对服务端造成访问压力。
rocketmq采用的长链接的方式,兼具了Push和Pull的优点,不过需要Server和Client的配合才能够实现。即Client发送消息请求,Server端接受请求,如果发现Server队列里没有新消息,Server端不立即返回,而是持有这个请求一段时间(通过设置超时时间来实现),在这段时间内轮询Server队列内是否有新的消息,如果有新消息,就利用现有的连接返回消息给消费者;如果这段时间内没有新消息进入队列,则返回空。
这样消费消息的主动权既保留在Client端,也不会出现Server积压大量消息后,短时间内推送给Client大量消息使client因为性能问题出现消费不及时的情况。
长轮询的弊端:在持有消费者请求的这段时间,占用了系统资源,因此长轮询适合客户端连接数可控的业务场景中
关于mq是会有很多的知识点,也有很多的细节,比如顺序消费,负载均衡,部署,原理好多的知识
结语
另外还有部分的面试题,这里就不一一回顾了,通过这次面试感觉到了自己很多东西都是一知半解,仅仅是简单的会用而已,而且有些知道的东西也表达不出来,简而言之肚子里面没货,另外面试过程中对于别人提出的问题,最好想清楚再回答,切记说出口后考官再进一步把自己坑进去了,切忌浮躁,好好复习
来源:https://www.cnblogs.com/zhmlearn/p/12201071.html