您的位置 首页 java

7 道 Java 高频面试题,看看你都会吗?

AQS 知道吗?讲讲你的理解

顺着这样的思路来讲:Why – What – How。为什么会有这个东西?这个东西是什么?怎么做到的?

任何技术的产生第一件事就是思考 Why?这个 AQS 的产生是为什么,大家平常用过 SYN chronized,可能觉得已经有锁了,还需要 AQS 吗?

Why: synchronized 只解决了有和没有的问题,但是锁的场景和多样性方面还很欠缺,例如: 带超时时间的获取锁、获取锁非阻塞(尝试获取锁)、带等待条件的请求锁。其实这些你通过 synchronized 加手动封装也能实现,但是需要些功力而且还容易出错,所以 Doug Lea 就写了功能更加丰富的 AQS 以及一些一系列多线程组件,方便大家按需扩展。AQS 是 JDK 1.5 引入的,那个时候 synchronized 还没做优化,没有偏向锁和轻量级锁,所以 AQS 的 CAS(Reentrantlock) 自选就很有必要了,毕竟有时间竞争并不激烈。

What: AQS 是 AbstractQueuedSynchronizer 的缩写,抽象队列同步器,我们很多同步工具 ReentrantLock、 Semaphore 、CountDownLatch、ReadWriteLock,CyclicBarrier 都是基于 AQS 实现的。

How: AQS 实现机制是什么呢?大家可以先不往下看,如果让你来实现一个 多线程 控制访问共享资源的工具,你会如何写?考虑这么几个问题:

  1. 当有多个 线程 竞争的时候,运行线程排队等待获取资源,如何做?
  2. 当某个线程使用完资源,如何通知正在排队等待的资源?
  3. synchronized 获取锁是阻塞的,也就是线程获取锁的时候一定会进入等待,但是如果希望实现一个线程过来访问,发现已经有其他线程持有锁了,直接返回,不希望产生锁竞争,怎么实现?

AQS 基本的原理是它提供了一套共享资源的访问的规范,通过 CLH(一个双向链表)的方式把线程等待管理起来。

7 道 Java 高频面试题,看看你都会吗?

它底层采用的是状态标志位( state 变量)+FIFO 队列的方式来记录获取锁、释放锁、竞争锁等一系列锁操作;

对于 AQS 而言,其中的 state 变量可以看做是锁,队列采用的是先进先出的双向链表,state 共享状态变量表示锁状态,内部使用 CAS 对 state 进行 原子操作 修改来完成锁状态变更(锁的持有和释放)。

实现一个生产者消费者模式

这个实现方式有很多种,通过 synchronized 的 wait、notify 可以,通过 Reentrantlock 的 Condition 也可以,当然还有别的方式,另外单独讲。

下面介绍第一种:

基本原理就是通过锁 + 等待/唤醒实现生产和消费

7 道 Java 高频面试题,看看你都会吗?

Java 内存模型清楚吗?

JMM 全称 Java Memory Model, 是 Java 中非常重要的一个概念,是 Java 并发编程的核心和基础。JMM 是 Java 定义的一套协议,用来屏蔽各种硬件和操作系统的内存访问差异,让 Java 程序在各种平台都能有一致的运行效果。

网络问题遇到过吗?TIME_WAIT 和 CLOSE_WAIT 的区别

答:这二个状态是四次挥手中的状态,TIME_WAIT 是主动关闭的一方发出 FIN 包会经过的状态,CLOSE_WAIT 是被动关闭连接的一端会经过的状态。TIME_WAIT 经过 2 个 MSL(最大报文段生存时间)才能到 CLOSE 状态,CLOSE_WAIT 如果不发送 FIN 报文会一直处在 CLOSE_WAIT 状态。所以一般在看机器连接状态,几千个 TIME_WAIT 一般是正常的(过 2MSL 自动关闭),处于 CLOSE _WAIT 状态的连接很多,证明有问题。

三次握手讲一讲

7 道 Java 高频面试题,看看你都会吗?

三次握手

对着上图看下面文字描述

  • 服务端进程启动,准备接收客户端进程的连接请求,此时接收方进入 LISTEN (监听)模式;
  • 三次握手第一步:客户端向服务端发出连接请求报文,这时报文首部 SYN 标志位为 1,同时设置一个初始序列号 seq = x(随机数); 做完这步动作,发送方进入 SYN_SENT (同步已发送状态) 。

名称解释:SYN:同步标志位 seq:包序列编号(每个包都有一个序列号)

第一次握手客户端发送的报文称为同步请求报文,希望与服务端建立同步连接,SYN 报文不携带数据。

  • 三次握手第二步:服务端收到来自客户端的连接请求报文后,需要确认收货,响应报文中 ACK(确认标志位)设置为 1,将确认号 ack 设置为第一步的请求序列号 seq 加 1(ack =x+1),另外自己也回客户端一个 SYN 包(可以建立同步连接),即 SYN + ACK 包,包序列号 seq = y,服务端进入 SYN_RCVD(同步收到)状态。

名词解释:ACK:确认状态位(这里 ACK=1),这个一定和 ack(32 位确认序号,这里 ack=x+1)区分开,可以看下面的 TCP 报文结构体图,ACK 是包的状态标志,ack 是确认序号。

  • 三次握手第三步:客户端收到来自服务端的 SYN + ACK 包,会发送一个 ACK 确认包,ACK =1,seq = x+1( 第二步的 ack),ack = y+1(第二步的 seq+1)。

想一想为什么需要三次握手来建立 TCP 连接?

答:

  • 第一次握手客户端发送报文给服务端,收到服务端的应答表明客户端发送数据的能力 ok;
  • 第二次握手服务端发送数据给客户端表示服务端接收数据能力 ok(我正常收到你的数据了,告诉你一声);
  • 第三次客户端发报文给服务端表明服务端的发送数据的能力也 ok,客户端接收数据能力也 ok(我能正常收到你的数据,代表你发的数据没问题,我的接收能力也没问题,所以告知你一声)。

所以要验证客户端和服务端发送&接收数据的能力都 ok 至少需要三次握手才能达到。

举个实际的例子,比如你投递简历,相当于第一次握手,HR 回复你简历已收到,相当于第二次握手,你回复 HR 已收到批准通知了,这相当于三次握手,HR 可以给你安排面试了。

因为每一次握手都有消息丢失的风险,所以需要往返至少三次才能保证连接的建立。

CountDownLatch、Seamphone、CyclicBarrier 都了解吗?

首先他们都是 基于 AQS 实现的

CountDownLatch 和 CyclicBarrier 都能够实现线程之间的等待,只不过它们侧重点不同:

  1. CountDownLatch 一般用于某个线程 A 等待若干个其他线程执行完任务之后,它才执行,就像上面主线程等 线程池 的几个其他线程执行结束再执行;
  2. CyclicBarrier 翻译过来叫回环栅栏,是不是感觉有点晕,我也晕,谁起的这名字,但无所谓,用过就理解了。一般作用是:用于一组线程互相等待至某个状态,然后这一组线程再同时执行;有个例子很生动,Barrier 的意思是栅栏,你可以把它想象成起跑的时候有根带子拦在所有参赛者面前,必须所有起跑者都到了带子的位置,大家才能开始一起跑。

CountDownLatch 是不能够重用的,而 CyclicBarrier 是可以重用的。这也是回环的意思。

CountDownLatch 可以用来让一个或多个线程等待,关于这个我写了个例子:

如下图所示, 一个任务是获取多个电话号码的电话信息,如果是单线程调用,效率不高,那分多个线程调用,主线程阻塞等待多个线程的调用完成,再组合结果。

CountDownLatch 从 n -> 0, 线程停止阻塞,继续向下执行。

这玩意说实话在多线程协同方面真的很好用,用了才知道 Doge Lea 还是挺牛逼的,有时候吧就觉得大牛的脑袋真好使,就应该用来写框架,用来做题纯属浪费。

7 道 Java 高频面试题,看看你都会吗?

在正规比赛中,终点线的那根带子叫撞线,也称“终点冲线”。

但是我们这里为了理解 CyclicBarrier,把它用在起跑的时候拦住所有队员,为的是所有人一起跑。

例子程序:

7 道 Java 高频面试题,看看你都会吗?

Semaphore 其实和锁有点类似,它一般用于控制对某组资源的访问权限。

比如很多时候用 Semaphore 做单机限流。

如下如图,虽然请求量有 1000,但是 Semaphore 可以控制同时只有 50 个线程在执行。

7 道 Java 高频面试题,看看你都会吗?

信号量,嗯,单机限流可以用。

以上三个都是基于 AQS 的,由于 AQS 实在重要,我会再写一篇专门讲 AQS 的文章。

private 、protected、public、关键字你平常怎么用的?

控制访问权限,很多时候我们会发现属性往往是 private 的,但是提供 public 的访问权限,父类中的抽象方法或者希望子类实现的方法是 protected 的,所以使用了 protected 了意图是很明显的,子类 可访问、可复用、可重写

  1. public:public 表明该数据成员、成员函数是对所有用户开放的,所有用户都可以直接进行调用
  2. private:private 表示私有,私有的意思就是除了 class 自己之外,任何人都不可以直接使用,私有财产神圣不可侵犯嘛,即便是子女,朋友,都不可以使用。
  3. protected:protected 对于子女来说,就是 public 的,可以自由使用,没有任何限制,而对于其他的外部 class,protected 就相当于 private。
7 道 Java 高频面试题,看看你都会吗?

访问权限表

另外一种是没有修饰的,子类也不能访问、重写,一般不建议用。

新生代和老生代区别?

二题合并讲。我建了一个 JVM 群,如果大家有兴趣可以加我好友 guofu-angela,备注 JVM 进群。

Java 虚拟机 通过 可达性分析 来判定对象是否存活。这个算法的基本思想是通过一系列称为”GC Roots”的对象作为起始点,从这些节点向下搜索,搜索走过的路径称为引用链,当一个对象到 GC Roots 没有与任何引用链相连时,则该对象是不可用的。

思考什么对象是”GC Roots”的对象?

熟悉几种常见的垃圾收集方法:

算法分为 “标记”和”清除” 两个阶段:首先标记出需要回收的对象,在标记完成后统一回收被标记的对象。

它主要不足有两个:

  • 一是 效率问题 ,标记和清除两个过程效率都不高。
  • 二是 空间问题 ,标记清除后会产生大量不连续内存碎片,碎片太多可能导致要分配较大对象时,无法找到足够的内存空间不得不提前触发一次垃圾收集动作。

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当一块内存用完了,将存活的对象复制到另一块上面,然后把已使用的内存空间一次清理掉。

这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等情况,只要移动堆顶指针,按顺序分配内存即可, 实现简单,运行高效

只是这种算法将 内存缩小为原来的一半 ,代价较高。

标记过程与”标记-清除”算法一样,但后续不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

商业虚拟机(如 Hotspot)的垃圾收集都采用分代收集算法,根据对象存活周期将内存划分为几块。

Java 堆分为 新生代 老年代 ,这样可以根据年代特点采用适当的收集算法。新生代中每次垃圾收集都有大批对象死去,那就选用复制算法。老年代对象存活率高,没有额外空间进行分配 担保 ,适合使用”标记-清理”或”标记-整理”算法来回收。

内存分配与回收策略

新生代 GC(Minor GC):发生在新生代的垃圾收集动作,因为 Java 对象大多朝生夕死,所以 Minor GC 非常频繁,回收速度也较快。

老年代 GC(Major GC/Full GC):发生在老年代的垃圾收集动作。出现 Major GC,经常会伴随至少一次 Minor GC。Major GC 的速度一般比 Minor GC 慢。

4. 分代收集算法

3. 标记-整理算法(Mark-Compact)

2. 复制算法

1. 标记-清除算法(Mark-Sweep)

虚拟机栈中栈桢中的局部变量(也叫局部变量表)中引用的对象

方法区中类的静态变量、常量引用的对象

本地方法栈中 JNI (Native 方法)引用的对象

  • 对象优先在 Eden 分区:

大多数情况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够空间分配时,虚拟机发起一次 Minor GC。GC 后对象尝试放入 Survivor 空间,如果 Survivor 空间无法放入对象时,只能通过空间分配担保机制提前转移到老年代。

  • 大对象直接进入老年代:

大对象指需要大量连续内存空间的 Java 对象。虚拟机提供-XX:PretenureSizeThreshold 参数,如果大于这个设置值对象则直接分配在老年代。这样可以避免新生代中的 Eden 区及两个 Survivor 区发生大量内存复制。

  • 长期存活的对象进入老年代:

虚拟机会给每个对象定义一个对象年龄计数器。如果对象在 Eden 出生并且经过一次 Minor GC 后任然存活,且能够被 Survivor 容纳,将被移动到 Survivor 空间中,并且对象年龄设为 1.每次 Minor GC 后对象任然存活在 Survivor 区中,年龄就加一,当年龄到达-XX:MaxTenuringThreshold 参数设定的值时,将会移动到老年代。

  • 动态年龄判断:

虚拟机不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold 设定的值才会将对象移动到老年代去。如果 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象可以直接进入老年代。

  • 空间分配担保:

在 Minor GC 前,虚拟机会检查老年代最大可用连续空间是否大于新生代所有对象总空间,如果条件成立,那么 Minor GC 是成立的。

如果不成立,虚拟机查看 Handle PromotionFailure 设置值是否允许担保失败。

如果允许,那么会继续检查老年代最大可用连续空间是否大于历次移动到老年代对象的平均大小;

如果大于,将尝试一次 Minor GC。如果小于,或者 HandlePromotionFailure 设置值不允许冒险,那将进行一次 Full GC。

垃圾回收器有哪几种? 你们生产环境用的哪种或哪几种?

Serial、ParNew、Parallel、CMS、G1

  • 优点:并发收集、低停顿
  • 缺点:产生大量空间碎片、并发阶段会降低吞吐量

使用方式:

-XX:+UseConcMarkSweepGC 使用 CMS 收集器

-XX:+ UseCMSCompactAtFullCollection Full GC 后,进行一次碎片整理;整理过程是独占的,会引起停顿时间变长

-XX:+CMSFullGCsBeforeCompaction 设置进行几次 Full GC 后,进行一次碎片整理

-XX:ParallelCMSThreads 设定 CMS 的线程数量(一般情况~可用 CPU 数量)

  • G1 收集器(Garbage First):对 G1 的研究不多,因为这玩意没在生产环境用过,就 测试环境 试过,感兴趣可以试试。G1 将内存区域划分为多个大小相等的独立区域(Region),使得它可以回收堆中的任何一个区域,而不是像其它的垃圾收集器要么只能回收新生代,要么只能回收老年代。但不是说 G1 就没有新生代和老年代了,它的每个 Region 都可以根据需要扮演 Eden、Survivor 或老年代,垃圾收集器也会针对不同角色的 Region 采用不同的策略去处理。

G1 的运行过程如上,它也包含了以下 4 个步骤:

  • Serial: 串行垃圾回收器在进行垃圾回收时,它会持有所有应用程序的线程,冻结所有应用程序线程,使用单个垃圾回收线程来进行垃圾回收工作。串行垃圾回收器是为单线程环境而设计的,如果你的程序不需要多线程,启动串行垃圾回收。

使用方法:-XX:+UseSerialGC 串联收集

  • ParNew: ParNew 收集器其实就是 Serial 收集器的多线程版本。新生代并行,老年代串行;新生代复制算法、老年代标记-压缩

使用方法:-XX:+UseParNewGC ParNew 收集器

Parallel: Parallel Scavenge 收集器类似 ParNew 收集器,Parallel 收集器更关注系统的吞吐量。可以通过参数来打开自适应调节策略,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大的吞吐量;也可以通过参数控制 GC 的时间不大于多少毫秒或者比例;新生代复制算法、老年代标记-压缩

  • Parallel Old 收集器: Parallel Old 是 Parallel Scavenge 收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在 JDK 1.6 中才开始提供。
  • CMS(Concurrent Mark Sweep)收集器: 是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的 Java 应用都集中在互联网站或 B/S 系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS 收集器是基于“标记-清除”算法实现的,它的运作过程相对于前面几种收集器来说要更复杂一些,整个过程分为 4 个步骤,包括:

初始标记: STW,也是只标记 GC Roots 直接关联的对象,并修改 TAMS 的指针值(G1 为每一个 Region 设计了两个名为 TAMS(Top at Mark Start)的指针,把 Region 中的一部分空间划分出来用于并发回收过程中的新对象分配,并发回收时新分配的对象地址都必须要在这两个指针位置以上,垃圾回收时也不会回收这部分空间),这个过程耗时很短,而且是借用进行 Minor GC 的时候同步完成的,所以 G1 收集器在这个阶段实际并没有额外的停顿。

并发标记: 可达性分析找出要回收的对象,在对象扫描完成后,由于是与用户线程并发执行的,所以存在引用变动的对象,这部分对象会由 SATB 算法来解决(原始快照,下一篇详细分析)。

最终标记: STW,处理并发阶段遗留的少量遗留的 SATB 记录。

筛选回收: 根据用户设定的-XX:MaxGCPauseMillis 最大 GC 停顿时间对 Region 进行排序,并回收价值最大的 Region,尽量保证满足参数设定的值(该值效果和 Parallel Scavenge 部分讲解的是一样的)。

这里的回收算法就是讲存活的对象 复制 到空的 Region 中,即 G1 局部 Region 之间采用的是 复制算法 ,而整体上采用的是 标记整理算法

特点:

G1 适合上百 G 的堆空间回收,与 CMS 的权衡在 6~8G 之间,较大的堆内存才能凸显 G1 的优势,

使用方式:

可以通过-XX:+UseG1GC 参数开启。

初始标记 (CMS initial mark)

并发标记 (CMS concurrent mark)

重新标记 (CMS remark)

并发清除 (CMS concurrent sweep)

其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。

初始标记仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,并发标记阶段就是进行 GC Roots Tracing 的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。

由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS 收集器的内存回收过程是与用户线程一起并发地执行。老年代收集器(新生代使用 ParNew)

文章来源:智云一二三科技

文章标题:7 道 Java 高频面试题,看看你都会吗?

文章地址:https://www.zhihuclub.com/191719.shtml

关于作者: 智云科技

热门文章

网站地图