前言
集体收藏的80道Java多线程/并发经典面试题,因为篇幅太长,当初先给出1-10的答案解析哈,前面一起欠缺,并且上传github哈~
https://github.com/whx123/Jav...
公众号:捡田螺的小男孩
1. synchronized的实现原理以及锁优化?
synchronized的实现原理
- synchronized作用于办法或者代码块,保障被润饰的代码在同一时间只能被一个线程拜访。
- synchronized润饰代码块时,JVM采纳monitorenter、monitorexit两个指令来实现同步
- synchronized润饰同步办法时,JVM采纳ACC_SYNCHRONIZED标记符来实现同步
- monitorenter、monitorexit或者ACC_SYNCHRONIZED都是基于Monitor实现的
- 实例对象里有对象头,对象头外面有Mark Word,Mark Word指针指向了monitor
- Monitor其实是一种同步工具,也能够说是一种同步机制。
- 在Java虚拟机(HotSpot)中,Monitor是由ObjectMonitor实现的。ObjectMonitor体现出Monitor的工作原理~
ObjectMonitor() { _header = NULL; _count = 0; // 记录线程获取锁的次数 _waiters = 0, _recursions = 0; //锁的重入次数 _object = NULL; _owner = NULL; // 指向持有ObjectMonitor对象的线程 _WaitSet = NULL; // 处于wait状态的线程,会被退出到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 处于期待锁block状态的线程,会被退出到该列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor的几个要害属性 _count、_recursions、_owner、_WaitSet、 _EntryList 体现了monitor的工作原理
锁优化
在探讨锁优化前,先看看JAVA对象头(32位JVM)中Mark Word的结构图吧~
Mark Word存储对象本身的运行数据,如哈希码、GC分代年龄、锁状态标记、偏差工夫戳(Epoch) 等,为什么辨别偏差锁、轻量级锁、重量级锁等几种锁状态呢?
在JDK1.6之前,synchronized的实现间接调用ObjectMonitor的enter和exit,这种锁被称之为重量级锁。从JDK6开始,HotSpot虚拟机开发团队对Java中的锁进行优化,如减少了适应性自旋、锁打消、锁粗化、轻量级锁和偏差锁等优化策略。
- 偏差锁:在无竞争的状况下,把整个同步都打消掉,CAS操作都不做。
- 轻量级锁:在没有多线程竞争时,绝对重量级锁,缩小操作系统互斥量带来的性能耗费。然而,如果存在锁竞争,除了互斥量自身开销,还额定有CAS操作的开销。
- 自旋锁:缩小不必要的CPU上下文切换。在轻量级锁降级为重量级锁时,就应用了自旋加锁的形式
- 锁粗化:将多个间断的加锁、解锁操作连贯在一起,扩大成一个范畴更大的锁。
举个例子,买门票进动物园。老师带一群小朋友去参观,验票员如果晓得他们是个个体,就能够把他们看成一个整体(锁租化),一次性验票过,而不须要一个个找他们验票。
- 锁打消:虚拟机即时编译器在运行时,对一些代码上要求同步,然而被检测到不可能存在共享数据竞争的锁进行削除。
有趣味的敌人们能够看看我这篇文章:
Synchronized解析——如果你违心一层一层剥开我的心
2. ThreadLocal原理,应用留神点,利用场景有哪些?
答复四个次要点:
- ThreadLocal是什么?
- ThreadLocal原理
- ThreadLocal应用留神点
- ThreadLocal的利用场景
ThreadLocal是什么?
ThreadLocal,即线程本地变量。如果你创立了一个ThreadLocal变量,那么拜访这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,理论是操作本人本地内存外面的变量,从而起到线程隔离的作用,防止了线程平安问题。
//创立一个ThreadLocal变量static ThreadLocal<String> localVariable = new ThreadLocal<>();
ThreadLocal原理
ThreadLocal内存结构图:
由结构图是能够看出:
- Thread对象中持有一个ThreadLocal.ThreadLocalMap的成员变量。
- ThreadLocalMap外部保护了Entry数组,每个Entry代表一个残缺的对象,key是ThreadLocal自身,value是ThreadLocal的泛型值。
对照着几段要害源码来看,更容易了解一点哈~
public class Thread implements Runnable { //ThreadLocal.ThreadLocalMap是Thread的属性 ThreadLocal.ThreadLocalMap threadLocals = null;}
ThreadLocal中的要害办法set()和get()
public void set(T value) { Thread t = Thread.currentThread(); //获取以后线程t ThreadLocalMap map = getMap(t); //依据以后线程获取到ThreadLocalMap if (map != null) map.set(this, value); //K,V设置到ThreadLocalMap中 else createMap(t, value); //创立一个新的ThreadLocalMap } public T get() { Thread t = Thread.currentThread();//获取以后线程t ThreadLocalMap map = getMap(t);//依据以后线程获取到ThreadLocalMap if (map != null) { //由this(即ThreadLoca对象)失去对应的Value,即ThreadLocal的泛型值 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
ThreadLocalMap的Entry数组
static class ThreadLocalMap { static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }}
所以怎么答复ThreadLocal的实现原理?如下,最好是能联合以上结构图一起阐明哈~
- Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程都有一个属于本人的ThreadLocalMap。
- ThreadLocalMap外部保护着Entry数组,每个Entry代表一个残缺的对象,key是ThreadLocal自身,value是ThreadLocal的泛型值。
- 每个线程在往ThreadLocal里设置值的时候,都是往本人的ThreadLocalMap里存,读也是以某个ThreadLocal作为援用,在本人的map里找对应的key,从而实现了线程隔离。
ThreadLocal 内存泄露问题
先看看一下的TreadLocal的援用示意图哈,
ThreadLocalMap中应用的 key 为 ThreadLocal 的弱援用,如下
弱援用:只有垃圾回收机制一运行,不论JVM的内存空间是否短缺,都会回收该对象占用的内存。
弱援用比拟容易被回收。因而,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,然而因为ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会呈现这种状况:ThreadLocalMap的key没了,value还在,这就会造成了内存透露问题。
如何解决内存透露问题?应用完ThreadLocal后,及时调用remove()办法开释内存空间。
ThreadLocal的利用场景
- 数据库连接池
- 会话治理中应用
3. synchronized和ReentrantLock的区别?
我记得校招的时候,这道面试题呈现的频率还是挺高的~能够从锁的实现、性能特点、性能等几个维度去答复这个问题,
- 锁的实现: synchronized是Java语言的关键字,基于JVM实现。而ReentrantLock是基于JDK的API层面实现的(个别是lock()和unlock()办法配合try/finally 语句块来实现。)
- 性能: 在JDK1.6锁优化以前,synchronized的性能比ReenTrantLock差很多。然而JDK6开始,减少了适应性自旋、锁打消等,两者性能就差不多了。
- 性能特点: ReentrantLock 比 synchronized 减少了一些高级性能,如期待可中断、可实现偏心锁、可实现选择性告诉。
- ReentrantLock提供了一种可能中断期待锁的线程的机制,通过lock.lockInterruptibly()来实现这个机制。
- ReentrantLock能够指定是偏心锁还是非偏心锁。而synchronized只能是非偏心锁。所谓的偏心锁就是先期待的线程先取得锁。
- synchronized与wait()和notify()/notifyAll()办法联合实现期待/告诉机制,ReentrantLock类借助Condition接口与newCondition()办法实现。
- ReentrantLock须要手工申明来加锁和开释锁,个别跟finally配合开释锁。而synchronized不必手动开释锁。
4. 说说CountDownLatch与CyclicBarrier区别
- CountDownLatch:一个或者多个线程,期待其余多个线程实现某件事情之后能力执行;
- CyclicBarrier:多个线程相互期待,直到达到同一个同步点,再持续一起执行。
举个例子吧:
- CountDownLatch:假如老师跟同学约定周末在公园门口汇合,等人齐了再发门票。那么,发门票(这个主线程),须要等各位同学都到齐(多个其余线程都实现),能力执行。
- CyclicBarrier:多名长跑运动员要开始田径比赛,只有等所有运动员筹备好,裁判才会鸣枪开始,这时候所有的运动员才会疾步如飞。
5. Fork/Join框架的了解
Fork/Join框架是Java7提供的一个用于并行执行工作的框架,是一个把大工作宰割成若干个小工作,最终汇总每个小工作后果后失去大工作后果的框架。
Fork/Join框架须要了解两个点,分而治之和工作窃取算法。
分而治之
以上Fork/Join框架的定义,就是分而治之思维的体现啦
工作窃取算法
把大工作拆分成小工作,放到不同队列执行,交由不同的线程别离执行时。有的线程优先把本人负责的工作执行完了,其余线程还在慢慢悠悠解决本人的工作,这时候为了充沛提高效率,就须要工作偷盗算法啦~
工作偷盗算法就是,某个线程从其余队列中窃取工作进行执行的过程。个别就是指做得快的线程(偷盗线程)抢慢的线程的工作来做,同时为了缩小锁竞争,通常应用双端队列,即快线程和慢线程各在一端。
6. 为什么咱们调用start()办法时会执行run()办法,为什么咱们不能间接调用run()办法?
看看Thread的start办法阐明哈~
/** * Causes this thread to begin execution; the Java Virtual Machine * calls the <code>run</code> method of this thread. * <p> * The result is that two threads are running concurrently: the * current thread (which returns from the call to the * <code>start</code> method) and the other thread (which executes its * <code>run</code> method). * <p> * It is never legal to start a thread more than once. * In particular, a thread may not be restarted once it has completed * execution. * * @exception IllegalThreadStateException if the thread was already * started. * @see #run() * @see #stop() */ public synchronized void start() { ...... }
JVM执行start办法,会另起一条线程执行thread的run办法,这才起到多线程的成果~ 为什么咱们不能间接调用run()办法?
如果间接调用Thread的run()办法,其办法还是运行在主线程中,没有起到多线程成果。
7. CAS?CAS 有什么缺点,如何解决?
CAS,Compare and Swap,比拟并替换;
CAS 波及3个操作数,内存地址值V,预期原值A,新值B;
如果内存地位的值V与预期原A值相匹配,就更新为新值B,否则不更新
CAS有什么缺点?
ABA 问题
并发环境下,假如初始条件是A,去批改数据时,发现是A就会执行批改。然而看到的尽管是A,两头可能产生了A变B,B又变回A的状况。此时A曾经非彼A,数据即便胜利批改,也可能有问题。
能够通过AtomicStampedReference解决ABA问题,它,一个带有标记的原子援用类,通过管制变量值的版本来保障CAS的正确性。
循环工夫长开销
自旋CAS,如果始终循环执行,始终不胜利,会给CPU带来十分大的执行开销。
很多时候,CAS思维体现,是有个自旋次数的,就是为了避开这个耗时问题~
只能保障一个变量的原子操作。
CAS 保障的是对一个变量执行操作的原子性,如果对多个变量操作时,CAS 目前无奈间接保障操作的原子性的。
能够通过这两个形式解决这个问题:
- 应用互斥锁来保障原子性;
- 将多个变量封装成对象,通过AtomicReference来保障原子性。
有趣味的敌人能够看看我之前的这篇实战文章哈~
CAS乐观锁解决并发问题的一次实际
9. 如何保障多线程下i++ 后果正确?
- 应用循环CAS,实现i++原子操作
- 应用锁机制,实现i++原子操作
- 应用synchronized,实现i++原子操作
没有代码demo,感觉是没有灵魂的~ 如下:
/** * @Author 捡田螺的小男孩 */public class AtomicIntegerTest { private static AtomicInteger atomicInteger = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { testIAdd(); } private static void testIAdd() throws InterruptedException { //创立线程池 ExecutorService executorService = Executors.newFixedThreadPool(2); for (int i = 0; i < 1000; i++) { executorService.execute(() -> { for (int j = 0; j < 2; j++) { //自增并返回以后值 int andIncrement = atomicInteger.incrementAndGet(); System.out.println("线程:" + Thread.currentThread().getName() + " count=" + andIncrement); } }); } executorService.shutdown(); Thread.sleep(100); System.out.println("最终后果是 :" + atomicInteger.get()); } }
运行后果:
...线程:pool-1-thread-1 count=1997线程:pool-1-thread-1 count=1998线程:pool-1-thread-1 count=1999线程:pool-1-thread-2 count=315线程:pool-1-thread-2 count=2000最终后果是 :2000
10. 如何检测死锁?怎么预防死锁?死锁四个必要条件
死锁是指多个线程因竞争资源而造成的一种相互期待的僵局。如图感受一下:
死锁的四个必要条件:
- 互斥:一次只有一个过程能够应用一个资源。其余过程不能拜访已调配给其余过程的资源。
- 占有且期待:当一个过程在期待调配失去其余资源时,其持续占有已调配失去的资源。
- 非抢占:不能强行抢占过程中已占有的资源。
- 循环期待:存在一个关闭的过程链,使得每个资源至多占有此链中下一个过程所须要的一个资源。
如何预防死锁?
- 加锁程序(线程按程序办事)
- 加锁时限 (线程申请所加上权限,超时就放弃,同时开释本人占有的锁)
- 死锁检测
参考与感激
牛顿说,我之所以看得远,是因为我站在伟人的肩膀上~ 谢谢以下各位前辈哈~
- 面试必问的CAS,你懂了吗?
- Java多线程:死锁
- ReenTrantLock可重入锁(和synchronized的区别)总结
- 聊聊并发(八)——Fork/Join 框架介绍
集体公众号
- 感觉写得好的小伙伴给个点赞+关注啦,谢谢~
- 如果有写得不正确的中央,麻烦指出,感激不尽。
- 同时十分期待小伙伴们可能关注我公众号,前面缓缓推出更好的干货~嘻嘻
- github地址:https://github.com/whx123/Jav...