每天十道面试题-20200326

题目

  • 1、synchronized与lock的区别,使用场景。看过synchronized的源码没
  • 2、JVM自动内存管理,Minor GC与Full GC的触发机制
  • 3、了解过JVM调优没,基本思路是什么?如何确定各个代的大小呢
  • 4、volatile关键字的如何保证内存可见性
  • 5、happen-before原则
  • 6、你说你熟悉并发编程,那么你说说Java锁有哪些种类,以及区别
  • 7、如何保证内存可见性
  • 8、Http请求的过程与原理
  • 9、三次握手与四次挥手?TCP连接的特点 TCP连接如何保证安全可靠的
  • 10、 为什么TCP连接需要三次握手,两次不可以吗,为什么【四次挥手呢?】

解答

题目一
  • 题干:synchronized与lock的区别,使用场景。看过synchronized的源码没
  • 分析:
  • 考察内置锁synchronized和Lock锁的区别,首先synchronized是jdk的一种内置锁,它是一种悲观的、可重入的、不公平的、重量级的【需要调用操作系统的相关接口】锁,jdk1.6之后对synchronized进行了一些优化,如锁消除、自旋锁、适应性自旋、偏向锁、轻量级锁,而Lock相对于synchronized来讲是一种更高级的锁,因为synchronized是一种互斥锁,使用synchronized时我们无法中断一个获取锁的线程,而且在获取锁的时候如果获取不到则会一直等待下去,而且需要使用在代码块上,当然它也有自身的好处,作为关键字使用简单,不需要显示释放锁,引入Lock锁就是来解决synchronized的一些劣势的,提供了tryLock可以防止一直等待下去有超时参数,提供lockInterruptibly方法来中断获取锁的线程,而且加锁的时候没有规定必须要在代码块中使用,当然它也有不好的地方,需要显示的释放在finally中显示释放锁,如果不释放会出现问题,此外Lock的实现类ReentrantLock提供了公平锁和非公平锁的选择,但是数据证明,非公平锁性能更好,故虽然Lock锁更高级一些,如果synchronized能够满足我们的使用时就没必要使用Lock,如果Lock满足不了在使用。
    synchronized的源码:每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加 上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记位来判断的。

  • 回答:
  • 区别见分析部分源码-1、JVM 每次从队列的尾部取出一个数据用于锁竞争候选者(OnDeck),但是并发情况下, ContentionList会被大量的并发线程进行CAS访问,为了降低对尾部元素的竞争,JVM会将 一部分线程移动到EntryList中作为候选竞争线程。 2. Owner 线程会在 unlock 时,将 ContentionList 中的部分线程迁移到 EntryList 中,并指定 EntryList中的某个线程为OnDeck线程(一般是最先进去的那个线程)。 3. Owner 线程并不直接把锁传递给 OnDeck 线程,而是把锁竞争的权利交给 OnDeck, OnDeck需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在 JVM中,也把这种选择行为称之为“竞争切换”。 4. OnDeck线程获取到锁资源后会变为Owner线程,而没有得到锁资源的仍然停留在EntryList 中。如果Owner线程被wait方法阻塞,则转移到WaitSet队列中,直到某个时刻通过notify 或者notifyAll唤醒,会重新进去EntryList中。 5. 处于 ContentionList、EntryList、WaitSet 中的线程都处于阻塞状态,该阻塞是由操作系统 来完成的(Linux内核下采用pthread_mutex_lock内核函数实现的)。 6. Synchronized是非公平锁。 Synchronized在线程进入ContentionList时,等待的线程会先 尝试自旋获取锁,如果获取不到就进入 ContentionList,这明显对于已经进入队列的线程是 不公平的,还有一个不公平的事情就是自旋获取锁的线程还可能直接抢占 OnDeck 线程的锁 资源。 参考:https://blog.csdn.net/zqz_zqz/article/details/70233767 7. 每个对象都有个 monitor 对象,加锁就是在竞争 monitor 对象,代码块加锁是在前后分别加 上monitorenter和monitorexit指令来实现的,方法加锁是通过一个标记位来判断的 8. synchronized 是一个重量级操作,需要调用操作系统相关接口,性能是低效的,有可能给线 程加锁消耗的时间比有用操作消耗的时间更多。。

题目二
  • 题干:JVM自动内存管理,Minor GC与Full GC的触发机制
  • 分析:
  • 考察年轻代的回收和整个堆的回收机制,Minor GC可以理解为对于年轻代的回收,FullGC则是收集整个堆的,总的来讲收集垃圾从本质上来讲一定是内存不够用了或者防止内存不够用了。

  • 回答:
  • 触发MinorGC的机制:就是Eden满了【无论是Eden块使用完了,还是新分配的对象太大Eden剩余空间放不下】,就是Eden使用达到99%,也就是说eden不能容纳更多对象,而后又需要有新的对象产生,也就是Eden 快要满的时候会触发MinorGC.发生MinorGC,对象会从Eden区进入Survivor区,如果Survivor区放不下从Eden区过来的对象时,此时会使用分配担保机制将对象直接移动到年老代。每进行一次MinorGC都会增长一岁,MaxTenuringThresHold 默认值是 15.但是我们呢需要知道的是MaxTenuringThresHold指的是最大的晋升年龄,它是对象今生的充分非必要条件,【即达到该年龄对象必然晋升,而未达到该年龄,对象也有可能晋升,事实上,对象的实际晋升年龄,是由虚拟机在运行时自行判断的.
    FuccGC触发机制:老年代空间不够使用的时候,还有当准备要触发一次young GC时,如果发现统计数据说之前young GC的平均晋升大小比目前old gen剩余的空间大,则不会触发young GC而是转为触发full GC(因为HotSpot VM的GC里,除了CMS的concurrent collection之外,其它能收集old gen的GC都会同时收集整个GC堆,包括young gen,所以不需要事先触发一次单独的young GC);或者,如果有perm gen的话,要在perm gen分配空间但已经没有足够空间时,也要触发一次full GC;或者System.gc()、heap dump带GC,默认也是触发full GC。

题目三
  • 题干:了解过JVM调优没,基本思路是什么?如何确定各个代的大小呢
  • 分析:
  • 思路:首先是要查看GC日志,然后分析MinorGC和FullGC时间及频度,根据指标确定是否要调优,如果要调优根据具体原因修改参数,进行调试然后找到最合适的参数值,常用的参数值Xms堆的初始化大小,Xmx最大堆内存,如 java -Xmx1024m、-Xss:线程栈大小一般将 -Xms 和 -Xmx 设置为相同值。

  • 回答:
  • 根据GC日志来判断是否需要调优,如果调优对于相关参数进行修改,然后不断测试,使用jConcel、jMap等根据辅助。。

题目四
  • 题干:volatile关键字的如何保证内存可见性

  • 分析:

  • 考察内存模型以及volatile,Java内存模型在主内存上有一个所有线程共享的二级缓存,对于运行线程的每个核都有自己的一级缓存以及寄存器控制器等这个就是内存模型,volatile语义:使用volatile的时候一个变量被它修饰,线程在写入变量的时候不会把值写到缓存寄存器或者其他地方比如以及缓存和二级缓存,而是直接刷新到主内存。所以当其他线程直接读取的时候直接去主内存中读取。而不是使用当前线程的工作内存。

  • 回答:

  • 写入volatile变量等价于线程退出syn关键字【把在synchronized块内对共享变量的的修改刷新到主内存。

    读取volatile变量等价于线程进入syn关键字【清空本地内存的值,直接从主内存读取】。线程在写入变量的时候不会把值写到缓存寄存器或者其他地方比如以及缓存和二级缓存,而是直接刷新到主内存。

题目五
  • 题干:happen-before原则
  • 分析:
  • 就是一个A操作先行发生于操作B,那么A做的修改B就都可以看到。

  • 回答:
  • 单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
    锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
    volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操作(当然也包括写操作了)。
    happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操作,那么A操作happen-before C操作。
    线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
    线程中断的happen-before原则:对线程interrupt方法的调用happen-before被中断线程的检测到中断发送的代码。
    线程终结的happen-before原则:线程中的所有操作都happen-before线程的终止检测。
    对象创建的happen-before原则:一个对象的初始化完成先于他的finalize方法调用。

题目六
  • 题干:你说你熟悉并发编程,那么你说说Java锁有哪些种类,以及区别
  • 分析:
  • 悲观锁:就像synchronized关键字一样是一种互斥锁;
    乐观锁:就像CAS操作通过版本号或者时间戳来规避ABA问题
    乐观锁核悲观锁都是一种思想。
    在Java并发中为了避免重量级的synchronized互斥锁操作,引入了自旋锁、适应性自旋锁、偏向锁、轻量级锁。

  • 回答:
  • 自旋锁:为了避免上下文切换,然获取锁的线程自旋一下【就是for循环什么都不做占用CPU一段时间】,这样在一段时间之后如果之前占用锁的线程释放掉该锁,那么自旋的线程就可以直接获取锁,如果自旋的时间小于上下文切换的时间那么就是提升性能。但是这个时间很难控制;
    适应性自旋:因为自旋锁的时间问题,所以引入适应性自旋,就是JVM根据CPU释放锁、上下文切换的时间在确定一个自旋的时间,来更大概率的让线程自旋后获得锁。
    因为大多数的锁都没有锁竞争,而且多数都是被一个线程获取到。
    偏向锁:偏向于第一个访问锁的线程,如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
    轻量级锁:轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁; 如果出现其他的线程来获取锁,使用CAS操作,来将对象有的Mark Word指向LockRecord如果更新成功则轻量级锁加锁成功,如果失败,则检查对象的Mark Word 是否指向当前线程的栈帧,如果是则说明获取到锁直接进同步块,否则说明是被其他线程抢占出现多个线程竞争,轻量级锁升级为重量级锁。
    【偏向锁、轻量级锁、无锁都是在对象头的Mark Word中的属性对应着】

题目七
  • 题干:如何保证内存可见性
  • 分析:
  • 讲一下内存模型说一下,synchronized和volatile保证内存可见性。

  • 回答:
  • 主内存外有一层二级缓存,每个线程都有自己的工作内存,然后那个核都有自己的以及缓存核寄存器等,因为这样的模型机会出现工作内存的数据修改没有及时刷新到主内存的情况,我们可以使用synchronized和volatile来保证内存可见性,一般使用volatile。

题目八
  • 题干:http请求的过程与原理

  • 分析:

  • 首先作为发送端的客户端在应用层(HTTP协议)发出一个想看某个web也页面的HTTP请求。
    接着,为了传输方便,在传输层(TCP协议)把从应用层处收到的数据(HTTP请求报文)进行分割,并在各个报文上打上标记序号及端口号后转发给网络层。
    在网络层(IP协议),增加作为通信目的地的MAC地址后转发给链路层。这样一来,发往网络的通信请求就准备齐全了。
    接收端的服务器在链路层接收到数据,按序往上层发送,一直到应用层。当传输层到应用层才能算真正接收到由客户端发送来的HTTP请求。

  • 回答:

  • 同上

题目九
  • 题干:三次握手与四次挥手?TCP连接的特点 TCP连接如何保证安全可靠的

  • 分析:

  • 三次握手用来建立连接,四次挥手用来断开连接,三次握手:第一次握手:建立连接时,客户端发送syn包(syn=x)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)。
    第二次握手:服务器收到syn包,必须确认客户的SYN(ack=x+1),同时自己也发送一个SYN包(syn=y),即SYN+ACK包,此时服务器进入SYN_RECV状态;
    第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包ACK(ack=y+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手。
    四次挥手:第一次挥手:客户端发送一个FIN=M,用来关闭客户端到服务器端的数据传送,客户端进入FIN_WAIT_1状态。意思是说"我客户端没有数据要发给你了",但是如果你服务器端还有数据没有发送完成,则不必急着关闭连接,可以继续发送数据。
    第二次挥手:服务器端收到FIN后,先发送ack=M+1,告诉客户端,你的请求我收到了,但是我还没准备好,请继续你等我的消息。这个时候客户端就进入FIN_WAIT_2 状态,继续等待服务器端的FIN报文。
    第三次挥手:当服务器端确定数据已发送完成,则向客户端发送FIN=N报文,告诉客户端,好了,我这边数据发完了,准备好关闭连接了。服务器端进入LAST_ACK状态。
    第四次挥手:客户端收到FIN=N报文后,就知道可以关闭连接了,但是他还是不相信网络,怕服务器端不知道要关闭,所以发送ack=N+1后进入TIME_WAIT状态,如果Server端没有收到ACK则可以重传。服务器端收到ACK后,就知道可以断开连接了。客户端等待了2MSL后依然没有收到回复,则证明服务器端已正常关闭,那好,我客户端也可以关闭连接了。最终完成了四次握手。
    保证:针对发送端发出的数据包的确认应答信号ACK。针对数据包丢失或者出现定时器超时的重发机制。针对数据包到达接收端主机顺序乱掉的顺序控制、针对高效传输数据包的流动窗口控制。针对避免网络拥堵时候的流量控制。针对刚开始启动的时候避免一下子发送大量数据包而导致网络瘫痪的慢启动算法和拥塞控制。

  • 回答:

  • 同上

题目十
  • 题干:为什么TCP连接需要三次握手,两次不可以吗,为什么【四次挥手呢?】
  • 分析:
  • 为了可靠的建立和关闭连接。

  • 回答:
  • 因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。如果SYN发送后,服务端返回SYN+ACK,这个时候我们不知道Server的情况所以两次不可以。而关闭连接时,当Server端收到FIN报文时,很可能并不会立即关闭SOCKET,所以只能先回复一个ACK报文,告诉Client端,“你发的FIN报文我收到了”。只有等到我Server端所有的报文都发送完了,我才能发送FIN报文,因此不能一起发送。故需要四步握手。

发布了122 篇原创文章 · 获赞 32 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/YangzaiLeHeHe/article/details/105094829