前言
文本已收录至我的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 :饱和策略。对于饱和策略上面独自介绍一下。