关于面试:2021Java后端工程师面试指南并发多线程

5次阅读

共计 8506 个字符,预计需要花费 22 分钟才能阅读完成。

前言

文本已收录至我的 GitHub 仓库,欢送 Star:https://github.com/bin3923282…
种一棵树最好的工夫是十年前,其次是当初

Tips

面试指南系列,很多状况下不会去深挖细节,是小六六以被面试者的角色去回顾常识的一种形式,所以我默认大部分的货色,作为面试官的你,必定是懂的。

https://www.processon.com/vie…

下面的是脑图地址

叨絮

可能大家感觉有点陈词滥调了,的确也是。面试题,面试宝典,轻易一搜,基本看不完,也看不过去,那我写这个的意义又何在呢?其实嘛我写这个的有以下的目标

  • 第一就是通过一个体系的温习,让本人后面的写的文章再从新的过一遍,总结升华嘛
  • 第二就是通过写文章帮忙大家建设一个温习体系,我会将大部分会问的的知识点以点带面的模式给大家做一个导论

而后上面是后面的文章汇总

  • 2021-Java 后端工程师面试指南 -(引言)
  • 2021-Java 后端工程师面试指南 -(Java 根底篇)

明天来看看多线程的,这块是重点,也是难点,硬核有点多哈哈。

## 并发
记得阿里的第一个题就是面的并发,哈哈 这个小六六得好好总结了。

### 聊聊 Java 的并发模型
这个为啥是第一个问题,必定是有起因的,如果连 Java 的并发模型都不分明,你跟我扯一堆的锁,一堆的 juc 有啥用呢?

    • Java 并发 采纳的是 共享内存模型,Java 线程之前的通信总是隐式进行的。
    • Java 线程通信由 Java 内存模型(简称 JMM)管制,JMM 决定一个线程对共享变量的写入何时对另一个线程可见。从形象角度看,JMM 定义了 线程 和 主内存 之间的形象关系:线程之间的共享变量贮存在主内存中,每个线程都有一个公有的本地内存,本地内存贮存了 该线程 以读共享变量的正本。

      对多线程理解吗,说说你平时怎么对临界资源的访问控制的。

      其实这个题就是一个引人,由浅入深的过程,

      • 如果对应的临界资源是在单 JVM 的过程中,那么咱们能够用 Synchronized 和 lock
      • 对于分布式环境下的多线程中,那么就得用上分布式锁(redis 或者 zookeeper 实现)

      那么聊聊你对 Synchronized 的意识吧

      • synchronized 关键字解决的是多个线程之间拜访资源的同步性,synchronized 关键字能够保障被它润饰的办法或者代码块在任意时刻只能有一个线程执行。
    • synchronized 最次要三种用法

      - 润饰实例办法 要取得以后对象实例的锁
      • 润饰静态方法 取得以后类对象的锁
      • 润饰代码块 synchronized(this|object) 示意进入同步代码库前要取得给定对象的锁。synchronized(类.class) 示意进入同步代码前要取得 以后 class 的锁
    • synchronized 关键字最次要的二种底层实现形式:

      • synchronized 同步语句块的实现应用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始地位,monitorexit 指令则指明同步代码块的完结地位。wait/notify 等办法也依赖于 monitor 对象,这就是为什么只有在同步的块或者办法中能力调用 wait/notify 等办法,否则会抛出 java.lang.IllegalMonitorStateException 的异样的起因。
      • synchronized 润饰的办法并没有 monitorenter 指令和 monitorexit 指令,获得代之的的确是 ACC_SYNCHRONIZED 标识,该标识指明了该办法是一个同步办法。JVM 通过该 ACC_SYNCHRONIZED 拜访标记来分别一个办法是否申明为同步办法,从而执行相应的同步调用。

    聊聊 Java 对象的布局

    • 首先咱们晓得 Java 对象散布由三个局部组成 对象头、实例数据、对对齐填充字节,上面咱们来一个个说说

      • 对象头的组成由 Mark Word、类元数据的指针(Klass Pointer)、数组长度(不肯定有),在 64 位 Java 虚拟机外面的 Mark word 蕴含了咱们的 hashcode 的值 咱们的分代年龄 锁标记位等
      • 实例数据 并不是所有的变量都寄存在这里,对象的的所有成员变量以及其父类的成员变量是寄存在这里的。
      • JVM 要求 Java 对象的大小必须是 8byte 的倍数,所以这个的作用就是把对象的大小补齐至 8byte 的倍数。

    那你说说 Synchronized 锁降级的过程吧

    锁级别从低到高顺次是:

    • 无锁状态
    • 偏差锁状态
    • 轻量级锁状态
    • 重量级锁状态

    小六六在这给大家明确的一点就是,Synchronized 锁的是对象而不是其包裹的代码。

    • 对象被 new 进去后,没有任何线程持有这个对象的锁,这时就是无锁状态;Mark Word 锁标识是 01
    • 当且仅当只有一个线程 A 获取到这个对象的锁的时候,对象就会从无锁状态降级成为偏差锁,Mark Word 中就会记录这个线程的标识(锁标识是 01),此时线程 A 持有这个对象的锁;
    • 还是这个线程 A 再次获取这个对象的锁时,发现他是一个偏差锁,并且对象头中记录着本人的线程标识,那么线程 A 就持续应用这把锁(不须要 cas 去获取锁了)。这里也是锁的可重入性,所以,synchronized 也是可重入锁;
    • 在线程 A 持有锁的时候,线程 B 也来争抢这把锁了,线程 B 发现这是一把偏差锁,并且对象头中的线程标识不是本人。那么首先进行偏差锁的撤销过程,而后偏差锁就会降级为轻量级锁,此时 Mark Word 的锁标识是 00
    • 又有一些线程来争抢这个轻量级锁了,争抢的过程其实就是利用 CAS 自旋。为了防止长时间自旋耗费 CPU 资源,当自旋超过 10 次的时候,轻量级锁降级为重量级锁(其余线程阻塞,不会消耗 cpu)。此时 Mark Word 的锁标识是 10

    能够聊聊 CAS 吗,它有什么问题吗?

    CAS,compare and swap 的缩写,就是一个保障原子性的一个伎俩,CAS 操作蕴含三个操作数 —— 内存地位(V)、预期原值(A)和新值(B)。如果内存地位的值与预期原值相匹配,那么处理器会主动将该地位值更新为新值。否则,处理器不做任何操作。利用 CPU 的 CAS 指令,同时借助 JNI 来实现 Java 的非阻塞算法。其它原子操作都是利用相似的个性实现的。而整个 J.U.C 都是建设在 CAS 之上的,因而对于 synchronized 阻塞算法,J.U.C 在性能上有了很大的晋升。

    问题

    • ABA 问题。因为 CAS 须要在操作值的时候查看下值有没有发生变化,如果没有发生变化则更新,然而如果一个值原来是 A,变成了 B,又变成了 A,那么应用 CAS 进行查看时会发现它的值没有发生变化,然而实际上却变动了。ABA 问题的解决思路就是应用版本号。在变量后面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A。
    • 循环工夫长开销大。自旋 CAS 如果长时间不胜利,会给 CPU 带来十分大的执行开销。
    • 只能保障一个共享变量的原子操作。当对一个共享变量执行操作时,咱们能够应用循环 CAS 的形式来保障原子操作,然而对多个共享变量操作时,循环 CAS 就无奈保障操作的原子性,这个时候就能够用锁。

      能够说说 Synchronized 和 Lock 的区别嘛

    • 首先 synchronized 是 java 内置关键字,在 jvm 层面,Lock 是个 java 类;
    • synchronized 会主动开释锁 (a 线程执行完同步代码会开释锁;b 线程执行过程中产生异样会开释锁),Lock 需在 finally 中手工开释锁(unlock() 办法开释锁),否则容易造成线程死锁;
    • synchronized 的锁可重入、不可中断、非偏心,而 Lock 锁可重入、可中断、可偏心(两者皆可)
    • synchronized 原始采纳的是 CPU 乐观锁机制,即线程取得的是独占锁。独占锁意味着其余线程只能依附阻塞来期待线程开释锁。而在 CPU 转换线程阻塞时会引起线程上下文切换,当有很多线程竞争锁的时候,会引起 CPU 频繁的上下文切换导致效率很低。
    • 而 Lock 用的是乐观锁形式。所谓乐观锁就是,每次不加锁而是假如没有抵触而去实现某项操作,如果因为抵触失败就重试,直到胜利为止。乐观锁实现的机制就是 CAS 操作(Compare and Swap)。咱们能够进一步钻研 ReentrantLock 的源代码,会发现其中比拟重要的取得锁的一个办法是 compareAndSetState。这里其实就是调用的 CPU 提供的非凡指令。

    既然提到了 Lock,那咱们来聊聊他最罕用的实现 ReentrantLock 吧,说说的偏心和非偏心是怎么实现的,他们的哪个效率高,默认是哪个,又是怎么事实可重入的

    • 首先偏心和非偏心是指多线程下各线程获取锁的程序,先到的线程优先获取锁,而非偏心锁则无奈提供这个保障,然而呢 咱们晓得 ReentrantLock 的非偏心实现,其实并不是随机的,它是有肯定程序的非偏心,举个非偏心的例子,假如 A 来获取锁,如果 A 取得了锁,此时 B 来获取锁,而后 B 失败了,B 就去队列期待,此时 C 来了,而后 C 也失败了,他也去期待,此时 D 过去了,而后 A 开释了锁,那你说如果是相对偏心的话这个时候应该是 B 获取锁才对,然而源码中是 D 此时有机会去获取锁,所以它是肯定程序的非偏心,非偏心锁效率高于偏心锁的,因为非偏心锁缩小了线程挂起的几率,起初的线程有肯定几率逃离被挂起的开销,所以默认是非偏心的锁。而咱们的 Synchronized 也是非偏心的
    • 可重入是指,当我一个线程获取了这把锁,下次以后线程再开释锁之前再去获取锁的时候是能够胜利,这就是可重入锁,Lock 的实现是判断是以后线程的时候,给锁状态 +1,而后咱们的 Synchronized 也是可重入的

    咱们的 ReentrantLock 的外围是 AQS,那么它怎么来实现的,继承吗? 说说其类内部结构关系,聊聊它的上锁过程。

    这个可能很多人不是很明确,然而我是站在被面试官问的角色,所以很多前置常识我默认你懂,嘿嘿,倡议大家去看我这篇 AQS

    • 首先要论述几个概念,AQS 全称是 AbstractQueuedSynchronizer,这个就是咱们所说所有无锁并发类的基石,其实就是一个队列,那么咱们须要重点关注它其中几个字段,Node(用来封装线程的)还有 State 锁的状态
    • Node 类的设计,外面有几个要害的字段他其实是一个双向链表底层,有它的前驱 有它的后继,还有线程的封装
    • 咱们来说说 Lock 的上锁过程吧

      • ReentrantLock 对象首先调用 lock 办法,尝试去上锁,首先当然是判断是偏心还是非偏心的形式去加锁(默认是说的是非偏心),而后 CAS 判断 AQS 外面的 state 的状态,如果小于等于 0,阐明没有人用,而后判断队列是否为空,而后 CAS 去创立队列等,最初胜利拿到锁
      • 如果拿到锁失败,就要把以后线程封装成一个 Node 去入队了,然而再封装好 Node 之后,(如果是非偏心的状况下)以后线程其实还是有一个机会尝试去拿锁的,如果这次它还失败了,那他就只能怪乖的入队了,这么做的目标还是为了缩小线程切换的开销。
      • 当然 aqs 的货色还很多,我也是仅仅把本人懂得表述进去。

      聊聊 volatile 吧

      • 可见性:总是能看到(任意线程)对这个 volatile 变量最初的写入。
      • 原子性:对任意单个 volatile 变量的读 / 写具备原子性,但相似于 volatile++ 这种复合操作不具备原子性。
      • 内存语义,当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为有效。线程接下来将从主内存中读取共享变量。当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。

      说说线程吧,它有哪些状态

    • NEW 初始状态
    • RUNNABLE 运行状态
    • BLOCKED 阻塞状态
    • WAITING 期待状态
    • TIME_WAITING 超时期待状态
    • TERMINATED 终止状态

    聊聊阻塞与期待的区别

    • 阻塞:当一个线程试图获取对象锁(非 java.util.concurrent 库中的锁,即 synchronized),而该锁被其余线程持有,则该线程进入阻塞状态。它的特点是应用简略,由 JVM 调度器来决定唤醒本人,而不须要由另一个线程来显式唤醒本人,不响应中断。
    • 期待:当一个线程期待另一个线程告诉调度器一个条件时,该线程进入期待状态。它的特点是须要期待另一个线程显式地唤醒本人,实现灵便,语义更丰盛,可响应中断。例如调用:Object.wait()、Thread.join()以及期待 Lock 或 Condition。期待 一个线程进入了锁,然而须要期待其余线程执行某些操作
    • 须要强调的是尽管 synchronized 和 JUC 里的 Lock 都实现锁的性能,但线程进入的状态是不一样的。synchronized 会让线程进入阻塞态,而 JUC 里的 Lock 是用 LockSupport.park()/unpark()来实现阻塞 / 唤醒的,会让线程进入期待态。但话又说回来,尽管等锁时进入的状态不一样,但被唤醒后又都进入 runnable 态,从行为成果来看又是一样的。一个线程进入了锁,然而须要期待其余线程执行某些操作

    说说 sleep() 办法和 wait() 办法区别和共同点?

    • 两者最次要的区别在于:sleep() 办法没有开释锁,而 wait() 办法开释了锁
    • wait() 办法被调用后,线程不会主动昏迷,须要别的线程调用同一个对象上的 notify() 或者 notifyAll() 办法。sleep() 办法执行实现后,线程会主动昏迷。或者能够应用 wait(long timeout) 超时后线程会主动昏迷。
    • wait 须要配合 synchronized 应用

    为什么咱们调用 start() 办法时会执行 run() 办法,为什么咱们不能间接调用 run() 办法?

    调用 start() 办法方可启动线程并使线程进入就绪状态,间接执行 run() 办法的话不会以多线程的形式执行。

    聊聊多个线程的同时拜访,比如说咱们的 Semaphore

    • synchronized 和 ReentrantLock 都是一次只容许一个线程拜访某个资源,Semaphore(信号量)能够指定多个线程同时拜访某个资源。
    • final Semaphore semaphore = new Semaphore(20) 构造方法,确定最多有多少线程资源应用的凭证,semaphore.acquire(1) 从总的那边借凭一个证过去,semaphore.release(1)开释 1 个凭证。

    说说 CountDownLatch(倒计时器)吧

    • CountDownLatch 容许 count 个线程阻塞在一个中央,直至所有线程的工作都执行结束,并且他是基于 AQS 实现的。
    • 而后 CountDownLatch 外面的构造方法传的参数,其实就是设置 AQS 外面的 state,而后它的 wait 办法,其实就是很简略,就是判断它的 state 是否为 0,而且是始终自旋的判断,而后 countDown 办法,就是 state-1,当然源码没那么简略,只是小六六大抵艰深的了解

    说说 CyclicBarrier(循环栅栏)

    • CyclicBarrier 和 CountDownLatch 十分相似,它也能够实现线程间的技术期待,然而它的性能比 CountDownLatch 更加简单和弱小。次要利用场景和 CountDownLatch 相似。
    • CyclicBarrier 外部通过一个 count 变量作为计数器,cout 的初始值为 parties 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减一。如果 count 值为 0 了,示意这是这一代最初一个线程达到栅栏,就尝试执行咱们构造方法中输出的工作。就像早上等地铁的限流,有木有
    • CountDownLatch 是计数器,线程实现一个记录一个,只不过计数不是递增而是递加,而 CyclicBarrier 更像是一个阀门,须要所有线程都达到,阀门能力关上,而后继续执行。

    说说线程平安的容器

    • ConcurrentHashMap: 线程平安的 HashMap
    • CopyOnWriteArrayList: 线程平安的 List,在读多写少的场合性能十分好,远远好于 Vector.
    • CopyOnWriteArrayList 类的所有可变操作(add,set 等等)都是通过创立底层数组的新副原本实现的。当 List 须要被批改的时候,我并不批改原有内容,而是对原有数据进行一次复制,将批改的内容写入正本。写完之后,再将批改完的正本替换原来的数据,这样就能够保障写操作不会影响读操作了。

    说说 Atomic 原子类

    • 其实这个也没啥好说,他们其实都是基于 CAS 实现的一些原子类,用法就是很简略,拿来就能够用
    • AtomicInteger AtomicLongArray AtomicReferenceArray AtomicReference 等等。

    聊聊 ThreadLocal 吧

    • 它的设计作用是为每一个应用该变量的线程都提供一个变量值的正本,每个线程都是扭转本人的正本并且不会和其余线程的正本抵触,这样一来,从线程的角度来看,就如同每个线程都领有了该变量。
    • 首先哈 咱们 new 一个 ThreadLocal 变量,而后呢调用它的 set 办法,此时就会获取以后线程,而后通过以后线程获取到 ThreadLocalMap,而后既然是一个 Map,间接调用 set 办法,key 就是以后 Threadlocal 实例,value 就是要存入的值。这样就能够实现每个线程的数据隔离

    那你说说为啥它要搞得这么简单,它为啥不之前用以后线程当 key, 而后 value 当值来设计呢?

    如果是这种设计的话,那么这样的话如果线程很多的话,那么这个 map 就会很大,会随线程数减少,而我这样设计的 Thread 外面 ThreadLocalMap 的大小就跟线程多小没有关系了,还是和须要存的货色无关了

    既然你说 ThreadLocalMap 是一个 Map 那你聊聊他底层是怎么样的,他的 hash 碰撞是怎么解决的

    • HashMap 中解决抵触的办法是在数组上结构一个链表构造,抵触的数据挂载到链表上,如果链表长度超过肯定数量则会转化成红黑树。
    • 而 ThreadLocalMap 中并没有链表构造,他只有数组,他的实现就是也是 hash 嘛,而后碰到抵触之后,那就接着往下遍历嘛,数组的遍历,找到一个不为 null 的中央,或者雷同的去插入就好了,因为还是要判断 equals 办法的嘛,哈哈说的很简略,然而源码还是简单的一批哦
    • 还有就是应用实现之后,记得 remove 一下。

    说说 Callable 与 Runnable Future

    • java.lang.Runnable 是一个接口,在它外面只申明了一个 run()办法,run 返回值是 void,工作执行结束后无奈返回任何后果
    • Callable 位于 java.util.concurrent 包下,它也是一个接口,在它外面也只申明了一个办法叫做 call(),这是一个泛型接口,call()函数返回的类型就是传递进来的 V 类型
    • 首先呢?future 是多线程有返回后果的一种,它的应用形式,第一种就是 callback, 第二种就是 futureTask

      理解 CompletableFuture, 说说它的用法

      • 这个是 Java8 的个性,为了补救 Future 的毛病,即异步的工作实现后,须要用其后果持续操作时,无需期待。能够间接通过 thenAccept、thenApply、thenCompose 等形式将后面异步解决的后果交给另外一个异步事件处理线程来解决。这种形式才是咱们须要的异步解决。一个控制流的多个异步事件处理能无缝的连贯在一起。
    • 在 Java8 中,CompletableFuture 提供了十分弱小的 Future 的扩大性能,能够帮忙咱们简化异步编程的复杂性,并且提供了函数式编程的能力,能够通过回调的形式解决计算结果,也提供了转换和组合 CompletableFuture 的办法。
    • 具体用法,比方生产一个线程的后果,转换,聚合等等。

    理解线程池嘛,说说线程池的益处

    • 池化技术的思维次要是为了缩小每次获取资源的耗费,进步对资源的利用率。
    • 升高资源耗费。通过反复利用已创立的线程升高线程创立和销毁造成的耗费。
    • 进步响应速度。当工作达到时,工作能够不须要的等到线程创立就能立刻执行。
    • 进步线程的可管理性。线程是稀缺资源,如果无限度的创立,不仅会耗费系统资源,还会升高零碎的稳定性,应用线程池能够进行对立的调配,调优和监控。

    聊聊线程池 ThreadPoolExecutor,它的参数的意义

    • corePoolSize : 外围线程数线程数定义了最小能够同时运行的线程数量。
    • maximumPoolSize : 当队列中寄存的工作达到队列容量的时候,以后能够同时运行的线程数量变为最大线程数。
    • workQueue: 当新工作来的时候会先判断以后运行的线程数量是否达到外围线程数,如果达到的话,新工作就会被寄存在队列中。
    • keepAliveTime: 当线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的工作提交,外围线程外的线程不会立刻销毁,而是会期待,直到期待的工夫超过了 keepAliveTime 才会被回收销毁;
    • unit : keepAliveTime 参数的工夫单位。
    • threadFactory :executor 创立新线程的时候会用到。
    • handler : 饱和策略。对于饱和策略上面独自介绍一下。
    正文完
     0